Created
April 2, 2026 18:37
-
-
Save haxwithaxe/5a61a13a074147cfa856bb6b7ab9ef79 to your computer and use it in GitHub Desktop.
Snippet for converting octal to symbolic permissions and symbolic to octal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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