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.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. 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
+ [![PyPI version](https://badge.fury.io/py/just-bash.svg)](https://pypi.org/project/just-bash/)
34
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
35
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](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.