Skip to content

Instantly share code, notes, and snippets.

@haxwithaxe
Created April 2, 2026 18:37
Show Gist options
  • Select an option

  • Save haxwithaxe/5a61a13a074147cfa856bb6b7ab9ef79 to your computer and use it in GitHub Desktop.

Select an option

Save haxwithaxe/5a61a13a074147cfa856bb6b7ab9ef79 to your computer and use it in GitHub Desktop.
Snippet for converting octal to symbolic permissions and symbolic to octal
"""A snippet for converting between permissions formats.
These aren't complete compared to `chmod`. They don't account for the sticky
bit, set user/group ID, or extra leading characters on octal modes.
symbolic_to_octal: Convert a set of symbolic mode permissions to its octal
string equivalent.
symbolic_to_int: Same as `symbolic_to_octal` but returns an `int` instead of an
octal string.
octal_to_symbolic: Convert octal mode permissions to symbolic mode permissions.
"""
import re
from numbers import Number
def octal_to_symbolic(octal: Number | str, dir_only: bool = False) -> str:
"""Return the symbolic equivalent of an octal permissions mode.
Arguments:
octal: If this is given as a number it is treated as base10. If this is
given as a string it is treated as an octal string. Octal strings
can have leading a ``0o`` or not.
dir_only: If `True` the execute bit character is changed to ``X``
instead of ``x`` to prevent setting execute bits on child files.
"""
if isinstance(octal, Number):
octal = oct(int(octal))
if 'o' in octal:
octal = octal.split('o')[-1].zfill(4)
execute_char = 'X' if dir_only else 'x'
result = {'u': '', 'g': '', 'o': ''}
octal_chars = [int(n) for n in str(octal).zfill(4)]
for set_num, perm_int in enumerate(octal_chars, start=-1):
if set_num == -1:
continue
set_char = list(result.keys())[set_num]
if perm_int == 7:
result[set_char] = f'rw{execute_char}'
continue
if perm_int == 6:
result[set_char] = 'rw'
continue
if perm_int == 5:
result[set_char] = f'r{execute_char}'
continue
if perm_int == 4:
result[set_char] = 'r'
continue
if perm_int == 3:
result[set_char] = f'w{execute_char}'
continue
if perm_int == 2:
result[set_char] = 'w'
continue
if perm_int == 1:
result[set_char] = f'{execute_char}'
continue
return f'u={result["u"]},g={result["g"]},o={result["o"]}'
def reconsile_symbolic_set(perm_sets_for) -> dict[str, bool | None]:
"""Combine a list of permissions ([r|w|x|X]+).
Returns:
A `dict` with a key for each permission type. Each value is one of
* `None` - Unset.
* `True` - Set with ``+`` or ``=``.
* `False` - Set with ``-``.
"""
perm_set = {'r': None, 'w': None, 'x': None, 'X': None}
for perm_for, oper, perm_spec in perm_sets_for:
if perm_for not in ('a', 'u', 'g', 'o'):
raise TypeError(f'"{perm_for}" is not a valid permissions set.')
for perm_char in perm_spec:
if oper == '-':
perm_set[perm_char] = False
else:
perm_set[perm_char] = True
return perm_set
def symbolic_to_octal(perms: str, is_dir: bool = False) -> str:
"""Return the octal equivalent of a symbolic permissions mode.
Arguments:
perms: Symbolic permissions as given to `chmod`. Any omitted fields
are set to `0` (as in `<u|g|o>=`). Any relative operations start
from `0`. For instance `o+x` resolves to `0100`. Any relative
operations that result in negative values are forced to `0`.
is_dir: If `True` treat ``X`` as ``x``.
"""
if isinstance(perms, Number):
return int(str(int(perms)).zfill(4), 8)
if len(re.findall(r'[=+-]', perms)) < 1:
return int(perms.zfill(4), 8)
# Fill in the defaults after parsing to help detect duplicates
result = {'a': 0o0, 'u': 0o0, 'g': 0o0, 'o': 0o0}
letter_to_int = {"r": 0o4, "w": 0o2, 'x': 0o1, 'X': 0o1}
found = re.findall(r'([augo]+)([=+-])([rwxX]+)?', perms)
perm_sets = {'a': [], 'u': [], 'g': [], 'o': []}
for letter, oper, spec in found:
if len(letter) > 1:
for ltr in letter:
perm_sets[ltr].append((ltr, oper, spec))
else:
perm_sets[letter].append((letter, oper, spec))
# Iterate over each set of permissions
perm_set_for_a = reconsile_symbolic_set(perm_sets['a'])
result = {'u': 0, 'g': 0, 'o': 0}
for perm_for, perm_sets_for in perm_sets.items():
if perm_for == 'a':
continue
for letter, is_set in reconsile_symbolic_set(perm_sets_for).items():
if letter == 'X' and not is_dir:
continue
if is_set is False:
continue
# fmt: off
if (
is_set is True # noqa: SIM114
and perm_set_for_a[letter] is not False
):
# fmt: on
result[perm_for] += letter_to_int[letter]
elif perm_set_for_a[letter] is True:
result[perm_for] += letter_to_int[letter]
return f'0{result["u"]}{result["g"]}{result["o"]}'
def symbolic_to_int(perms, is_dir=False):
"""Return the integer equivalent of a symbolic permissions mode.
Arguments:
perms: Symbolic permissions as given to `chmod`. Any omitted fields
are set to `0` (as in `<u|g|o>=`). Any relative operations start
from `0`. For instance `o+x` resolves to `0100`. Any relative
operations that result in negative values are forced to `0`.
is_dir: If `True` treat ``X`` as ``x``.
"""
return int(symbolic_to_octal(perms, is_dir=is_dir), 8)
# Just tests below here
def _test_symbolic_to_octal() -> None:
# u=rwx == '0700'
print(f'{symbolic_to_octal("u=rwx")=} == "0700"')
assert symbolic_to_octal('u=rwx') == '0700'
# u-r == '0000'
print(f'{symbolic_to_octal('u-r')=} == "0000"')
assert symbolic_to_octal('u-r') == '0000'
# a+rw == '0666'
print(f'{symbolic_to_octal("a+rw")=} == "0666"')
assert symbolic_to_octal('a+rw') == '0666'
# u=r,a=x == '0511'
print(f'{symbolic_to_octal("u=r,a=x")=} == "0511"')
assert symbolic_to_octal('u=r,a=x') == '0511'
# u=r,g=w,o=x == '0421'
print(f'{symbolic_to_octal("u=r,g=w,o=x")=} == "0421"')
assert symbolic_to_octal('u=r,g=w,o=x') == '0421'
# u=r,g=w,o=x,a-x == '0420'
print(f'{symbolic_to_octal("u=r,g=w,o=x,a-x")=} == "0420"')
assert symbolic_to_octal('u=r,g=w,o=x,a-x') == '0420'
# a=rw,u-r,g-r,o+x == '0227'
print(f'{symbolic_to_octal("a=rw,u-r,g-r,o+x")=} == "0227"')
assert symbolic_to_octal('a=rw,u-r,g-r,o+x') == '0227'
# a=rwX not dir == '0666'
print(f'{symbolic_to_octal("a=rwX")=} == "0666"')
assert symbolic_to_octal('a=rwX', is_dir=False) == '0666'
# a=rwX is dir == '0777'
print(f'{symbolic_to_octal("a=rwX")=} == "0777"')
assert symbolic_to_octal('a=rwX', is_dir=True) == '0777'
def _test_octal_to_symbolic() -> None:
# u=rwx == '0700'
print(f'{octal_to_symbolic("0700")=} == "u=rwx,g=,o="')
assert octal_to_symbolic('0700') == 'u=rwx,g=,o='
# g=rwx == '0070'
print(f'{octal_to_symbolic("0070")=} == "u=,g=rwx,o="')
assert octal_to_symbolic('0070') == 'u=,g=rwx,o='
# o=rwx == '0007'
print(f'{octal_to_symbolic("0007")=} == "u=,g=,o=rwx"')
assert octal_to_symbolic('0007') == 'u=,g=,o=rwx'
# o=rw == '0006'
print(f'{octal_to_symbolic("0006")=} == "u=,g=,o=rw"')
assert octal_to_symbolic('0006') == 'u=,g=,o=rw'
# o=rx == '0005'
print(f'{octal_to_symbolic("0005")=} == "u=,g=,o=rx"')
assert octal_to_symbolic('0005') == 'u=,g=,o=rx'
# o=wx == '0003'
print(f'{octal_to_symbolic("0003")=} == "u=,g=,o=wx"')
assert octal_to_symbolic('0003') == 'u=,g=,o=wx'
# g=rw == '0060'
print(f'{octal_to_symbolic("0060")=} == "u=,g=rw,o="')
assert octal_to_symbolic('0060') == 'u=,g=rw,o='
# g=rx == '0050'
print(f'{octal_to_symbolic("0050")=} == "u=,g=rx,o="')
assert octal_to_symbolic('0050') == 'u=,g=rx,o='
# g=wx == '0030'
print(f'{octal_to_symbolic("0030")=} == "u=,g=wx,o="')
assert octal_to_symbolic('0030') == 'u=,g=wx,o='
# u=rw == '0600'
print(f'{octal_to_symbolic("0060")=} == "u=,g=rw,o="')
assert octal_to_symbolic('0060') == 'u=,g=rw,o='
# u=rx == '0500'
print(f'{octal_to_symbolic("0050")=} == "u=,g=rx,o="')
assert octal_to_symbolic('0050') == 'u=,g=rx,o='
# u=wx == '0300'
print(f'{octal_to_symbolic("0030")=} == "u=,g=wx,o="')
assert octal_to_symbolic('0030') == 'u=,g=wx,o='
# u=r,g=r,o=r == '0222'
print(f'{octal_to_symbolic("0444")=} == "u=r,g=r,o=r"')
assert octal_to_symbolic('0444') == 'u=r,g=r,o=r'
# u=w,g=w,o=w == '0444'
print(f'{octal_to_symbolic("0222")=} == "u=w,g=w,o=w"')
assert octal_to_symbolic('0222') == 'u=w,g=w,o=w'
# u=x,g=x,o=x == '0111'
print(f'{octal_to_symbolic("0111")=} == "u=x,g=x,o=x"')
assert octal_to_symbolic('0111') == 'u=x,g=x,o=x'
# u=r,g=w,o=x == '0421'
print(f'{octal_to_symbolic("0421")=} == "u=r,g=w,o=x"')
assert octal_to_symbolic('0421') == 'u=r,g=w,o=x'
print(f'{octal_to_symbolic(oct(0o0421))=} == "u=r,g=w,o=x"')
assert octal_to_symbolic(oct(0o0421)) == 'u=r,g=w,o=x'
print(f'{octal_to_symbolic(0o421)=} == "u=r,g=w,o=x"')
assert octal_to_symbolic(0o421) == 'u=r,g=w,o=x'
# u=,g=,o=x == int(1)
print(f'{octal_to_symbolic(1)=} == "u=,g=,o=x"')
assert octal_to_symbolic(1) == 'u=,g=,o=x'
def _test() -> None:
_test_symbolic_to_octal()
_test_octal_to_symbolic()
print(f'{symbolic_to_int("u=rwx")=} == {0o0700=}')
assert symbolic_to_int('u=rwx') == 0o0700
if __name__ == '__main__':
_test()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment