Skip to content

Instantly share code, notes, and snippets.

@eestrada
Last active September 5, 2022 21:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eestrada/0ee33f1f027332e1c43f to your computer and use it in GitHub Desktop.
Save eestrada/0ee33f1f027332e1c43f to your computer and use it in GitHub Desktop.
Python chmod helper functions.
from __future__ import division, absolute_import, print_function
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>
# This code was originally written by Ethan Estrada (https://gist.github.com/eestrada)
# and hosted at: https://gist.github.com/eestrada/0ee33f1f027332e1c43f
# Author attribution is appreciated, but (as per the public domain dedication above) not required.
"""The missing chmod functions for Python."""
import os
import re
import stat
import logging
import operator
log = logging.getLogger('chmod')
def rchmod(dir, mode, fmode=None, action=0, topdown=True, ignore_errors=False):
"""Recursively chmod a directory.
A separate mode can be used for files than for directories. This is useful
since directories should usually be marked as executable, but files should
usually not.
:param dir: the root directory to recursively chmod
:param mode: the mode to use on directories and files
:param fmode: if not None, the mode to use for files that replaces the
`mode` argument
:param action: How to use the given mode(s). A value equal to 0 (the
default) means ignore the existing permissions on the
files/directories and simply replace them with the given mode(s).
A value greater than 0 means do a bitwise OR on the existing
permissions, effectively adding them to the existing permission set.
A value less than 0 means do a complement of the given mode(s) and
then bitwise AND on the existing permissions, effectively stripping
them from the the existing permission set.
:param topdown: Whether to modify files top down or bottom up. Which one
is more desirable in a given situation depends on whether you are
making permissions more or less restrictive as you walk the directory
structure.
:param ignore_errors: Whether to ignore OSErrors when running to os.chmod
"""
fmode = mode if fmode is None else fmode
def chmod_builder(bitwise_action):
def chmod_wrapper(path, mode, os_chmod=os.chmod):
# strip out non-permission bits from st_mode
perms_only = os.stat(path).st_mode & ~0o770000
new_mode = bitwise_action(perms_only, mode)
os_chmod(path, new_mode)
return chmod_wrapper
if action == 0:
chmod = os.chmod
elif action > 0:
chmod = chmod_builder(operator.__or__)
elif action < 0:
# equivalent to `a & ~b`
chmod = chmod_builder(lambda a, b: operator.__and__(a, operator.__invert__(b)))
else:
raise ValueError('Unusable action value {0}'.format(action))
if topdown:
chmod(dir, mode)
for root, dirs, files in os.walk(dir, topdown=topdown):
for d in dirs:
path = os.path.join(root, d)
if os.path.islink(path):
continue
try:
chmod(path, mode)
except Exception:
if not ignore_errors:
raise
log.exception('')
for f in files:
path = os.path.join(root, f)
if os.path.islink(path):
continue
try:
chmod(path, fmode)
except Exception:
if not ignore_errors:
raise
log.exception('')
if not topdown:
chmod(dir, mode)
def mode2str(mode):
"""Convert chmod mode integer to a human readable string.
Useful for debugging purposes.
"""
raise NotImplementedError
_chmod_flags = {}
_chmod_flags['us'] = stat.S_ISUID # Set UID bit.
_chmod_flags['gs'] = stat.S_ISGID # Set-group-ID bit. This bit has several special uses. For a directory it indicates that BSD semantics is to be used for that directory: files created there inherit their group ID from the directory, not from the effective group ID of the creating process, and directories created there will also get the S_ISGID bit set. For a file that does not have the group execution bit (S_IXGRP) set, the set-group-ID bit indicates mandatory file/record locking (see also S_ENFMT).
_chmod_flags['t'] = stat.S_ISVTX # Sticky bit. When this bit is set on a directory it means that a file in that directory can be renamed or deleted only by the owner of the file, by the owner of the directory, or by a privileged process.
_chmod_flags['ut'] = _chmod_flags['gt'] = _chmod_flags['ot'] = _chmod_flags['t']
_chmod_flags['u'] = stat.S_IRWXU # Mask for file owner permissions.
_chmod_flags['ur'] = stat.S_IRUSR # Owner has read permission.
_chmod_flags['uw'] = stat.S_IWUSR # Owner has write permission.
_chmod_flags['ux'] = stat.S_IXUSR # Owner has execute permission.
_chmod_flags['g'] = stat.S_IRWXG # Mask for group permissions.
_chmod_flags['gr'] = stat.S_IRGRP # Group has read permission.
_chmod_flags['gw'] = stat.S_IWGRP # Group has write permission.
_chmod_flags['gx'] = stat.S_IXGRP # Group has execute permission.
_chmod_flags['o'] = stat.S_IRWXO # Mask for permissions for others (not in group).
_chmod_flags['or'] = stat.S_IROTH # Others have read permission.
_chmod_flags['ow'] = stat.S_IWOTH # Others have write permission.
_chmod_flags['ox'] = stat.S_IXOTH # Others have execute permission.
_chmod_re = re.compile(r'\A\s*(?P<who>[ugo]+|a)(?P<how>[+-=])(?P<what>[rwxXst]+)\s*\Z')
def str2mode(str, mode=None):
"""Convert chmod symbolic string into its integer equivalent.
Calculations are done on a default octal value. The value dependencies
on the operation. If it is `-` then the defaul value is `0777`. However, if
the operation is `+` then the default value is `0000` (meaning it behaves
identically to `=` when using the defaul value). An explicit
mode integer may be passed in instead to do the calculations against.
"""
match = _chmod_re.match(str)
if not match:
raise ValueError('Not an acceptable chmod string "%s"' % str)
who = set(match.group('who')) # who are we affecting
how = match.group('how') # how are we affecting them
what = set(match.group('what')) # what are we going to change about them
if 'a' in who:
who = set(['u', 'g', 'o'])
tmp_mode = 0o0000
log.debug(oct(tmp_mode))
for w in who:
for t in what:
log.debug(w+t)
tmp_mode |= _chmod_flags.get(w+t, 0o0)
log.debug(oct(tmp_mode))
log.debug(oct(mode) if mode is not None else mode)
if how == '=':
# always ignore the mode parameter when doing '='
mode = 0o0000 | tmp_mode
elif how == '+':
if mode is None:
mode = 0o0000
mode |= tmp_mode
elif how == '-':
if mode is None:
mode = 0o0777
mode &= ~tmp_mode
else:
pass
log.debug(oct(mode) if mode is not None else mode)
return mode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment