Skip to main content

Epycs is a simple way to convert shell scripts to python

Project description

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.

pip install epycs   # or: uv add epycs

Run a command

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

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.
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:

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):

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
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

Pipe commands with |

Chain commands with | exactly like a shell pipe. Operands are uncalled programs (bake args with .arg(...)); the pipeline runs only when you call it:

from epycs import cmd

# stdout of each stage is wired to stdin of the next
errors = (cmd.cat.arg("app.log") | cmd.grep.arg("ERROR"))(out_filter="text_lines")

# args/kwargs of the terminal call go to the last stage
n = (cmd.cat.arg("app.log") | cmd.grep)("ERROR", out_filter="text")

Pipelines behave like a shell pipe with set -o pipefail: the pipeline's return code is the rightmost non-zero stage code (0 if all succeed). Combined with the default exit_on_error, any failing stage aborts the script — so cmd.false | cmd.cat fails (a plain shell pipe would return cat's 0).

import epycs.subprocess as esp
esp.exit_on_error = False
(cmd.false | cmd.cat)().returncode   # -> 1 (pipefail), not 0

Extras

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.1.0

Pipe commands with the | operator, with shell-like pipefail semantics: the pipeline's return code is the rightmost non-zero stage, so any failing stage aborts the script under the default exit_on_error. out_filter and stdout_tee apply to the last stage.

Justfile now builds and publishes with uv instead of build / twine.

  • 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

epycs-2.1.0.tar.gz (17.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

epycs-2.1.0-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file epycs-2.1.0.tar.gz.

File metadata

  • Download URL: epycs-2.1.0.tar.gz
  • Upload date:
  • Size: 17.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for epycs-2.1.0.tar.gz
Algorithm Hash digest
SHA256 a0ffebfadf53f0eaa8a6dbb89f5c599ab098d46da1b02955b71ca96a37876d54
MD5 9c7f78e75b23219d61652e858d23f5f8
BLAKE2b-256 3a9df17c623f418170283f0b11871ca63f42bb5ae9c926fc166af54848b592f8

See more details on using hashes here.

File details

Details for the file epycs-2.1.0-py3-none-any.whl.

File metadata

  • Download URL: epycs-2.1.0-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.13 {"installer":{"name":"uv","version":"0.9.13"},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for epycs-2.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 95334a7e22b3ba023ff675f92aef57c65d0afd65c46952b64ef3d49ddca86b09
MD5 4a601a601dac41cf70f3577764413626
BLAKE2b-256 c4366e3a37f5630b4636fd75a46dc47a8a197c2a23ebd26d97e46032f9b5c879

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page