Metadata-Version: 2.5
Name: oschmod
Version: 0.5.0
Summary: Consistent chmod-style file permissions on Windows, macOS, and Linux
Project-URL: Homepage, https://github.com/YakDriver/oschmod
Project-URL: Issues, https://github.com/YakDriver/oschmod/issues
Project-URL: Changelog, https://github.com/YakDriver/oschmod/blob/main/CHANGELOG.md
Author-email: YakDriver <dirk.avery@GEEmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: acl,chmod,file mode,permissions,windows
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: System :: Filesystems
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: pywin32>=311; sys_platform == 'win32'
Provides-Extra: dev
Requires-Dist: bump-my-version==1.6.7; extra == 'dev'
Requires-Dist: mypy==2.4.0; extra == 'dev'
Requires-Dist: pytest-cov==7.1.0; extra == 'dev'
Requires-Dist: pytest==9.1.1; extra == 'dev'
Requires-Dist: ruff==0.17.3; extra == 'dev'
Description-Content-Type: text/markdown

<p>
    <a href="./LICENSE" alt="License">
        <img src="https://img.shields.io/github/license/YakDriver/oschmod.svg" /></a>
    <a href="https://github.com/YakDriver/oschmod/actions/workflows/ci.yml" alt="Build status">
        <img src="https://github.com/YakDriver/oschmod/actions/workflows/ci.yml/badge.svg?branch=main" /></a>
    <a href="https://pypi.python.org/pypi/oschmod" alt="Python versions">
        <img src="https://img.shields.io/pypi/pyversions/oschmod.svg" /></a>
    <a href="https://pypi.python.org/pypi/oschmod" alt="Version">
        <img src="https://img.shields.io/pypi/v/oschmod.svg" /></a>
</p>

# oschmod

***oschmod*** sets consistent file permissions across Windows, Linux and macOS.

## oschmod TL;DR

***oschmod*** brings `chmod` functionality to **Windows**, macOS, and Linux! If you're not familiar, `chmod` is a handy macOS and Linux-only tool for setting file permissions.

Prior to ***oschmod***, Windows file permissions couldn't be set in the familiar `chmod` way. Tools did not translate `chmod`-style permissions into Windows-style file permissions. Even though Python's `os.chmod()` sets read, write, and execute file permissions, on Windows, `os.chmod()` basically has no effect. Even worse, Python on Windows gives no warnings or errors. If you think you set file permissions on Windows with `os.chmod()`, you're wrong!

***oschmod*** allows you to set consistent file permissions in a consistent way across platforms.

