termish 📺
Virtual terminal with shell-like commands over a pluggable filesystem.
Parses and executes shell scripts (pipelines, redirects, semicolons) against any object that implements the FileSystem protocol. Zero runtime dependencies. Pure Python.
Features
- Shell parser -- pipes, redirects (
>,>>,<,2>,2>>,2>&1), heredocs (<<EOF), semicolons, quoted strings, line continuation - Variable expansion --
$?(last exit code),$VAR/${VAR}from an env dict; expands in unquoted and double-quoted contexts, literal in single quotes - Terminal-faithful transcript -- stderr diagnostics appear in the returned output when execution continues past a failure (
cmd; next,cmd || rescue), like a real terminal screen; a failure with nothing after it raisesTerminalError. Stderr redirects are honored:2>filecaptures,2>/dev/nullsuppresses,2>&1merges into the pipe (cmd 2>&1 | headworks) - 36 builtins -- ls, cat, grep, find, sed, tr, sort, uniq, cut, wc, diff, tar, gzip, zcat, zip, jq, xargs, file, true, false, basename, dirname, ...
- Custom commands -- inject your own command handlers alongside builtins; injected commands override builtins and compose in pipelines
- jq engine -- built-in jq filter parser and evaluator (field access, pipes, functions, conditionals)
- Pluggable filesystem --
FileSystemis atyping.Protocol; any object with the right methods works - MemoryFS included -- in-memory filesystem for testing and lightweight use
Install
pip install termish
Quick example
from termish import execute, MemoryFS
fs = MemoryFS()
execute("mkdir -p src", fs)
execute("echo 'def main(): pass' > src/app.py", fs)
execute("echo 'import os' > src/utils.py", fs)
# Pipelines work
output = execute("grep -r 'def' src | wc -l", fs)
print(output) # 1
# jq works
execute('echo \'{"name": "alice", "score": 42}\' > data.json', fs)
output = execute('jq -r ".name" data.json', fs)
print(output) # alice
Variables
$? expands to the last pipeline's exit code. $VAR / ${VAR} read from
an optional env dict, which is shared with command handlers via ctx.env
-- mutations persist across commands (and across execute() calls if you
reuse the dict):
output = execute('cat /missing; echo "exit=$?"', fs)
print(output) # exit=1
env = {"NAME": "alice"}
output = execute("echo hello $NAME", fs, env=env)
print(output) # hello alice
Unset variables expand to the empty string. Single quotes suppress
expansion ('$?' stays literal). Command substitution $(...) is not
supported and raises ParseError rather than mangling silently.
Heredoc bodies are never expanded.
Expansions are never field-split -- this is zsh's behavior, not
bash's, and it's deliberate: a value with spaces stays one argument
(grep $PAT file with PAT="a b" searches for a b), and a
multi-word command name is a visible command not found rather than a
silent re-parse. An empty-expanding command word shifts away
($UNSET echo hi runs echo hi), also as in zsh.
Custom commands
Inject your own commands via the commands parameter. They receive a CommandContext and compose naturally with builtins in pipelines:
from termish import execute, MemoryFS, CommandContext, CommandResult
def greet(ctx: CommandContext) -> CommandResult | None:
name = ctx.args[0] if ctx.args else "world"
ctx.stdout.write(f"hello {name}\n")
return None
fs = MemoryFS()
output = execute("greet alice | wc -c", fs, commands={"greet": greet})
print(output) # 12
# Injected commands override builtins with the same name
All commands — builtin and injected — use the same CommandContext signature. See CommandContext, CommandResult, and CommandFunc in termish.context and termish.errors.
FileSystem protocol
Any object implementing these 16 methods works with termish -- no inheritance required:
class FileSystem(Protocol):
def getcwd(self) -> str: ...
def chdir(self, path: str) -> None: ...
def read(self, path: str) -> bytes: ...
def write(self, path: str, content: bytes, mode: str = "w") -> None: ...
def exists(self, path: str) -> bool: ...
def isfile(self, path: str) -> bool: ...
def isdir(self, path: str) -> bool: ...
def stat(self, path: str) -> FileMetadata: ...
def mkdir(self, path: str, parents: bool = False, exist_ok: bool = False) -> None: ...
def makedirs(self, path: str, exist_ok: bool = True) -> None: ...
def remove(self, path: str) -> None: ...
def rmdir(self, path: str) -> None: ...
def rename(self, src: str, dst: str) -> None: ...
def list(self, path: str = ".", recursive: bool = False) -> list[str]: ...
def list_detailed(self, path: str = ".", recursive: bool = False) -> list[FileInfo]: ...
def glob(self, pattern: str) -> list[str]: ...
Part of the agex stack
termish provides shell commands for AI agents in agex, operating over virtual filesystems from monkeyfs.
Compatible filesystems
monkeyfs VirtualFS and IsolatedFS both satisfy the termish FileSystem protocol and can be passed directly to execute().
Builtin commands
| Category | Commands |
|---|---|
| Filesystem | pwd, cd, mkdir, ls, touch, cp, mv, rm, basename, dirname |
| I/O | echo, cat, head, tail, tee |
| Search | grep, find |
| Text | wc, sort, uniq, cut, sed, tr |
| Diff | diff |
| Archive | tar, gzip, gunzip, zcat/gzcat, zip, unzip |
| Meta | xargs |
| JSON | jq |
| Inspection | file |
| Control | true, false |
Development
uv sync --extra dev
uv run pytest
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 termish-0.1.9.tar.gz.
File metadata
- Download URL: termish-0.1.9.tar.gz
- Upload date:
- Size: 98.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c65d1137ec6e021ff20be6d2b04ec35ae7d4e210ccc68f6627d2f9cbc70449c
|
|
| MD5 |
eaf4adec614e05918fa3a70a0fe5afab
|
|
| BLAKE2b-256 |
85229b7a1febc998ce502dd1e7013fd5c769e9386d50a85091982316702522f5
|
File details
Details for the file termish-0.1.9-py3-none-any.whl.
File metadata
- Download URL: termish-0.1.9-py3-none-any.whl
- Upload date:
- Size: 71.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b71922c3c649339c920c4c9b61d0219d2c01cf55641cb4d08bbf42609502dce7
|
|
| MD5 |
d3ac1c7594b7683ec7a48a952c6ea303
|
|
| BLAKE2b-256 |
0c0dc3563ad240aa46746545e853096a77a3fe6b6538e18e4809d55fbd50e5e1
|