Skip to main content

A Python wrapper around the just-bash virtual shell

Project description

just-py-bash

PyPI Python Versions CI Coverage

Python bindings for just-bash.

just-py-bash gives Python code the same long-lived virtual shell that upstream just-bash exposes in TypeScript, with two public API styles:

  • Bash for synchronous code
  • AsyncBash for native-asyncio code

Each session owns a dedicated Node.js worker process and a real upstream just-bash Bash instance. That means upstream semantics are preserved:

  • each exec() call gets its own isolated shell state
  • the virtual filesystem is shared across exec() calls
  • results come back as structured Python objects

Install

uv add just-py-bash

Install name: just-py-bash
Import name: just_bash

By default, just-py-bash uses a system-provided Node.js runtime.

For an explicit bundled-Node install:

uv add 'just-py-bash[node]'

That extra installs the first-party just-bash-bundled-runtime companion package.

js-exec / JavaScriptConfig(...) require Node.js >= 22.6. If the resolved runtime is older, just-py-bash raises UnsupportedRuntimeConfigurationError with upgrade guidance. Installing just-py-bash[node] satisfies this automatically.

Quick Start

Synchronous API

from just_bash import Bash

with Bash(cwd="/workspace") as bash:
    bash.exec("export NAME=alice; echo 'hello from the shared filesystem' > greeting.txt; cd /tmp")

    result = bash.exec(
        'printf "name=%s cwd=%s file=%s\n" "${NAME:-missing}" "$PWD" "$(cat greeting.txt)"',
    )
    print(result.stdout, end="")

    bash.fs.write_text("note.txt", "written via bash.fs\n")
    print(bash.read_text("note.txt"), end="")

Asynchronous API

AsyncBash is implemented with native asyncio subprocesses, tasks, futures, and locks.

import asyncio

from just_bash import AsyncBash


async def main() -> None:
    async with AsyncBash(cwd="/workspace") as bash:
        await bash.exec("export NAME=alice; echo 'hello from async shared filesystem' > greeting.txt; cd /tmp")

        result = await bash.exec(
            'printf "name=%s cwd=%s file=%s\n" "${NAME:-missing}" "$PWD" "$(cat greeting.txt)"',
        )
        print(result.stdout, end="")

        await bash.fs.write_text("note.txt", "written via async bash.fs\n")
        print(await bash.read_text("note.txt"), end="")


asyncio.run(main())

Custom Commands

Upstream just-bash uses defineCommand(...). The Python wrapper exposes the same capability with a Python mapping of command names to callables.

Sync custom commands

from just_bash import Bash, CustomCommandContext


def greet(args: list[str], ctx: CustomCommandContext) -> dict[str, str | int]:
    del ctx
    name = args[0] if args else "world"
    return {"stdout": f"hello, {name}!\n", "exit_code": 0}


with Bash(custom_commands={"greet": greet}) as bash:
    result = bash.exec("greet mars")
    print(result.stdout, end="")

Async custom commands

import asyncio

from just_bash import AsyncBash, AsyncCustomCommandContext


async def annotate(args: list[str], ctx: AsyncCustomCommandContext) -> dict[str, str | int]:
    label = args[0] if args else "note"
    nested = await ctx.exec("wc -w", stdin=ctx.stdin)
    words = nested.stdout.strip().split()[0] if nested.stdout.strip() else "0"
    return {"stdout": f"[{label}] words={words}\n", "exit_code": 0}


async def main() -> None:
    async with AsyncBash(custom_commands={"annotate": annotate}) as bash:
        result = await bash.exec("printf 'one two three' | annotate summary")
        print(result.stdout, end="")


asyncio.run(main())

Custom commands can:

  • receive shell arguments
  • read ctx.stdin, ctx.cwd, and ctx.env
  • run nested shell commands with ctx.exec(...)
  • participate in pipelines and redirections
  • override built-in command names if desired
  • return non-zero exit codes
  • raise exceptions, which become shell failures

Supported Commands

The wrapper delegates command execution to upstream just-bash, so the Python API gets the same command families. For programmatic introspection, use:

from just_bash import (
    get_command_names,
    get_javascript_command_names,
    get_network_command_names,
    get_python_command_names,
)

print(len(get_command_names()))
print(sorted(get_network_command_names()))
print(sorted(get_python_command_names()))
print(sorted(get_javascript_command_names()))
Current upstream command categories

File Operations

