just-bash 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- just_bash/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
just_bash/types.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Core types for just-bash."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol, Callable, Awaitable, Optional, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ExecResult:
|
|
9
|
+
"""Result of executing a command or script."""
|
|
10
|
+
|
|
11
|
+
stdout: str = ""
|
|
12
|
+
stderr: str = ""
|
|
13
|
+
exit_code: int = 0
|
|
14
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BashExecResult(ExecResult):
|
|
19
|
+
"""Extended result with environment state."""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Convenience constants
|
|
25
|
+
OK = ExecResult(stdout="", stderr="", exit_code=0)
|
|
26
|
+
FAIL = ExecResult(stdout="", stderr="", exit_code=1)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ExecutionLimits:
|
|
31
|
+
"""Configurable execution limits for security."""
|
|
32
|
+
|
|
33
|
+
max_call_depth: int = 100
|
|
34
|
+
max_command_count: int = 100_000
|
|
35
|
+
max_loop_iterations: int = 10_000
|
|
36
|
+
max_awk_iterations: int = 10_000
|
|
37
|
+
max_sed_iterations: int = 10_000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class NetworkConfig:
|
|
42
|
+
"""Network access configuration."""
|
|
43
|
+
|
|
44
|
+
allowed_url_prefixes: list[str] = field(default_factory=list)
|
|
45
|
+
allowed_methods: list[str] = field(default_factory=lambda: ["GET", "POST", "PUT", "DELETE"])
|
|
46
|
+
max_redirects: int = 10
|
|
47
|
+
timeout_ms: int = 30_000
|
|
48
|
+
dangerously_allow_full_internet_access: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class IFileSystem(Protocol):
|
|
52
|
+
"""Abstract filesystem interface."""
|
|
53
|
+
|
|
54
|
+
async def read_file(self, path: str, encoding: str = "utf-8") -> str:
|
|
55
|
+
"""Read file contents as string."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
async def read_file_bytes(self, path: str) -> bytes:
|
|
59
|
+
"""Read file contents as bytes."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
async def write_file(
|
|
63
|
+
self, path: str, content: str | bytes, encoding: str = "utf-8"
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Write content to file."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
async def append_file(self, path: str, content: str | bytes) -> None:
|
|
69
|
+
"""Append content to file."""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
async def exists(self, path: str) -> bool:
|
|
73
|
+
"""Check if path exists."""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
async def is_file(self, path: str) -> bool:
|
|
77
|
+
"""Check if path is a file."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
async def is_directory(self, path: str) -> bool:
|
|
81
|
+
"""Check if path is a directory."""
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
async def mkdir(self, path: str, recursive: bool = False) -> None:
|
|
85
|
+
"""Create directory."""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
async def readdir(self, path: str) -> list[str]:
|
|
89
|
+
"""List directory contents."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
async def rm(self, path: str, recursive: bool = False, force: bool = False) -> None:
|
|
93
|
+
"""Remove file or directory."""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
async def stat(self, path: str) -> "FsStat":
|
|
97
|
+
"""Get file/directory stats."""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
async def chmod(self, path: str, mode: int) -> None:
|
|
101
|
+
"""Change file mode."""
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
async def symlink(self, target: str, link_path: str) -> None:
|
|
105
|
+
"""Create symbolic link."""
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
async def readlink(self, path: str) -> str:
|
|
109
|
+
"""Read symbolic link target."""
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def resolve_path(self, base: str, path: str) -> str:
|
|
113
|
+
"""Resolve path relative to base."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class FsStat:
|
|
119
|
+
"""File/directory statistics."""
|
|
120
|
+
|
|
121
|
+
is_file: bool = False
|
|
122
|
+
is_directory: bool = False
|
|
123
|
+
is_symbolic_link: bool = False
|
|
124
|
+
mode: int = 0o644
|
|
125
|
+
size: int = 0
|
|
126
|
+
mtime: float = 0.0
|
|
127
|
+
nlink: int = 1
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class CommandExecOptions:
|
|
132
|
+
"""Options for exec calls within commands."""
|
|
133
|
+
|
|
134
|
+
cwd: str
|
|
135
|
+
"""Working directory for the exec."""
|
|
136
|
+
|
|
137
|
+
env: dict[str, str] | None = None
|
|
138
|
+
"""Environment variables to merge."""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class CommandContext:
|
|
143
|
+
"""Context provided to command execution."""
|
|
144
|
+
|
|
145
|
+
fs: IFileSystem
|
|
146
|
+
"""Virtual filesystem interface."""
|
|
147
|
+
|
|
148
|
+
cwd: str
|
|
149
|
+
"""Current working directory."""
|
|
150
|
+
|
|
151
|
+
env: dict[str, str]
|
|
152
|
+
"""Environment variables."""
|
|
153
|
+
|
|
154
|
+
stdin: str = ""
|
|
155
|
+
"""Standard input content."""
|
|
156
|
+
|
|
157
|
+
limits: ExecutionLimits | None = None
|
|
158
|
+
"""Execution limits configuration."""
|
|
159
|
+
|
|
160
|
+
exec: Optional[Callable[[str, dict[str, Any]], Awaitable[ExecResult]]] = None
|
|
161
|
+
"""Execute a subcommand (for xargs, bash -c, etc.)."""
|
|
162
|
+
|
|
163
|
+
fetch: Optional[Any] = None
|
|
164
|
+
"""Secure fetch function for network requests (for curl)."""
|
|
165
|
+
|
|
166
|
+
get_registered_commands: Optional[Callable[[], list[str]]] = None
|
|
167
|
+
"""Returns names of all registered commands (for help)."""
|
|
168
|
+
|
|
169
|
+
sleep: Optional[Callable[[float], Awaitable[None]]] = None
|
|
170
|
+
"""Custom sleep implementation for testing."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Command(Protocol):
|
|
174
|
+
"""Protocol for command implementations."""
|
|
175
|
+
|
|
176
|
+
name: str
|
|
177
|
+
|
|
178
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
179
|
+
"""Execute the command with given arguments and context."""
|
|
180
|
+
...
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: just-bash
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: A pure Python bash interpreter with in-memory virtual filesystem
|
|
5
|
+
Project-URL: Homepage, https://github.com/dbreunig/just-bash-py
|
|
6
|
+
Project-URL: Repository, https://github.com/dbreunig/just-bash-py
|
|
7
|
+
Author: Drew Breunig
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: bash,interpreter,sandbox,shell,virtual-filesystem
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: aiofiles>=23.0
|
|
21
|
+
Requires-Dist: aiohttp>=3.9
|
|
22
|
+
Requires-Dist: markdownify>=0.11
|
|
23
|
+
Requires-Dist: nest-asyncio>=1.5
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# just-bash-py (pre-release)
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/just-bash/)
|
|
34
|
+
[](https://www.python.org/downloads/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
A pure Python bash interpreter with an in-memory virtual filesystem, designed for AI agents needing a secure, sandboxed bash environment.
|
|
38
|
+
|
|
39
|
+
This is a Python port of [just-bash](https://github.com/vercel-labs/just-bash), the emulated bash interpreter for TypeScript, from Vercel.
|
|
40
|
+
|
|
41
|
+
**This is a pre-release.** This as much a demonstration of coding agents' ability to implement software given a tight spec and high test coverage, as [discussed here](https://www.dbreunig.com/2026/01/08/a-software-library-with-no-code.html) and [here](https://github.com/dbreunig/whenwords).
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Pure Python** - No external binaries, no WASM dependencies
|
|
46
|
+
- **Flexible filesystems** - In-memory, real filesystem access, copy-on-write overlays, or mount multiple sources
|
|
47
|
+
- **70+ commands** - grep, sed, awk, jq, curl, and more
|
|
48
|
+
- **Full bash syntax** - Pipes, redirections, variables, arrays, functions, control flow
|
|
49
|
+
- **32 shell builtins** - cd, export, declare, test, and more
|
|
50
|
+
- **Async execution** - Built on asyncio for non-blocking operation
|
|
51
|
+
- **Security limits** - Prevent infinite loops, excessive recursion, runaway execution
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install just-bash
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from just_bash import Bash
|
|
63
|
+
|
|
64
|
+
bash = Bash()
|
|
65
|
+
|
|
66
|
+
# Simple command
|
|
67
|
+
result = await bash.exec('echo "Hello, World!"')
|
|
68
|
+
print(result.stdout) # Hello, World!
|
|
69
|
+
|
|
70
|
+
# Pipes and text processing
|
|
71
|
+
result = await bash.exec('echo "banana apple cherry" | tr " " "\\n" | sort')
|
|
72
|
+
print(result.stdout) # apple\nbanana\ncherry\n
|
|
73
|
+
|
|
74
|
+
# Variables and arithmetic
|
|
75
|
+
result = await bash.exec('x=5; echo $((x * 2))')
|
|
76
|
+
print(result.stdout) # 10
|
|
77
|
+
|
|
78
|
+
# Arrays
|
|
79
|
+
result = await bash.exec('arr=(a b c); echo "${arr[@]}"')
|
|
80
|
+
print(result.stdout) # a b c
|
|
81
|
+
|
|
82
|
+
# In-memory files
|
|
83
|
+
result = await bash.exec('echo "test" > /tmp/file.txt; cat /tmp/file.txt')
|
|
84
|
+
print(result.stdout) # test
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A synchronous `bash.run()` wrapper is also available and works in any context, including Jupyter notebooks.
|
|
88
|
+
|
|
89
|
+
## Demo
|
|
90
|
+
|
|
91
|
+
Run the interactive demo to see all features in action:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python examples/demo.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This demonstrates variables, arrays, control flow, pipes, text processing, JSON handling with jq, functions, and more.
|
|
98
|
+
|
|
99
|
+
## API
|
|
100
|
+
|
|
101
|
+
### Bash Class
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from just_bash import Bash
|
|
105
|
+
|
|
106
|
+
# Create with optional initial files
|
|
107
|
+
bash = Bash(files={
|
|
108
|
+
"/data/input.txt": "line1\nline2\nline3\n",
|
|
109
|
+
"/config.json": '{"key": "value"}'
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
# Execute commands
|
|
113
|
+
result = await bash.exec("cat /data/input.txt | wc -l")
|
|
114
|
+
|
|
115
|
+
# Result object
|
|
116
|
+
print(result.stdout) # Standard output
|
|
117
|
+
print(result.stderr) # Standard error
|
|
118
|
+
print(result.exit_code) # Exit code (0 = success)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Configuration Options
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
bash = Bash(
|
|
125
|
+
files={...}, # Initial filesystem contents
|
|
126
|
+
env={...}, # Environment variables
|
|
127
|
+
cwd="/home/user", # Working directory
|
|
128
|
+
network=NetworkConfig(...), # Network configuration (for curl)
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Filesystem Options
|
|
133
|
+
|
|
134
|
+
just-bash provides four filesystem implementations for different use cases:
|
|
135
|
+
|
|
136
|
+
#### InMemoryFs (Default)
|
|
137
|
+
|
|
138
|
+
Pure in-memory filesystem - completely sandboxed with no disk access.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from just_bash import Bash
|
|
142
|
+
|
|
143
|
+
# Default: in-memory filesystem with optional initial files
|
|
144
|
+
bash = Bash(files={
|
|
145
|
+
"/data/input.txt": "hello world\n",
|
|
146
|
+
"/config.json": '{"key": "value"}'
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
result = await bash.exec("cat /data/input.txt")
|
|
150
|
+
print(result.stdout) # hello world
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### ReadWriteFs
|
|
154
|
+
|
|
155
|
+
Direct access to the real filesystem, rooted at a specific directory. All paths are translated relative to the root.
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from just_bash import Bash
|
|
159
|
+
from just_bash.fs import ReadWriteFs, ReadWriteFsOptions
|
|
160
|
+
|
|
161
|
+
# Access real files under /path/to/project
|
|
162
|
+
fs = ReadWriteFs(ReadWriteFsOptions(root="/path/to/project"))
|
|
163
|
+
bash = Bash(fs=fs, cwd="/")
|
|
164
|
+
|
|
165
|
+
# /src/main.py in bash maps to /path/to/project/src/main.py on disk
|
|
166
|
+
result = await bash.exec("cat /src/main.py")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Warning**: ReadWriteFs provides direct disk access. Use with caution.
|
|
170
|
+
|
|
171
|
+
#### OverlayFs
|
|
172
|
+
|
|
173
|
+
Copy-on-write overlay - reads from the real filesystem, but all writes go to an in-memory layer. The real filesystem is never modified.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from just_bash import Bash
|
|
177
|
+
from just_bash.fs import OverlayFs, OverlayFsOptions
|
|
178
|
+
|
|
179
|
+
# Overlay real files at /home/user/project, changes stay in memory
|
|
180
|
+
fs = OverlayFs(OverlayFsOptions(
|
|
181
|
+
root="/path/to/real/project",
|
|
182
|
+
mount_point="/home/user/project"
|
|
183
|
+
))
|
|
184
|
+
bash = Bash(fs=fs)
|
|
185
|
+
|
|
186
|
+
# Read real files
|
|
187
|
+
result = await bash.exec("cat /home/user/project/README.md")
|
|
188
|
+
|
|
189
|
+
# Writes only affect the in-memory layer
|
|
190
|
+
await bash.exec("echo 'modified' > /home/user/project/README.md")
|
|
191
|
+
# Real file on disk is unchanged!
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Use cases:
|
|
195
|
+
- Safe experimentation with real project files
|
|
196
|
+
- Testing scripts without modifying actual files
|
|
197
|
+
- AI agents that need to read real code but not write to disk
|
|
198
|
+
|
|
199
|
+
#### MountableFs
|
|
200
|
+
|
|
201
|
+
Mount multiple filesystems at different paths, similar to Unix mount points.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from just_bash import Bash
|
|
205
|
+
from just_bash.fs import (
|
|
206
|
+
MountableFs, MountableFsOptions, MountConfig,
|
|
207
|
+
InMemoryFs, ReadWriteFs, ReadWriteFsOptions, OverlayFs, OverlayFsOptions
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Create a mountable filesystem with multiple sources
|
|
211
|
+
fs = MountableFs(MountableFsOptions(
|
|
212
|
+
base=InMemoryFs(), # Default for paths outside mounts
|
|
213
|
+
mounts=[
|
|
214
|
+
# Mount real project at /project (read-write)
|
|
215
|
+
MountConfig(
|
|
216
|
+
mount_point="/project",
|
|
217
|
+
filesystem=ReadWriteFs(ReadWriteFsOptions(root="/path/to/project"))
|
|
218
|
+
),
|
|
219
|
+
# Mount another project as overlay (read-only to disk)
|
|
220
|
+
MountConfig(
|
|
221
|
+
mount_point="/reference",
|
|
222
|
+
filesystem=OverlayFs(OverlayFsOptions(
|
|
223
|
+
root="/path/to/other/project",
|
|
224
|
+
mount_point="/"
|
|
225
|
+
))
|
|
226
|
+
),
|
|
227
|
+
]
|
|
228
|
+
))
|
|
229
|
+
|
|
230
|
+
bash = Bash(fs=fs)
|
|
231
|
+
|
|
232
|
+
# Access different filesystems through unified paths
|
|
233
|
+
await bash.exec("ls /project") # Real filesystem
|
|
234
|
+
await bash.exec("ls /reference") # Overlay filesystem
|
|
235
|
+
await bash.exec("ls /tmp") # In-memory (base)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
#### Direct Filesystem Access
|
|
239
|
+
|
|
240
|
+
You can also access the filesystem directly through the `bash.fs` property:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
import asyncio
|
|
244
|
+
from just_bash import Bash
|
|
245
|
+
|
|
246
|
+
bash = Bash(files={"/data.txt": "initial content"})
|
|
247
|
+
|
|
248
|
+
# Async filesystem operations
|
|
249
|
+
async def main():
|
|
250
|
+
# Read
|
|
251
|
+
content = await bash.fs.read_file("/data.txt")
|
|
252
|
+
|
|
253
|
+
# Write
|
|
254
|
+
await bash.fs.write_file("/output.txt", "new content")
|
|
255
|
+
|
|
256
|
+
# Check existence
|
|
257
|
+
exists = await bash.fs.exists("/data.txt")
|
|
258
|
+
|
|
259
|
+
# List directory
|
|
260
|
+
files = await bash.fs.readdir("/")
|
|
261
|
+
|
|
262
|
+
# Get file stats
|
|
263
|
+
stat = await bash.fs.stat("/data.txt")
|
|
264
|
+
print(f"Size: {stat.size}, Mode: {oct(stat.mode)}")
|
|
265
|
+
|
|
266
|
+
asyncio.run(main())
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Security
|
|
270
|
+
|
|
271
|
+
- **No native execution** - All commands are pure Python implementations
|
|
272
|
+
- **Network disabled by default** - curl requires explicit enablement
|
|
273
|
+
- **Execution limits** - Prevents infinite loops and excessive resource usage
|
|
274
|
+
- **Filesystem isolation** - Virtual filesystem keeps host system safe
|
|
275
|
+
- **SQLite sandboxed** - Only in-memory databases allowed
|
|
276
|
+
|
|
277
|
+
## Supported Features
|
|
278
|
+
|
|
279
|
+
### Shell Syntax
|
|
280
|
+
- Variables: `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR:+alt}`, `${#VAR}`
|
|
281
|
+
- Arrays: `arr=(a b c)`, `${arr[0]}`, `${arr[@]}`, `${#arr[@]}`
|
|
282
|
+
- Arithmetic: `$((expr))`, `((expr))`, increment/decrement, ternary
|
|
283
|
+
- Quoting: Single quotes, double quotes, `$'...'`, escapes
|
|
284
|
+
- Expansion: Brace `{a,b}`, tilde `~`, glob `*.txt`, command `$(cmd)`
|
|
285
|
+
- Control flow: `if/then/else/fi`, `for/do/done`, `while`, `until`, `case`
|
|
286
|
+
- Functions: `func() { ... }`, local variables, return values
|
|
287
|
+
- Pipes: `cmd1 | cmd2 | cmd3`
|
|
288
|
+
- Redirections: `>`, `>>`, `<`, `2>&1`, here-docs
|
|
289
|
+
|
|
290
|
+
### Parameter Expansion
|
|
291
|
+
- Default values: `${var:-default}`, `${var:=default}`
|
|
292
|
+
- Substring: `${var:offset:length}`
|
|
293
|
+
- Pattern removal: `${var#pattern}`, `${var##pattern}`, `${var%pattern}`, `${var%%pattern}`
|
|
294
|
+
- Replacement: `${var/pattern/string}`, `${var//pattern/string}`
|
|
295
|
+
- Case modification: `${var^^}`, `${var,,}`, `${var^}`, `${var,}`
|
|
296
|
+
- Length: `${#var}`, `${#arr[@]}`
|
|
297
|
+
- Indirection: `${!var}`, `${!prefix*}`, `${!arr[@]}`
|
|
298
|
+
- Transforms: `${var@Q}`, `${var@a}`, `${var@A}`
|
|
299
|
+
|
|
300
|
+
### Conditionals
|
|
301
|
+
- Test command: `[ -f file ]`, `[ "$a" = "$b" ]`
|
|
302
|
+
- Extended test: `[[ $var == pattern ]]`, `[[ $var =~ regex ]]`
|
|
303
|
+
- Arithmetic test: `(( x > 5 ))`
|
|
304
|
+
- File tests: `-e`, `-f`, `-d`, `-r`, `-w`, `-x`, `-s`, `-L`
|
|
305
|
+
- String tests: `-z`, `-n`, `=`, `!=`, `<`, `>`
|
|
306
|
+
- Numeric tests: `-eq`, `-ne`, `-lt`, `-le`, `-gt`, `-ge`
|
|
307
|
+
|
|
308
|
+
## Shell Builtins
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
: . [ alias break builtin cd command
|
|
312
|
+
continue declare eval exec exit export false let
|
|
313
|
+
local mapfile readarray readonly return set shift shopt
|
|
314
|
+
source test true type typeset unalias unset wait
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Available Commands
|
|
318
|
+
|
|
319
|
+
### File Operations
|
|
320
|
+
```
|
|
321
|
+
cat chmod cp find ln ls mkdir mv
|
|
322
|
+
rm stat touch tree
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Text Processing
|
|
326
|
+
```
|
|
327
|
+
awk column comm cut diff expand fold grep
|
|
328
|
+
egrep fgrep head join nl od paste rev
|
|
329
|
+
rg sed sort split strings tac tail tee
|
|
330
|
+
tr unexpand uniq wc
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Data Processing
|
|
334
|
+
```
|
|
335
|
+
jq yq xan sqlite3
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
#### xan - CSV Toolkit
|
|
339
|
+
|
|
340
|
+
The `xan` command provides CSV manipulation capabilities. Most commands are implemented:
|
|
341
|
+
|
|
342
|
+
**Implemented:**
|
|
343
|
+
```
|
|
344
|
+
headers count head tail slice select
|
|
345
|
+
drop rename filter search sort reverse
|
|
346
|
+
behead enum shuffle sample dedup top
|
|
347
|
+
cat transpose fixlengths flatten explode implode
|
|
348
|
+
split view stats frequency to json from json
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Not Yet Implemented** (require expression evaluation):
|
|
352
|
+
```
|
|
353
|
+
join agg groupby map transform pivot
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
Example usage:
|
|
357
|
+
```python
|
|
358
|
+
# Show column names
|
|
359
|
+
await bash.exec("xan headers data.csv")
|
|
360
|
+
|
|
361
|
+
# Filter and select
|
|
362
|
+
await bash.exec("xan filter 'age > 30' data.csv | xan select name,age")
|
|
363
|
+
|
|
364
|
+
# Convert to JSON
|
|
365
|
+
await bash.exec("xan to json data.csv")
|
|
366
|
+
|
|
367
|
+
# Sample random rows
|
|
368
|
+
await bash.exec("xan sample 10 --seed 42 data.csv")
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Path Utilities
|
|
372
|
+
```
|
|
373
|
+
basename dirname pwd readlink which
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Compression & Encoding
|
|
377
|
+
```
|
|
378
|
+
base64 gzip gunzip zcat md5sum sha1sum sha256sum tar
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### System & Environment
|
|
382
|
+
```
|
|
383
|
+
alias clear date du echo env expr false
|
|
384
|
+
file help history hostname printenv printf read seq
|
|
385
|
+
sleep timeout true unalias xargs
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Network
|
|
389
|
+
```
|
|
390
|
+
curl (disabled by default)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Shell
|
|
394
|
+
```
|
|
395
|
+
bash sh
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
Apache 2.0
|
|
401
|
+
|
|
402
|
+
## Backlog
|
|
403
|
+
|
|
404
|
+
Future improvements under consideration:
|
|
405
|
+
|
|
406
|
+
- **Separate sync/async implementations**: Replace the current `nest_asyncio`-based `run()` wrapper with a truly synchronous implementation. This would follow the pattern used by libraries like httpx (`Client` vs `AsyncClient`) and the OpenAI SDK, providing cleaner separation without event loop patching.
|
|
407
|
+
|
|
408
|
+
## Acknowledgments
|
|
409
|
+
|
|
410
|
+
This project is a Python port of [just-bash](https://github.com/vercel-labs/just-bash) by Vercel. The TypeScript implementation provided the design patterns, test cases, and feature specifications that guided this Python implementation.
|