Metadata-Version: 2.4
Name: epycs
Version: 2.0.0
Summary: Epycs is a simple way to convert shell scripts to python
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pytest>=9.0.1
Dynamic: license-file

# Epycs

Epycs is a simple way to convert shell scripts to python.
It features

  - A simple subprocess API
  - A sane behaviour of exiting by default on subprocess failures
  - A show-output-on-fail behaviour

The goal of this package is to be able to write shell-script equivalent code
in python while still being terse, but adding a tons of goodness in terms of
arithmetical expression, string manipulation, code reuse etc...

Say no to .sh and welcome .py with epycs, you'll thank me later.

# Usage

> This section is a complete, copy-pasteable cheat sheet of the public API.
> If you are an LLM/agent: everything you need to use epycs is below — no need
> to read the source or search the web.

```sh
pip install epycs   # or: uv add epycs
```

## Run a command

`cmd` is a magic shell: any attribute is resolved to a program on `PATH`.

```python
from epycs import cmd

cmd.ls("-l", "/tmp")          # runs `ls -l /tmp`, output goes to the terminal
cmd.git("clone", url, dest)   # args are str()-ed automatically (Path, int, ...)
```

By default a command **prints to the terminal and returns** a
`subprocess.CompletedProcess`. To capture and parse the output instead, use
`out_filter` (see below).

### Default behaviours (the "goodness" over raw subprocess)

* **Exit on failure**: a non-zero return code exits your script with that code
  (like `set -e`). Disable globally with `exit_on_error = False`.
* **Quiet-but-loud-on-fail**: with `quiet=True` output is hidden, *unless* the
  command fails, in which case it is dumped to stderr.
* `text=True` is the default, so captured output is `str`, not `bytes`.

```python
import epycs.subprocess as esp
esp.verbose = True          # print every command to stderr (like `set -x`)
esp.exit_on_error = False   # don't sys.exit() on non-zero; inspect .returncode
```

## Capture & parse output: `out_filter`

`out_filter` captures stdout and runs it through a parser. Built-in filters
(pass the name as a string) or any `callable(str) -> object`:

```python
from epycs import cmd

txt   = cmd.cat("f.txt",  out_filter="text")          # str (raw)
lines = cmd.ls("-1",      out_filter="text_lines")     # list[str], split on \n
nul   = cmd.find("-print0", out_filter="text_lines_0") # list[str], split on \0
data  = cmd.curl(url,     out_filter="json")           # parsed JSON (dict/list)
root  = cmd.cat("f.xml",  out_filter="xml")            # xml.etree Element
rows  = cmd.cat("f.csv",  out_filter="csv")            # list[list[str]]
dicts = cmd.cat("f.csv",  out_filter="csv_dict")       # list[dict], header = keys
last  = cmd.echo("hi",    out_filter=lambda s: s.strip())  # custom callable
```

Available built-in filters: `text`, `text_lines`, `text_lines_0`, `json`,
`xml`, `csv`, `csv_dict`.

## Build & reuse commands: `find_program` and `.arg(...)`

`find_program(name, *aliases)` resolves a program once (trying aliases in order,
raising `ShellProgramNotFoundError` if none found) and returns a reusable
`ShellProgram`. `.arg(...)` returns a *new* command with extra args and/or
default kwargs baked in (originals are never mutated):

```python
from epycs import cmd, find_program

ls   = find_program("ls", "/usr/bin/ls")          # first match wins
echo = find_program("echo").arg(out_filter="text")  # always capture as text
echo("hello")                                      # -> "hello\n"

git  = cmd.git.arg("-C", repo)                     # `git -C <repo> ...`
git("status", "--short", out_filter="text_lines")

cmd.echo.toto(out_filter="text")  # attribute access also adds args -> `echo toto`
```

## Keyword options

These epycs-specific kwargs work on any call; everything else is forwarded to
`subprocess.run` (e.g. `cwd=`, `timeout=`, `input=`):

| kwarg | effect |
|-------|--------|
| `out_filter` | capture stdout and parse it (see above) |
| `quiet=True` | hide output unless the command fails (then dump to stderr) |
| `stdout_tee=True` | capture stdout *and* still print it |
| `background=True` | start via `Popen` and return it immediately (no wait) |
| `additional_env={"K": "v"}` | add/override env vars; value `None` removes the var |
| `additional_pathenv={"PATH": ["/x"]}` | append entries to a `PATH`-like var |

```python
cmd.long_task(quiet=True)                       # silent unless it fails
proc = cmd.server(background=True); proc.wait()  # Popen, run concurrently
cmd.make(cwd="build", additional_env={"CC": "clang"})

ec = cmd.echo.arg("-c").env(TOTO="hi")           # `.env(**vars)` == additional_env
```

## Extras

```python
from epycs.subprocess import source_shell_script, python_to_subprocess
from epycs.config import load_from

source_shell_script("env.sh")   # apply a shell script's env to os.environ

# load_from looks at $NAME_CONFIG then ~/.config/<name>/<name>.{toml,json,...}
cfg = load_from("myapp")        # returns parsed config, or None if absent

@python_to_subprocess            # turn a python function into a pipeable subprocess
def worker(cmd_open): ...
```

# Changelog

* v2.0.0

**Breaking**: dropped support for Python < 3.13 (`requires-python = ">=3.13"`).

**Breaking**: removed the deprecated `ShellProgram` methods `with_arg`,
`with_default_kw` and `kwarg` — use `arg(...)` instead.

`pytest` is now a real dependency; test/dev environment and VSCode setup.

Typing improvements: separate KW and KWVal types, typed path helper, clarified
typing throughout.

Added an LLM/agent-friendly "Usage" cheat sheet to the README.

* v1.5.0

`epycs.cmd` directly accessible

`ShellProgram.add` supports kwargs, deprecated other methods, they will be removed
in a future version.

`ShellProgram.env(name=value)` for setting additional environment variables

tox testing on python 3.7 to 3.13

Various CI fixes

Using UV for package management

* v1.4.0

Improved handling of additional environment (allows any str-convertible,
and providing `None` deletes the env var)

Added new `python_to_subprocess` function, which turns a local python
function to a full-fledged subprocess, allowing for pure-python piping.

* v1.3.0

Added sourcing of shell scripts

Added new `out_filter=text_lines_0` that splits by NUL character

* v1.2.0

Added epycs.config for lightweight user-defined config management