cat, cp, file, ln, ls, mkdir, mv, readlink, rm, rmdir, split, stat, touch, tree

Text Processing

awk, base64, column, comm, cut, diff, expand, fold, grep (+ egrep, fgrep), head, join, md5sum, nl, od, paste, printf, rev, rg, sed, sha1sum, sha256sum, sort, strings, tac, tail, tr, unexpand, uniq, wc, xargs

Data Processing

jq (JSON), sqlite3 (SQLite), xan (CSV), yq (YAML/XML/TOML/CSV)

Optional Runtimes

js-exec (requires javascript=True / JavaScriptConfig(...)), python3 / python (requires python=True)

Compression & Archives

gzip (+ gunzip, zcat), tar

Navigation & Environment

basename, cd, dirname, du, echo, env, export, find, hostname, printenv, pwd, tee

Shell Utilities

alias, bash, chmod, clear, date, expr, false, help, history, seq, sh, sleep, time, timeout, true, unalias, which, whoami

Network

curl, html-to-markdown (require network=...)

All commands support --help for usage information.

Configuration

The Python API mirrors the upstream configuration model, adapted to Python types and keyword arguments.

from just_bash import Bash, ExecutionLimits, JavaScriptConfig

bash = Bash(
    files={"/data/file.txt": "content"},
    env={"MY_VAR": "value"},
    cwd="/app",
    execution_limits=ExecutionLimits(max_call_depth=50),
    python=True,
    javascript=JavaScriptConfig(bootstrap="globalThis.answer = 42;"),
)

result = bash.exec("echo $TEMP", env={"TEMP": "value"}, cwd="/tmp")
result = bash.exec("cat", stdin="hello from stdin\n")
result = bash.exec("env", replace_env=True, env={"ONLY": "this"})
result = bash.exec("grep", args=["-r", "TODO", "src/"])
result = bash.exec("cat <<EOF\n  indented\nEOF", raw_script=True)
result = bash.exec("while true; do sleep 1; done", timeout=5)

bash.close()

Session options

  • files: initial in-memory files; values can be plain text/bytes, FileInit(...), or LazyFile(...)
  • env: initial environment
  • cwd: starting directory
  • fs: upstream-style filesystem config object (InMemoryFs, OverlayFs, ReadWriteFs, MountableFs)
  • execution_limits: validated execution protection settings
  • python=True: enable python / python3
  • javascript=True or JavaScriptConfig(...): enable js-exec
  • commands: allowlist commands
  • custom_commands: register Python-defined commands
  • network: configure allow-listed network access
  • process_info: process metadata passed to the backend

Filesystem configuration objects

The wrapper now exposes upstream-style init-time filesystem config objects:

  • InMemoryFs(files=...)
  • OverlayFs(root=..., mount_point=..., read_only=...)
  • ReadWriteFs(root=...)
  • MountableFs(base=..., mounts=[MountConfig(...)])

On Windows, host-backed filesystem configs (OverlayFs, ReadWriteFs, and MountableFs containing those) raise UnsupportedRuntimeConfigurationError. Upstream just-bash host filesystem semantics are currently unstable on Windows, so just-py-bash fails early with a clear explanation instead of letting file operations silently misbehave. Use InMemoryFs, avoid host-backed mounts, or run under WSL / another POSIX environment.

files= and InMemoryFs(files=...) both support richer initial values too:

  • plain text / bytes
  • FileInit(content=..., mode=..., mtime=...)
  • LazyFile(provider=...)

LazyFile(provider=...) accepts either:

  • static deferred content (str / bytes), or
  • a Python callable returning str / bytes (sync or async)

These are serialized when the session starts and decoded into real upstream just-bash filesystem instances inside the Node worker. The filesystem config classes are config objects, not live Python filesystem implementations.

from just_bash import Bash, MountConfig, MountableFs, OverlayFs, ReadWriteFs

with Bash(
    fs=MountableFs(
        mounts=[
            MountConfig(
                mount_point="/workspace",
                filesystem=ReadWriteFs(root="/tmp/project"),
            ),
            MountConfig(
                mount_point="/docs",
                filesystem=OverlayFs(
                    root="/path/to/docs",
                    mount_point="/",
                    read_only=True,
                ),
            ),
        ],
    ),
    cwd="/workspace",
) as bash:
    result = bash.exec("cp /docs/README.md ./README.copy.md && ls")
    print(result.stdout, end="")

If you pass both files= and fs=, upstream just-bash semantics apply and fs= takes precedence.

