"""
This logic was ported to ub.Path.chmod in 1.3.5, can remove and depend on that
when it is ready.
"""
def _parse_chmod_code(code):
"""
Expand a chmod code into a list of actions.
Args:
code (str): of the form: [ugoa…][-+=]perms…[,…]
perms is either zero or more letters from the set rwxXst, or a
single letter from the set ugo.
Yields:
Tuple[str, str, str]: target, op, and perms.
The target is modified by the operation using the value.
target -- specified 'u' for user, 'g' for group, 'o' for other.
op -- specified as '+' to add, '-' to remove, or '=' to assign.
val -- specified as 'r' for read, 'w' for write, or 'x' for execute.
Example:
>>> print(list(_parse_chmod_code('ugo+rw,+r,g=rwx')))
>>> print(list(_parse_chmod_code('o+x')))
>>> print(list(_parse_chmod_code('u-x')))
>>> print(list(_parse_chmod_code('x')))
>>> print(list(_parse_chmod_code('ugo+rwx')))
[('ugo', '+', 'rw'), ('ugo', '+', 'r'), ('g', '=', 'rwx')]
[('o', '+', 'x')]
[('u', '-', 'x')]
[('u', '+', 'x')]
[('ugo', '+', 'rwx')]
>>> import pytest
>>> with pytest.raises(ValueError):
>>> list(_parse_chmod_code('a+b+c'))
"""
import re
pat = re.compile(r'([\+\-\=])')
parts = code.split(',')
for part in parts:
ab = pat.split(part)
len_ab = len(ab)
if len_ab == 3:
targets, op, perms = ab
elif len_ab == 1:
perms = ab[0]
op = '+'
targets = 'u'
else:
raise ValueError('unknown chmod code pattern: part={part}')
if targets == '' or targets == 'a':
targets = 'ugo'
yield (targets, op, perms)
def _resolve_chmod_code(old_mode, code):
"""
Modifies integer stat permissions based on a string code.
Args:
old_mode (int): old mode from st_stat
code (str): chmod style codeold mode from st_stat
Returns:
int : new code
Example:
>>> print(oct(_resolve_chmod_code(0, '+rwx')))
>>> print(oct(_resolve_chmod_code(0, 'ugo+rwx')))
>>> print(oct(_resolve_chmod_code(0, 'a-rwx')))
>>> print(oct(_resolve_chmod_code(0, 'u+rw,go+r,go-wx')))
>>> print(oct(_resolve_chmod_code(0o777, 'u+rw,go+r,go-wx')))
0o777
0o777
0o0
0o644
0o744
>>> import pytest
>>> with pytest.raises(NotImplementedError):
>>> print(oct(_resolve_chmod_code(0, 'u=rw')))
>>> with pytest.raises(ValueError):
>>> _resolve_chmod_code(0, 'u?w')
"""
import stat
import itertools as it
action_lut = {
# TODO: handle suid, sgid, and sticky?
# suid = stat.S_ISUID
# sgid = stat.S_ISGID
# sticky = stat.S_ISVTX
'ur' : stat.S_IRUSR,
'uw' : stat.S_IWUSR,
'ux' : stat.S_IXUSR,
'gr' : stat.S_IRGRP,
'gw' : stat.S_IWGRP,
'gx' : stat.S_IXGRP,
'or' : stat.S_IROTH,
'ow' : stat.S_IWOTH,
'ox' : stat.S_IXOTH,
}
actions = _parse_chmod_code(code)
new_mode = int(old_mode) # (could optimize to modify inplace if needed)
for action in actions:
targets, op, perms = action
try:
action_keys = (target + perm for target, perm in it.product(targets, perms))
action_values = (action_lut[key] for key in action_keys)
action_values = list(action_values)
if op == '+':
for val in action_values:
new_mode |= val
elif op == '-':
for val in action_values:
new_mode &= (~val)
elif op == '=':
raise NotImplementedError(f'new chmod code for op={op}')
else:
raise AssertionError(
f'should not be able to get here. unknown op code: op={op}')
except KeyError:
# Give a better error message if something goes wrong
raise ValueError(f'Unknown action: {action}')
return new_mode
def _encode_chmod_int(int_code):
"""
Convert a chmod integer code to a string
Currently unused, but may be useful in the future.
Example:
>>> int_code = 0o744
>>> print(_encode_chmod_int(int_code))
u=rwx,g=r,o=r
"""
import stat
action_lut = {
'ur' : stat.S_IRUSR,
'uw' : stat.S_IWUSR,
'ux' : stat.S_IXUSR,
'gr' : stat.S_IRGRP,
'gw' : stat.S_IWGRP,
'gx' : stat.S_IXGRP,
'or' : stat.S_IROTH,
'ow' : stat.S_IWOTH,
'ox' : stat.S_IXOTH,
}
from collections import defaultdict
target_to_perms = defaultdict(list)
for key, val in action_lut.items():
target, perm = key
if int_code & val:
target_to_perms[target].append(perm)
parts = [k + '=' + ''.join(vs) for k, vs in target_to_perms.items()]
code = ','.join(parts)
return code
[docs]
def new_chmod(path, code):
"""
dpath = ub.Path.appdir('util/chmod/test').ensuredir()
path = (dpath / 'file').touch()
code = 'g+x,g+r'
new_chmod(path, code)
import stat
stat.filemode(path.stat().st_mode)
"""
old_mode = path.stat().st_mode
new_mode = _resolve_chmod_code(old_mode, code)
path.chmod(new_mode)