* Read more about [oschmod](https://medium.com/@dirk.avery/securing-files-on-windows-macos-and-linux-7b2b9899992) on Medium
* For more background, have a look at the [oschmod Wiki](https://github.com/YakDriver/oschmod/wiki).

## Installation

```console
$ pip install oschmod
```

Or with conda:

```console
$ conda install -c conda-forge oschmod
```

***oschmod*** requires Python 3.11 or newer. On Windows, it depends on `pywin32`, which is installed automatically.

## GNU Documentation

***oschmod*** changes the file mode bits of each given file according to mode, which can be either a symbolic representation of changes to make, or an octal number representing the bit pattern for the new mode bits.

The format of a symbolic mode is `[ugoa...][+-=][perms...]` where perms is zero or more letters from the set `rwx`. Multiple symbolic modes can be given, separated by commas.

A combination of the letters `ugoa` controls which users' access to the file will be changed: the user who owns it (`u`), other users in the file's group (`g`), other users not in the file's group (`o`), or all users (`a`). If none of these are given, the effect is as if `a` were given.

*(Modified from the GNU manpage for chmod.)*

## Command line interface

***oschmod*** brings the ability to set consistent file permissions using the command line to Windows, macOS, and Linux platforms. If you are familiar with `chmod`, ***oschmod*** works similarly, albeit with fewer options.

```console
$ oschmod -h
usage: oschmod [-h] [-R] [-H | -L | -P] [-v] [--other-sid SID] [--version]
               mode object

Change the mode (permissions) of a file or directory

positional arguments:
  mode             octal or symbolic mode of the object
  object           file or directory

options:
  -h, --help       show this help message and exit
  -R               apply mode recursively
  -H               with -R, follow the object if it is a symlink, but no links
                   below it (default)
  -L               with -R, follow every symlink (changes link targets, even
                   outside the tree)
  -P               with -R, follow no symlinks
  -v               verbose: show each object as its mode is set; repeat (-vv)
                   to also show old and new modes
  --other-sid SID  Windows only: SID that receives "other" permissions
                   (default: S-1-5-32-545, the built-in Users group)
  --version        show program's version number and exit
```

You can also run it as `python -m oschmod`.

## Command line examples

You can use symbolic (e.g., "u+rw") or octal (e.g., "600") representations of modes. Multiple mode modifications can be made in a single call by separating modifiers with commas.

### Symbolic representation examples

Symbolic representation mode modifiers have three parts:

1. **whom:** To whom does the modification apply? You can include zero or more of `[ugoa]*` where `a` is for all, `u` is for the file owner (i.e., "**u**ser"), `g` is for the file group, and `o` is for others. In other words, `ugo` is equivalent to `a`. Also, if you do not provide a "whom," ***oschmod*** assumes you mean `a` (everyone).
2. **operation:** Which operation should be applied? You must include one and only one operation, `[+-=]{1}`, per modifier (although you can have multiple modifiers). `+` adds permissions, `-` removes permissions, and `=` sets permissions regardless of previous permissions. `+` and `-` modifications often depend on the current permissions.
3. **permission:** Which permission or permissions will be affected? You can include zero or more of `[rwx]*` where `r` is for read, `w` is for write, and `x` is for execute. If you do not include a permission with `+` or `-` (e.g., `u-`), the modifier has no effect. However, if you use no permissions with `=` (e.g., `o=`), all permissions are removed.

**Example 1:** To give everyone execute permissions on a file (all of these are equivalent):

```console
$ oschmod +x <file name>
$ oschmod a+x <file name>
$ oschmod ugo+x <file name>
```

**Example 2:** To remove read, write, and execute permissions from the file group and all others (these are equivalent):

```console
$ oschmod go-rwx <file name>
$ oschmod go= <file name>
```

**Example 3:** To give the file owner read and execute permissions, and remove execute permissions from the group and all others:

```console
$ oschmod u+rx,go-x <file name>
```

**Example 4:** To give everyone all permissions, and then remove execute write from the group, and execute from all others:

```console
$ oschmod a+rwx,g-w,o-x <file name>
```

### Octal representation examples

For more about what octal representations mean, see [this article](https://medium.com/@dirk.avery/securing-files-on-windows-macos-and-linux-7b2b9899992) on Medium.

**Example 5:** To give everyone read, write, and execute permissions on a file:

```console
$ oschmod 777 <file name>
```

**Example 6:** To lock down a file to just give the file owner read, write, and execute permissions and deny all permissions to everyone else:

```console
$ oschmod 700 <file name>
```

### Verbose output

**Example 7:** Show each object as its mode is set (`-v`), or show old and new modes too (`-vv`):

```console
$ oschmod -vv 600 secrets.txt
secrets.txt: 0o644 (rw-r--r--) -> 0o600 (rw-------)
```

### Recursion and symlinks

With `-R`, symlinks found inside the tree are **not** followed, so nothing outside the tree is changed. This is the same as GNU `chmod -R` and is the safe choice, especially when running as root or Administrator on a directory that others can write to. On Windows, directory junctions are treated the same way as symlinks.

* `-H` (default): if the object you name is a symlink to a directory, follow it, but no links below it.
* `-L`: follow every symlink, changing link targets wherever they are. Use this only for trees you control, for example a deployment directory that links to shared storage. Directory cycles are skipped.
* `-P`: follow no symlinks; a symlink to a directory given on the command line is left alone.

If an object can't be changed, ***oschmod*** prints an error, keeps going, and exits with status 1 at the end. A directory that can't be listed because of its current mode (e.g., `000`) gets its new mode first, so `oschmod -R u+rwx mydir` can repair it.

## Python usage

You can use ***oschmod*** from Python code. Any of the command line examples above will work very similarly. For example, *Example 4* above, in Python code, would look like this:

```python
import oschmod

oschmod.set_mode("myfile", "a+rwx,g-w,o-x")
```

*Example 5* above, in Python code, could be done in two ways:

```python
import oschmod

oschmod.set_mode("myfile", "777")
oschmod.set_mode("myfile", 0o777)
```

***oschmod*** is compatible with bitwise permissions as defined in the `stat` module. To give a file's owner read, write, and execute permissions and deny the group and others any permissions (i.e., equivalent of `700`):

```python
import oschmod
import stat

oschmod.set_mode("myfile", stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
```

Replacing `os.chmod()` with ***oschmod*** should usually be an easy drop-in replacement. Replacement will allow you to get consistent file permission settings on Windows, macOS, and Linux:

If this is your Python code using `os.chmod()`:

```python
import os

os.chmod("myfile1", "u+x")
os.chmod("myfile2", 0o777)
```

The replacement using ***oschmod*** is very similar:

```python
import oschmod

oschmod.set_mode("myfile1", "u+x")
oschmod.set_mode("myfile2", 0o777)
```

`get_mode()` returns only the read/write/execute bits (`0o777`). On macOS and Linux, use `get_full_mode()` to also include set-user-ID, set-group-ID, and sticky bits (`0o7777`). Symbolic `+` and `-` (e.g., `g+w`) leave those special bits as they are. As in GNU chmod, `=` clears them for the classes it sets: `u=` clears set-user-ID, `g=` set-group-ID, and `o=` the sticky bit. Directories keep their set-user-ID and set-group-ID bits.

An invalid mode string raises `oschmod.ModeError`, a `ValueError`.

`set_mode_recursive()` skips symlinks below the path by default. Pass `follow_symlinks=True` to follow them. Errors stop the walk unless you pass `on_error`, which is called with the path and exception for each failure:

```python
import oschmod

failures = []
oschmod.set_mode_recursive(
    "mydir", "u=rw,go=r", "u=rwx,go=rx", on_error=lambda path, exc: failures.append(path)
)
```

Paths can be strings or any path-like object, such as `pathlib.Path`:

```python
from pathlib import Path

import oschmod

oschmod.set_mode(Path("myfile"), 0o600)
```

## Windows notes

Windows has no owner/group/other mode bits, so ***oschmod*** maps them to ACL entries:

* **Owner** is the file's owner SID, and **group** is the file's primary group SID.
* **Others** is the built-in *Users* group (`S-1-5-32-545`) by default. To use a different account, pass `other_sid` (for example, `"S-1-5-11"` for *Authenticated Users*). In Python, use `oschmod.set_mode(path, "o+r", other_sid="S-1-5-11")`; on the command line, use `--other-sid S-1-5-11`.
* Setting a mode replaces the object's DACL with explicit entries and marks it **protected**, so permissions are no longer inherited from the parent folder, including after later changes to the parent. Any existing entry for the local SYSTEM account is kept.
* Set-user-ID, set-group-ID, and sticky bits have no Windows equivalent and are ignored there.
* When reading a mode, only entries for the owner, the group, and the "other" account count. Entries for groups that include everyone in "other" also count: *Everyone* always, and *Authenticated Users* when "other" is the default Users. Entries for any other account, such as *Administrators*, are ignored when reading, and removed when a mode is set (SYSTEM excepted). Pass the same `other_sid` to `get_mode()` that you use with `set_mode()`.
* "Write" lets an account change the file's contents, not its permissions or owner. The owner can always change permissions.
* If one account fills two roles (for example, the owner is also the group), it gets the bits of the first role, in the order owner, group, other, as on POSIX.
* `ACCESS_DENIED` entries are not considered when reading a mode.