Session filesystem API

Each session now also exposes a session-bound filesystem proxy at bash.fs / async_bash.fs.

Available methods:

  • read_text(path)
  • read_bytes(path)
  • write_text(path, content)
  • write_bytes(path, content)
  • append_text(path, content)
  • append_bytes(path, content)
  • exists(path)
  • stat(path)FsStat
  • lstat(path)FsStat
  • mkdir(path, recursive=False)
  • readdir(path)
  • readdir_with_file_types(path)list[DirentEntry]
  • rm(path, recursive=False, force=False)
  • cp(src, dest, recursive=False)
  • mv(src, dest)
  • resolve_path(path, *, base=None)
  • get_all_paths()
  • chmod(path, mode)
  • symlink(target, link_path)
  • link(existing_path, new_path)
  • readlink(path)
  • realpath(path)
  • utimes(path, atime, mtime)

Paths are resolved with upstream just-bash session semantics, so relative paths are interpreted against the session cwd.

from datetime import UTC, datetime

from just_bash import Bash, FileInit, LazyFile

with Bash(
    files={
        "seed.txt": FileInit(
            content="seed\n",
            mode=0o640,
            mtime=datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC),
        ),
        "lazy.txt": LazyFile(provider=lambda: "lazy content\n"),
    },
    cwd="/workspace",
) as bash:
    bash.fs.mkdir("docs")
    bash.fs.write_text("docs/note.txt", "hello\n")
    bash.fs.cp("docs/note.txt", "copy.txt")
    bash.exec("ln -s copy.txt link.txt")

    stat = bash.fs.stat("copy.txt")
    print(stat.mode)
    print(bash.fs.readlink("link.txt"))
    print(bash.fs.realpath("link.txt"))

Per-exec options

  • env: environment variables for this execution only
  • cwd: working directory for this execution only
  • stdin: standard input passed to the script
  • args: argv passed directly to the first command
  • replace_env: start with an empty environment instead of merging
  • raw_script: skip leading-whitespace normalization
  • timeout: cooperative timeout in seconds

AsyncBash.exec(...) accepts the same options; you just await the call.

Option Hooks and Callback Surfaces

The wrapper exposes upstream construction-time hooks as Python callables and protocol-style objects.

  • fetch: intercept or implement HTTP requests for curl and other network consumers
  • logger: receive upstream info(...) / debug(...) events
  • trace: receive structured TraceEvent timing callbacks
  • coverage: receive feature-hit notifications
  • defense_in_depth: configure the upstream defense layer and optionally receive SecurityViolation objects
from collections.abc import Mapping

from just_bash import Bash, DefenseInDepthConfig, FetchRequest, FetchResult


class Logger:
    def info(self, message: str, data: Mapping[str, object] | None = None) -> None:
        print("INFO", message, data)

    def debug(self, message: str, data: Mapping[str, object] | None = None) -> None:
        print("DEBUG", message, data)


class Coverage:
    def __init__(self) -> None:
        self.hits: list[str] = []

    def hit(self, feature: str) -> None:
        self.hits.append(feature)


coverage = Coverage()
trace_events = []
violations = []


def fetch(request: FetchRequest) -> FetchResult:
    return FetchResult(
        status=200,
        status_text="OK",
        headers={"content-type": "text/plain"},
        body="hello from fetch\n",
        url=request.url,
    )


with Bash(
    logger=Logger(),
    trace=trace_events.append,
    coverage=coverage,
    fetch=fetch,
    javascript=True,
    defense_in_depth=DefenseInDepthConfig(enabled=True, audit_mode=True, on_violation=violations.append),
) as bash:
    print(bash.exec("curl -s https://example.com").stdout, end="")
    bash.exec("find . -maxdepth 1 -type f")

See examples/option_hooks.py for a runnable end-to-end example.

Optional Capabilities

Network Access

Network access is disabled by default. Enable it with network=....

from just_bash import Bash

with Bash(
    network={
        "allowedUrlPrefixes": [
            "http://example.com",
            "https://api.github.com/repos/vercel-labs/",
        ],
    }
) as bash:
    result = bash.exec("curl -s http://example.com | html-to-markdown | head -n 12")
    print(result.stdout, end="")

Like upstream just-bash, curl only exists when network access is configured. The repository example examples/network_access.py demonstrates allow-listed methods and header transforms using a local HTTP fixture, so it stays smoke-testable without depending on the public internet.

Python Support

