PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.
Project description
PySH
PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.
PySH — fast, Python-first universal interactive shell for Debian and Unix-like systems.
PySH is a small, dependency-free interactive shell written in pure Python.
It is packaged as a regular PyPI distribution (pysh-shell), installs a
single console command (pysh), and is designed to feel familiar to anyone
used to a Bourne-style shell while remaining hackable from Python.
Current development version: 0.4.0. PySH targets Python 3.13+ and is validated primarily on Debian 13 and Unix-like systems.
Features
- Interactive REPL with persistent history (
~/.pysh_history). - Bash-like reverse incremental search bound to Ctrl+R when GNU readline is available; degrades silently on other backends.
- Welcome banner and configurable prompt with user and CWD (
~collapsed). - Robust quote-aware parser:
- Splits chains on
;,&&,||only outside of quotes. - Splits pipelines on
|only outside of quotes.
- Splits chains on
- Pipelines with correctly managed file-descriptor handover.
- Redirection:
<,>,>>,2>,2>>,&>,&>>. - Command substitution:
$(command)and`command`. Quote-aware: evaluated inside double quotes, suppressed inside single quotes. Bounded by a 5-second timeout by default. - Local variables (
NAME=value) and exported environment variables (export NAME=value) with$NAME/${NAME}expansion. - Aliases with sane defaults;
aliasandunaliasbuiltins. - Migration layer for zsh/bash/sh:
source_zsh <file>preserves the existing alias importer,source_zsh_profile <file>andsource_sh_aliases <file>statically import simple aliases, exports and assignments without executing profile code,run_script <file> [args...]delegates shebang scripts to their real interpreter, andcompat_check <file>reports migration risk before execution. - Zsh Transition Layer for explicit delegation:
zsh <command>delegates to real zsh when installed, andzsh_fallbackcan be enabled for controlled fallback experiments. - Python-native runtime bridge:
py <code>executes one-line Python code in a persistent per-session runtime context. - Python automation blocks:
py { ... }runs a multiline Python block in the same persistent runtime context as the one-linepyform. - Debian/system profile helpers:
sys_info,env_audit,path_audit,which_all,apt_check,apt_search— non-mutating, never callsudo. - Command planning:
plan <command...>previews how PySH would classify and execute a command without running it. - Startup file
~/.pyshrcplus a plugin directory at~/.pyshrc.d/whose*.pyshfiles load in deterministic lexicographic order. - Mini rc-interpreter for control flow inside
~/.pyshrcand plugins:if/else/fi,for/do/done,while/do/done(with a hard iteration safety limit). - Directory stack:
pushd,popd,dirs. svcbuiltin for PyInit-style service control by PID file:svc list,svc status,svc stop,svc restart,svc start.- PyInit service metadata parser with dependency tracking
(
depends: [network]). - Safe ANSI color helpers that respect
NO_COLORandTERM=dumb, used for the banner and diagnostics. The input line itself is left untouched so editing remains stable. - Basic tab completion for aliases, builtins, files and directories.
- Clean Ctrl+C (cancels current line, keeps the shell alive) and Ctrl+D (exits the shell).
Installation
From PyPI
pip install pysh-shell
Then start the shell with:
pysh
Quick start
pysh
pysh --version
pysh -c "echo hi"
alias ll='ls -lah'
source_zsh_profile ~/.zshrc
source_sh_aliases ~/.bash_aliases
compat_check ~/scripts/maintenance.sh
run_script ~/scripts/maintenance.sh --dry-run
py import platform; print(platform.platform())
Development install
python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
Running
pysh # console entry point installed by the wheel
python -m pysh # equivalent module entry point
pysh --version # print version and exit
pysh -c "echo hi; echo there" # run one command line and exit
Documentation
Full documentation lives under the repository docs/:
- Installation — installing from PyPI and dev install.
- Usage — invocation, operators, pipelines, redirection, command substitution, variables, builtins.
- Builtins — syntax, examples, return behavior and limitations for every builtin.
- Operators — chains, pipelines, redirection, command substitution, quoting and parser limitations.
- Configuration —
~/.pyshrc, plugins under~/.pyshrc.d/, aliases, exports, prompt behavior. - Migration — static profile import, script transition runner, and compatibility reporting.
- Zsh compatibility — transition bridge, safe profile import, explicit zsh delegation, optional fallback mode.
- Python runtime — persistent Python-native
pyexecution context. - Development — running the test suite, linting, building artifacts, repository layout.
- Release process — how PySH ships via GitHub Actions and PyPI Trusted Publishing.
- System profile —
sys_info,env_audit,path_audit,which_all,apt_check,apt_search. - Command planning —
plan <command...>, the advisory classifier. - Packaging — PyPI /
.deb/.rpmartifact naming contract and build scripts. - Limitations — explicit non-goals and compatibility boundaries.
- Documentation policy — required coverage for new commands, parser behavior, migration helpers and limitations.
Builtins
Available shell builtins. Most run inside the shell process; transition
builtins such as zsh and run_script may delegate explicitly as
documented.
| Builtin | Description |
|---|---|
cd |
Change the current working directory. |
pwd |
Print the current working directory. |
alias |
Define or display aliases. |
unalias |
Remove one or more aliases. |
export |
Define or display exported environment vars. |
source |
Execute commands from a file (also .). |
source_zsh |
Safely import simple aliases from a zsh-compatible file. |
source_zsh_profile |
Statically import simple zsh profile entries. |
source_sh_aliases |
Statically import simple sh/bash aliases and vars. |
run_script |
Run a script through a shebang interpreter or native PySH lines. |
compat_check |
Produce a static migration report for a shell file. |
zsh |
Execute one command through real zsh -lc. |
zsh_fallback |
Enable or disable explicit zsh fallback mode. |
py |
Execute Python code in the persistent PySH runtime. |
sys_info |
Print platform / Python / user / shell / PATH summary. |
env_audit |
Print a redacted environment audit summary. |
path_audit |
Report missing / duplicate / non-directory PATH entries. |
which_all |
Print every executable match for a command in PATH. |
apt_check |
Run apt list --upgradable (Debian helper, no sudo). |
apt_search |
Run apt search <query> (Debian helper, no sudo). |
plan |
Preview classification/execution of a command, advisory. |
pushd |
Push CWD onto the directory stack and cd to a path. |
popd |
Pop the directory stack and cd to the popped entry. |
dirs |
Print the current directory followed by the stack. |
svc |
Query / signal PyInit services. See svc / PyInit. |
exit |
Exit the shell with an optional status code. |
quit |
Same as exit. |
Operators
| Operator | Meaning |
|---|---|
cmd1 ; cmd2 |
Run cmd1, then unconditionally run cmd2. |
cmd1 && cmd2 |
Run cmd2 only if cmd1 exits with status 0. |
cmd1 || cmd2 |
Run cmd2 only if cmd1 exits with non-zero status. |
cmd1 | cmd2 |
Pipe cmd1's stdout into cmd2's stdin. |
Operators inside single or double quotes are treated as literal text.
echo "🐍 PySH v0.4.0 | Python 3.13.5"
echo "Test | pipe & semicolon; && ok"
python3.13 -c "import subprocess; print('ok')"
Redirection
| Syntax | Effect |
|---|---|
< file |
Read stdin from file. |
> file |
Write stdout to file (truncate). |
>> file |
Write stdout to file (append). |
2> file |
Write stderr to file (truncate). |
2>> file |
Write stderr to file (append). |
&> file |
Write stdout + stderr to file. |
&>> file |
Append stdout + stderr to file. |
ls -la 2>/dev/null | head -3
python3.13 -c "import sys; print('err', file=sys.stderr)" 2> err.log
echo "hello" > out.txt
echo "again" >> out.txt
Redirection operators inside quotes are kept as literal characters.
Pipelines
PySH connects each stage with a real OS pipe and closes the parent's duplicate handles after the child is spawned, so neither side deadlocks.
ls -la | head -3
apt list --upgradable 2>/dev/null | grep -c "/"
Command substitution
echo "Kernel: `uname -r`"
echo "Date: $(date '+%Y-%m-%d')"
echo 'No substitution: $(date)'
$(...)and`...`are both supported.- Quotes are honoured: substitutions inside single quotes are kept literally; substitutions inside double quotes are evaluated.
- Each substitution runs with a 5-second timeout by default. On timeout or failure the substitution expands to an empty string and the shell remains usable.
Variables
NAME=world # local shell variable
export GREETING=hi # exported environment variable
echo "$GREETING, $NAME"
Local variables shadow environment variables when expanded. Single quotes suppress expansion; double quotes do not.
Zsh Transition Layer
PySH is Python-first, not a full zsh clone. The zsh compatibility bridge is for transition: it lets users move aliases and selected legacy commands into PySH without pretending that every zsh grammar feature is native.
source_zsh ~/.zsh_aliases
source_zsh_profile ~/.zshrc
source_sh_aliases ~/.bash_aliases
compat_check ~/scripts/maintenance.sh
run_script ~/scripts/maintenance.sh --dry-run
zsh 'source ~/.zshrc; my_old_alias'
zsh 'print -r -- hello'
source_zsh <file> statically imports supported simple alias definitions
such as alias ll='ls -lah', ignores comments and unsupported constructs,
and never executes the file as code. It prints imported=N skipped=M file=<path> and reports malformed alias lines deterministically on stderr.
source_zsh_profile <file> and source_sh_aliases <file> use the 0.3.0
static profile importer. They read files such as ~/.zshrc, .profile and
.bash_aliases without executing them, import supported simple aliases,
exports and local assignments, and print aliases=N exports=N vars=N skipped=M file=<path>.
compat_check <file> produces a static report with supported,
delegated, skipped and risky counts. Risky constructs such as eval,
command substitution, source and shell functions cause exit status 2.
run_script <file> [args...] is an explicit transition runner. Scripts with
zsh, bash or sh shebangs are delegated to the real interpreter through
an argv list; no-shebang scripts are executed line-by-line by PySH's native
engine where possible.
zsh <command> delegates explicitly to real zsh -lc <command>. If zsh is
not installed, it returns 127 with a deterministic error.
Fallback mode is off by default. It can be enabled only explicitly:
zsh_fallback on
zsh_fallback off
PYSH_ZSH_FALLBACK=1
When fallback is on, PySH may delegate commands it cannot parse or execute natively to zsh. Builtins already handled by PySH stay native, and native command failures are not hidden.
Python Runtime
The py builtin executes one-line Python code in a persistent runtime context
owned by the current PySH session:
py print("hello from python")
py import platform; print(platform.platform())
py from pathlib import Path; print(Path(".").resolve())
Variables and imports persist across py invocations:
py x = 10
py print(x)
py import pathlib
py print(pathlib.Path(".").exists())
Exceptions are printed to stderr and return non-zero without terminating the shell.
Multiline Python automation blocks
py {
import os
targets = [p for p in os.environ.get("PATH", "").split(":") if p]
print(f"PATH entries: {len(targets)}")
}
The opener line is exactly py {, the closer is a line that contains only
}. Block bodies execute in the same persistent runtime context as one-line
py invocations, so variables and imports flow in both directions.
Unterminated blocks return non-zero in script/source mode; nested
py { ... } blocks are rejected deterministically.
System profile helpers
PySH 0.3.0 adds a small, non-mutating Debian/system profile layer. None of
these helpers call sudo or modify system state.
sys_info # platform, Python, user, shell, PATH count
env_audit # safe env audit with secret redaction
path_audit # report missing / duplicate / non-directory entries
which_all python3 # all executables for "python3" along PATH
apt_check # apt list --upgradable
apt_search vim # apt search vim
Variables whose name contains KEY, TOKEN, SECRET, PASSWORD, PASS,
CREDENTIAL, or AUTH are replaced with <redacted> in env_audit. See
docs/system-profile.md.
Command planning
plan <command...> previews how PySH would classify and execute a command
without running it. It is advisory only — there is no policy enforcement.
plan ls -la
plan echo a && echo b
plan sudo apt update
plan py print("x")
The output prints original=, kind=, execution=, risk= and reason=
fields. See
docs/command-planning.md.
~/.pyshrc and ~/.pyshrc.d/
At startup PySH first executes ~/.pyshrc (if present), then every file in
~/.pyshrc.d/ whose name ends with .pysh. Plugins are loaded in
deterministic lexicographic order so prefix numbering (10-…, 20-…)
gives predictable layering.
You can also re-source any file at any time:
source ~/.pyshrc
A failing line is reported on stderr and the next line is still executed. A broken plugin does not prevent later plugins from loading.
Example ~/.pyshrc
export EDITOR="nano"
export PAGER="less"
export LANG="pl_PL.UTF-8"
export PYTHONDONTWRITEBYTECODE="1"
alias rm="rm -i"
alias cp="cp -i"
alias mv="mv -i"
alias python="python3.13"
alias pip="pip3.13"
if [ -d /opt/local/bin ]; then
export PATH="/opt/local/bin:$PATH"
fi
for dir in ~/bin ~/.local/bin; do
if [ -d "$dir" ]; then
export PATH="$dir:$PATH"
fi
done
echo "🐍 PySH 0.4.0 | Python 3.13+"
echo "💡 Operators: && || ; | > >> < 2> 2>> &> &>> + \$() and backticks"
Example plugin ~/.pyshrc.d/10-aliases.pysh
# Loaded after ~/.pyshrc, in lexicographic order.
alias gs="git status -sb"
alias gd="git diff"
alias gl="git log --oneline --decorate"
if [ -f ~/.work_aliases ]; then
source ~/.work_aliases
fi
Mini rc-interpreter cheat sheet
| Construct | Notes |
|---|---|
if [ <cond> ]; then ... fi |
else block optional |
for VAR in a b c; do ... done |
Iterates literal words |
while [ <cond> ]; do ... done |
Bounded by a safety iteration limit |
The canonical else keyword is else. The form else: is accepted as a
compatibility alias.
Supported test operators:
| Test | Meaning |
|---|---|
[ -f path ] |
path exists and is a regular file |
[ -d path ] |
path exists and is a directory |
[ -e path ] |
path exists |
[ -z "$VAR" ] |
$VAR is empty |
[ -n "$VAR" ] |
$VAR is non-empty |
[ "$A" = "$B" ] |
string equality |
[ "$A" == "$B" ] |
string equality (alias) |
[ "$A" != "$B" ] |
string inequality |
[ ! -f path ] etc. |
negate any of the above |
Directory stack
pushd /tmp
pushd /var/log
dirs
popd
popd
pushd path pushes the current directory onto the stack and changes to
path. popd returns to the most-recently-pushed directory. dirs prints
the current directory followed by the stack contents. Popping an empty
stack produces a deterministic error and a non-zero exit status.
svc / PyInit
PySH ships a small service client used by the svc builtin. It is
deliberately PID-file based so it can be useful on systems that do not run
a full supervisor, but it is also designed to plug into PyInit when a
control interface is present.
svc list
svc status <name>
svc start <name>
svc stop <name>
svc restart <name>
svc listwalks/run/pyinit/*.pidand prints each service asname\tactive|dead\tpid=N.svc status <name>checks the PID file at/run/pyinit/<name>.pid.svc stop <name>reads the PID file and sendsSIGTERM.svc restart <name>sendsSIGTERM. Without a registered PyInit control interface it then reports that restart requires supervision.svc start <name>requires a PyInit control interface. Without one it fails deterministically rather than pretending to work.
PySH never calls sudo. To control system-wide PyInit services, run PySH
under an account that already has permission.
PyInit service metadata
PyInit-style service files live in ~/.config/pyinit/services/ (or a path
chosen by your integration). PySH ships a strict metadata parser:
# ~/.config/pyinit/services/example.service
name: example
command: python3.13 -m http.server 8080
depends: [network]
Recognised fields:
name— required, identifiercommand— required, the launch command linedepends— optional list of dependency names; accepts[a, b]or comma-separated form
Invalid metadata (unknown syntax, malformed dependencies, missing fields)
produces a deterministic ServiceMetadataError rather than silently
dropping content. This is metadata support for PyInit integration, not
a complete replacement for systemd.
Tab completion
Tab completes aliases and builtins for the first word, and filesystem
paths for any word. Inaccessible directories are silently skipped.
Limitations
- No job control (
&,bg,fg,jobs,Ctrl+Zjob suspension). - No full POSIX shell grammar — only the constructs documented above.
- No glob expansion is performed by PySH itself; external commands still
receive globs through their own expansion logic when run via a system
shell, but plain pipelines do not expand
*/?in arguments. - No full zsh compatibility. The zsh compatibility bridge is a transition layer with safe static alias import and explicit delegation to real zsh.
svc startandsvc restartto actually re-launch a process require a PyInit control interface; without one they return a deterministic error.- Multiline
py { ... }blocks do not support nested blocks; the opener must be exactlypy {and the closer must be a line containing only}. - Debian helpers (
apt_check,apt_search) requireaptto exist; they return 127 deterministically when it does not. planis advisory only. Policy enforcement is intentionally out of scope in 0.4.0.
Testing and quality gates
pytest -q
ruff check src tests
python -m build
twine check dist/*
The project ships unit tests for the parser, the redirection module, the
rc loader and mini-interpreter, command substitution, the history manager,
the highlighting helpers, the plugin loader, directory stack, unalias,
the svc builtin, the PyInit metadata parser, the zsh transition layer and
the Python runtime bridge.
Publishing
This repository is configured for PyPI Trusted Publishing via GitHub
Actions. See .github/workflows/publish.yml — it uses pypa/gh-action-pypi-publish@release/v1 with id-token: write and the pypi environment. Tagging a release on GitHub publishes the build.
Do not publish from a developer machine; let the workflow do it.
Target platform
- Primary target: Debian 13 with Python 3.13+.
- Should work on any POSIX system with a working
subprocessandreadline, but only Debian 13 is regularly validated.
License
PySH is distributed under the GNU General Public License v3.0 or later
(GPL-3.0-or-later). See LICENSE for the full text.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pysh_shell-0.4.0.tar.gz.
File metadata
- Download URL: pysh_shell-0.4.0.tar.gz
- Upload date:
- Size: 71.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0d6b4e53dbd8f28c86da4d6ccff99c189c707a586f8cabf1a94ddca2205a346
|
|
| MD5 |
99d957fb434517203b07e3dc82df587a
|
|
| BLAKE2b-256 |
8f0e59e67492e0dc71d70d7ab9d80122605f52e2f600d15d883b052c278258c8
|
Provenance
The following attestation bundles were made for pysh_shell-0.4.0.tar.gz:
Publisher:
publish.yml on SSobol77/pysh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pysh_shell-0.4.0.tar.gz -
Subject digest:
c0d6b4e53dbd8f28c86da4d6ccff99c189c707a586f8cabf1a94ddca2205a346 - Sigstore transparency entry: 1624649862
- Sigstore integration time:
-
Permalink:
SSobol77/pysh@69d252afe023400183ac4a29582beefef442485e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/SSobol77
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@69d252afe023400183ac4a29582beefef442485e -
Trigger Event:
release
-
Statement type:
File details
Details for the file pysh_shell-0.4.0-py3-none-any.whl.
File metadata
- Download URL: pysh_shell-0.4.0-py3-none-any.whl
- Upload date:
- Size: 69.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b688dd26b9fc18e8b541efc929fda720181e0b8a838e37cd8c346e9a8fb00810
|
|
| MD5 |
006b652f95dd02d097296d864232446a
|
|
| BLAKE2b-256 |
fd40316caa6e268d2b80f9be9403e5382963fa84df212df48682b1d291224c35
|
Provenance
The following attestation bundles were made for pysh_shell-0.4.0-py3-none-any.whl:
Publisher:
publish.yml on SSobol77/pysh
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pysh_shell-0.4.0-py3-none-any.whl -
Subject digest:
b688dd26b9fc18e8b541efc929fda720181e0b8a838e37cd8c346e9a8fb00810 - Sigstore transparency entry: 1624649891
- Sigstore integration time:
-
Permalink:
SSobol77/pysh@69d252afe023400183ac4a29582beefef442485e -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/SSobol77
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@69d252afe023400183ac4a29582beefef442485e -
Trigger Event:
release
-
Statement type: