Skip to main content

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 raises TerminalError. Stderr redirects are honored: 2>file captures, 2>/dev/null suppresses, 2>&1 merges into the pipe (cmd 2>&1 | head works)
  • 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 -- FileSystem is a typing.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

termish-0.1.8.tar.gz (97.8 kB view details)

Uploaded Source

Built Distribution

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

termish-0.1.8-py3-none-any.whl (71.4 kB view details)

Uploaded Python 3

File details

Details for the file termish-0.1.8.tar.gz.

File metadata

  • Download URL: termish-0.1.8.tar.gz
  • Upload date:
  • Size: 97.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for termish-0.1.8.tar.gz
Algorithm Hash digest
SHA256 0233fe9986ddc6c03d929812a1747b5a1c2ac85405b72a50b84821fade8756ee
MD5 4a1ce15c5ba4ca3f471b641165f1b5e3
BLAKE2b-256 8a711eba74d75a910382c57975239ebeb5fff49c3698aca7456408dfeca7e8b6

See more details on using hashes here.

File details

Details for the file termish-0.1.8-py3-none-any.whl.

File metadata

  • Download URL: termish-0.1.8-py3-none-any.whl
  • Upload date:
  • Size: 71.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.13

File hashes

Hashes for termish-0.1.8-py3-none-any.whl
Algorithm Hash digest
SHA256 23b6f325681b644690cf033d286f8d33da9b927721d6cb95696e638a5bc901d1
MD5 3937d0344bae1d37a380e726cccf8959
BLAKE2b-256 e53df1ea5731880a44b65ac1c53ec0c980476419ec3b53ea0579604ef81e0c98

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.9

2 files

This release

0.1.8 This release

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page