The Python wrapper passes python=True through to upstream just-bash.

from just_bash import Bash

with Bash(python=True) as bash:
    result = bash.exec('python -c "print(sum([2, 3, 5]))"')
    print(result.stdout, end="")

JavaScript Support

The Python wrapper passes javascript=True or JavaScriptConfig(...) through to upstream just-bash.

from just_bash import Bash, JavaScriptConfig

with Bash(javascript=JavaScriptConfig(bootstrap="globalThis.prefix = 'bootstrapped';")) as bash:
    result = bash.exec("js-exec -c 'console.log(globalThis.prefix + \":\" + (2 + 3))'")
    print(result.stdout, end="")

See examples/configuration_and_runtimes.py for a runnable end-to-end example.

Broader Upstream Exports

The wrapper now also exposes the main parser / transform / sandbox / security helper surfaces that map cleanly into Python.

Command registry helpers and parser helpers

from just_bash import get_command_names, parse, serialize

script = "echo hello | grep h"
ast = parse(script)

print("echo" in get_command_names())
print(ast["type"])
print(serialize(ast))

Transform pipeline helpers

from datetime import UTC, datetime

from just_bash import Bash, BashTransformPipeline, CommandCollectorPlugin, TeePlugin

pipeline = (
    BashTransformPipeline()
    .use(TeePlugin(output_dir="/tmp/logs", timestamp=datetime(2024, 1, 15, 10, 30, 45, 123000, tzinfo=UTC)))
    .use(CommandCollectorPlugin())
)
result = pipeline.transform("echo hello | grep h")
print(result.metadata)

with Bash() as bash:
    bash.register_transform_plugin(CommandCollectorPlugin())
    transformed = bash.transform("echo hello | grep h")
    exec_result = bash.exec("echo hello | grep h")
    print(transformed.metadata)
    print(exec_result.metadata)

Sandbox helpers

from just_bash import Sandbox, SandboxOptions

with Sandbox.create(SandboxOptions(cwd="/app")) as sandbox:
    sandbox.write_files({"/app/hello.txt": "hello from sandbox\n"})
    command = sandbox.run_command("cat /app/hello.txt")
    print(command.stdout(), end="")

Async code can use AsyncSandbox with the same high-level shape.

Security helpers

from just_bash import SecurityViolation, SecurityViolationLogger

logger = SecurityViolationLogger(include_stack_traces=False)
logger.record(
    SecurityViolation(
        timestamp=1,
        type="eval",
        message="blocked eval",
        path="globalThis.eval",
    )
)
print(logger.get_summary())

Examples

The repo includes a Python examples/ directory that mirrors the spirit of the vendored upstream examples and README. These examples are smoke-tested from the repo root so they stay aligned with the shipped public API:

File What it shows
examples/quickstart_sync.py Basic synchronous usage, shell-state reset semantics, shared filesystem state, and bash.fs helpers
examples/quickstart_async.py Native-async usage with AsyncBash and async filesystem helpers
examples/custom_commands_sync.py A Python port of the upstream custom-command showcase
examples/custom_commands_async.py Async custom commands with nested async exec
examples/configuration_and_runtimes.py Session config, per-exec overrides, replace_env, Python, and JavaScript runtimes
examples/filesystem_surfaces.py FileInit, LazyFile, FsStat, DirentEntry, and the session-bound filesystem API
examples/network_access.py Allow-listed network access, method policy, and header transforms via a local HTTP fixture
examples/option_hooks.py Python callback surfaces for fetch, logger, trace, coverage, and defense_in_depth
examples/parser_and_command_registry.py Command-name helpers plus standalone parse(...) / serialize(...)
examples/transforms.py Standalone transform pipelines and session-integrated transform registration
examples/sandbox.py Upstream-style sandbox helpers, detached commands, and file IO
examples/security_helpers.py Security violation logging helpers

See examples/README.md for run instructions.

Result Handling

exec() returns an ExecResult with:

  • stdout: str
  • stderr: str
  • exit_code: int
  • ok: bool
  • check() / check_returncode()

Example:

from just_bash import Bash

with Bash() as bash:
    result = bash.exec("false")

if not result.ok:
    print(result.exit_code)
    print(result.stderr)

Backend Selection

By default the package uses its vendored just-bash runtime and resolves Node.js in this order:

  1. node_command= passed to Bash(...) or AsyncBash(...)
  2. JUST_BASH_NODE
  3. the first-party bundled Node provider installed by just-py-bash[node]
  4. a system node on PATH

When javascript=True or JavaScriptConfig(...) is enabled, the resolved Node.js runtime must be at least 22.6 because upstream just-bash's js-exec worker depends on node:module.stripTypeScriptTypes. If the resolved runtime is too old, the wrapper raises UnsupportedRuntimeConfigurationError before opening the session and suggests upgrading Node, installing just-py-bash[node], or overriding node_command=.

To point at a different just-bash backend artifact, set:

  • JUST_BASH_JS_ENTRY
  • JUST_BASH_PACKAGE_JSON
  • optionally JUST_BASH_NODE

If you provide only js_entry= or JUST_BASH_JS_ENTRY, the wrapper will try to infer the matching package.json by walking parent directories. That works for both dist/index.js and dist/bundle/index.js, but you can still pass package_json= / JUST_BASH_PACKAGE_JSON explicitly when you want to be precise.

CLI Launchers

The Python package now ships thin launchers over the upstream CLI assets:

  • just-py-bash → delegates to upstream just-bash
  • just-py-bash-shell → delegates to upstream just-bash-shell

These launchers forward argv, stdin, stdout, stderr, and the final exit code directly to the upstream CLI implementation. The Python package keeps Python-specific binary names, but CLI semantics come from upstream just-bash rather than a separate Python reimplementation.

Examples:

just-py-bash -c 'echo hello'
echo 'pwd' | just-py-bash
just-py-bash ./script.sh
just-py-bash --json -c 'echo hello'
just-py-bash-shell --cwd /

Scope Compared to Upstream TypeScript API

The wrapper now covers the main upstream session API, filesystem config and session-fs surfaces, option hooks, command-name helpers, standalone parser/serializer helpers, the built-in transform pipeline/plugin surfaces (BashTransformPipeline, CommandCollectorPlugin, TeePlugin), upstream-style sandbox/security helper utilities, and thin CLI delegation via just-py-bash / just-py-bash-shell.

What it still does not expose is the live TypeScript-side filesystem adapter interface for plugging arbitrary Python filesystem implementations directly into upstream just-bash. What it now does expose is the full vendored session-facing filesystem surface on bash.fs / async_bash.fs, including append_text / append_bytes, lstat, readdir_with_file_types, resolve_path, get_all_paths, symlink, link, and utimes, alongside the existing text/bytes helpers and core session operations. If you need to implement a new low-level filesystem backend in TypeScript, use upstream just-bash. If you want the Pythonic session-oriented shell API plus the portable parser / transform / sandbox / security helper surfaces described above, use just-py-bash.

Contributing

See the repo README for development setup, Makefile recipes, conformance testing, and release flow.

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

just_py_bash-2.14.2.post1.tar.gz (8.6 MB view details)

Uploaded Source

Built Distribution

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

just_py_bash-2.14.2.post1-py3-none-any.whl (8.8 MB view details)

Uploaded Python 3

File details

Details for the file just_py_bash-2.14.2.post1.tar.gz.

File metadata

  • Download URL: just_py_bash-2.14.2.post1.tar.gz
  • Upload date:
  • Size: 8.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for just_py_bash-2.14.2.post1.tar.gz
Algorithm Hash digest
SHA256 61c73bad1b9d5cbb9fefac2459aaedf29c2d25252cb4408f1a1e2e4af4afde49
MD5 e222d05b021b7ceb5df335fcc5abbb69
BLAKE2b-256 b5f4bce031478ccbb5dd772795d753df761023fbb132c13ee8fe00f87f9bfb5d

See more details on using hashes here.

Provenance

The following attestation bundles were made for just_py_bash-2.14.2.post1.tar.gz:

Publisher: release.yml on nathan-gage/just-py-bash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file just_py_bash-2.14.2.post1-py3-none-any.whl.

File metadata

File hashes

Hashes for just_py_bash-2.14.2.post1-py3-none-any.whl
Algorithm Hash digest
SHA256 540da92113b5dc07f62bd0e74d4ec4f383d7a9da3152c6c404a83da240ffdb7e
MD5 d9a86252d02ea9f44ac1220fc686f6c1
BLAKE2b-256 8f9672088dc7b8a908678c542b8de6afaeb988ec2cc400c8e3085445044d455d

See more details on using hashes here.

Provenance

The following attestation bundles were made for just_py_bash-2.14.2.post1-py3-none-any.whl:

Publisher: release.yml on nathan-gage/just-py-bash

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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