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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Export builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: export [name[=value] ...]
|
|
4
|
+
|
|
5
|
+
Mark variables for export to child processes. If no arguments are given,
|
|
6
|
+
list all exported variables.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..types import InterpreterContext
|
|
14
|
+
from ...types import ExecResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def handle_export(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
18
|
+
"""Execute the export builtin."""
|
|
19
|
+
from ...types import ExecResult
|
|
20
|
+
|
|
21
|
+
# No arguments: list all exported variables
|
|
22
|
+
if not args:
|
|
23
|
+
lines = []
|
|
24
|
+
for k, v in sorted(ctx.state.env.items()):
|
|
25
|
+
# Skip internal variables
|
|
26
|
+
if k.startswith("PIPESTATUS_") or k == "?" or k == "#":
|
|
27
|
+
continue
|
|
28
|
+
# Escape special characters in value
|
|
29
|
+
escaped_v = v.replace("\\", "\\\\").replace('"', '\\"')
|
|
30
|
+
lines.append(f'declare -x {k}="{escaped_v}"')
|
|
31
|
+
return ExecResult(stdout="\n".join(lines) + "\n" if lines else "", stderr="", exit_code=0)
|
|
32
|
+
|
|
33
|
+
# Process each argument
|
|
34
|
+
for arg in args:
|
|
35
|
+
# Skip options
|
|
36
|
+
if arg.startswith("-"):
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
if "=" in arg:
|
|
40
|
+
name, value = arg.split("=", 1)
|
|
41
|
+
else:
|
|
42
|
+
# Export existing variable or create empty
|
|
43
|
+
name = arg
|
|
44
|
+
value = ctx.state.env.get(arg, "")
|
|
45
|
+
|
|
46
|
+
# Validate identifier
|
|
47
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
|
|
48
|
+
return ExecResult(
|
|
49
|
+
stdout="",
|
|
50
|
+
stderr=f"bash: export: '{name}': not a valid identifier\n",
|
|
51
|
+
exit_code=1,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
ctx.state.env[name] = value
|
|
55
|
+
|
|
56
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Let builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: let expr [expr ...]
|
|
4
|
+
|
|
5
|
+
Evaluates arithmetic expressions. Each expr is evaluated as an arithmetic
|
|
6
|
+
expression. Returns 0 if the last expression evaluates to non-zero, 1 otherwise.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..types import InterpreterContext
|
|
13
|
+
from ...types import ExecResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
17
|
+
"""Create an ExecResult."""
|
|
18
|
+
from ...types import ExecResult
|
|
19
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def handle_let(
|
|
23
|
+
ctx: "InterpreterContext", args: list[str]
|
|
24
|
+
) -> "ExecResult":
|
|
25
|
+
"""Execute the let builtin."""
|
|
26
|
+
from ..expansion import evaluate_arithmetic_sync
|
|
27
|
+
from ...parser.parser import Parser
|
|
28
|
+
|
|
29
|
+
if not args:
|
|
30
|
+
return _result("", "bash: let: expression expected\n", 1)
|
|
31
|
+
|
|
32
|
+
parser = Parser()
|
|
33
|
+
last_value = 0
|
|
34
|
+
|
|
35
|
+
for expr_str in args:
|
|
36
|
+
try:
|
|
37
|
+
# Parse and evaluate the arithmetic expression
|
|
38
|
+
arith_expr = parser._parse_arithmetic_expression(expr_str)
|
|
39
|
+
last_value = evaluate_arithmetic_sync(ctx, arith_expr)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
return _result("", f"bash: let: {expr_str}: syntax error\n", 1)
|
|
42
|
+
|
|
43
|
+
# Return 0 if last value is non-zero, 1 if zero
|
|
44
|
+
return _result("", "", 0 if last_value != 0 else 1)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Local builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: local [name[=value] ...]
|
|
4
|
+
|
|
5
|
+
Create local variables for use within a function. When the function
|
|
6
|
+
returns, any local variables are restored to their previous values.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ..types import InterpreterContext
|
|
14
|
+
from ...types import ExecResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def handle_local(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
18
|
+
"""Execute the local builtin."""
|
|
19
|
+
from ...types import ExecResult
|
|
20
|
+
|
|
21
|
+
# Check if we're inside a function
|
|
22
|
+
if not ctx.state.local_scopes:
|
|
23
|
+
return ExecResult(
|
|
24
|
+
stdout="",
|
|
25
|
+
stderr="bash: local: can only be used in a function\n",
|
|
26
|
+
exit_code=1,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
current_scope = ctx.state.local_scopes[-1]
|
|
30
|
+
|
|
31
|
+
for arg in args:
|
|
32
|
+
# Skip options
|
|
33
|
+
if arg.startswith("-"):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
if "=" in arg:
|
|
37
|
+
name, value = arg.split("=", 1)
|
|
38
|
+
else:
|
|
39
|
+
name = arg
|
|
40
|
+
value = ""
|
|
41
|
+
|
|
42
|
+
# Validate identifier
|
|
43
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
|
|
44
|
+
return ExecResult(
|
|
45
|
+
stdout="",
|
|
46
|
+
stderr=f"bash: local: '{name}': not a valid identifier\n",
|
|
47
|
+
exit_code=1,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Save original value for restoration (if not already saved)
|
|
51
|
+
if name not in current_scope:
|
|
52
|
+
current_scope[name] = ctx.state.env.get(name)
|
|
53
|
+
|
|
54
|
+
# Set the new value
|
|
55
|
+
ctx.state.env[name] = value
|
|
56
|
+
|
|
57
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Mapfile/readarray builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: mapfile [-d delim] [-n count] [-O origin] [-s count] [-t] [array]
|
|
4
|
+
readarray [-d delim] [-n count] [-O origin] [-s count] [-t] [array]
|
|
5
|
+
|
|
6
|
+
Reads lines from stdin into an array variable.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-d delim Use delim as line delimiter instead of newline
|
|
10
|
+
-n count Read at most count lines (0 means all)
|
|
11
|
+
-O origin Begin assigning at index origin (default 0)
|
|
12
|
+
-s count Skip the first count lines
|
|
13
|
+
-t Remove trailing delimiter from each line
|
|
14
|
+
array Name of array variable (default: MAPFILE)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..types import InterpreterContext
|
|
21
|
+
from ...types import ExecResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
25
|
+
"""Create an ExecResult."""
|
|
26
|
+
from ...types import ExecResult
|
|
27
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def handle_mapfile(
|
|
31
|
+
ctx: "InterpreterContext", args: list[str], stdin: str = ""
|
|
32
|
+
) -> "ExecResult":
|
|
33
|
+
"""Execute the mapfile/readarray builtin."""
|
|
34
|
+
# Parse options
|
|
35
|
+
delimiter = "\n"
|
|
36
|
+
max_count = 0 # 0 means unlimited
|
|
37
|
+
origin = 0
|
|
38
|
+
skip_count = 0
|
|
39
|
+
strip_trailing = False
|
|
40
|
+
array_name = "MAPFILE"
|
|
41
|
+
|
|
42
|
+
i = 0
|
|
43
|
+
while i < len(args):
|
|
44
|
+
arg = args[i]
|
|
45
|
+
|
|
46
|
+
if arg == "--":
|
|
47
|
+
if i + 1 < len(args):
|
|
48
|
+
array_name = args[i + 1]
|
|
49
|
+
break
|
|
50
|
+
|
|
51
|
+
if arg == "-d" and i + 1 < len(args):
|
|
52
|
+
delimiter = args[i + 1]
|
|
53
|
+
# Handle escape sequences
|
|
54
|
+
if delimiter == "\\n":
|
|
55
|
+
delimiter = "\n"
|
|
56
|
+
elif delimiter == "\\t":
|
|
57
|
+
delimiter = "\t"
|
|
58
|
+
elif delimiter == "":
|
|
59
|
+
delimiter = "\0" # NUL delimiter
|
|
60
|
+
i += 2
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if arg == "-n" and i + 1 < len(args):
|
|
64
|
+
try:
|
|
65
|
+
max_count = int(args[i + 1])
|
|
66
|
+
except ValueError:
|
|
67
|
+
return _result("", f"bash: mapfile: {args[i + 1]}: invalid count\n", 1)
|
|
68
|
+
i += 2
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if arg == "-O" and i + 1 < len(args):
|
|
72
|
+
try:
|
|
73
|
+
origin = int(args[i + 1])
|
|
74
|
+
except ValueError:
|
|
75
|
+
return _result("", f"bash: mapfile: {args[i + 1]}: invalid origin\n", 1)
|
|
76
|
+
i += 2
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if arg == "-s" and i + 1 < len(args):
|
|
80
|
+
try:
|
|
81
|
+
skip_count = int(args[i + 1])
|
|
82
|
+
except ValueError:
|
|
83
|
+
return _result("", f"bash: mapfile: {args[i + 1]}: invalid count\n", 1)
|
|
84
|
+
i += 2
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
if arg == "-t":
|
|
88
|
+
strip_trailing = True
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if arg.startswith("-"):
|
|
93
|
+
# Unknown option - might be combined like -tn
|
|
94
|
+
for c in arg[1:]:
|
|
95
|
+
if c == "t":
|
|
96
|
+
strip_trailing = True
|
|
97
|
+
elif c == "d":
|
|
98
|
+
return _result("", "bash: mapfile: -d: option requires an argument\n", 1)
|
|
99
|
+
elif c == "n":
|
|
100
|
+
return _result("", "bash: mapfile: -n: option requires an argument\n", 1)
|
|
101
|
+
elif c == "O":
|
|
102
|
+
return _result("", "bash: mapfile: -O: option requires an argument\n", 1)
|
|
103
|
+
elif c == "s":
|
|
104
|
+
return _result("", "bash: mapfile: -s: option requires an argument\n", 1)
|
|
105
|
+
else:
|
|
106
|
+
return _result("", f"bash: mapfile: -{c}: invalid option\n", 2)
|
|
107
|
+
i += 1
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Not an option - must be array name
|
|
111
|
+
array_name = arg
|
|
112
|
+
i += 1
|
|
113
|
+
|
|
114
|
+
# Clear existing array elements
|
|
115
|
+
prefix = f"{array_name}_"
|
|
116
|
+
to_remove = [k for k in ctx.state.env if k.startswith(prefix) and not k.startswith(f"{array_name}__")]
|
|
117
|
+
for k in to_remove:
|
|
118
|
+
del ctx.state.env[k]
|
|
119
|
+
|
|
120
|
+
# Mark as array
|
|
121
|
+
ctx.state.env[f"{array_name}__is_array"] = "indexed"
|
|
122
|
+
|
|
123
|
+
# Split input by delimiter
|
|
124
|
+
if not stdin:
|
|
125
|
+
return _result("", "", 0)
|
|
126
|
+
|
|
127
|
+
if delimiter == "\n":
|
|
128
|
+
lines = stdin.split("\n")
|
|
129
|
+
# Remove last empty element if input ends with newline
|
|
130
|
+
if lines and lines[-1] == "":
|
|
131
|
+
lines = lines[:-1]
|
|
132
|
+
else:
|
|
133
|
+
lines = stdin.split(delimiter)
|
|
134
|
+
|
|
135
|
+
# Skip first N lines
|
|
136
|
+
if skip_count > 0:
|
|
137
|
+
lines = lines[skip_count:]
|
|
138
|
+
|
|
139
|
+
# Limit to N lines
|
|
140
|
+
if max_count > 0:
|
|
141
|
+
lines = lines[:max_count]
|
|
142
|
+
|
|
143
|
+
# Store lines in array
|
|
144
|
+
for idx, line in enumerate(lines):
|
|
145
|
+
if strip_trailing:
|
|
146
|
+
# Remove trailing delimiter (already split, so just strip the char if present)
|
|
147
|
+
if delimiter != "\n" and line.endswith(delimiter):
|
|
148
|
+
line = line[:-len(delimiter)]
|
|
149
|
+
|
|
150
|
+
ctx.state.env[f"{array_name}_{origin + idx}"] = line
|
|
151
|
+
|
|
152
|
+
return _result("", "", 0)
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Miscellaneous builtins: colon, true, false, type, command, builtin, exec, wait.
|
|
2
|
+
|
|
3
|
+
These are simple builtins that don't need their own files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..types import InterpreterContext
|
|
10
|
+
from ...types import ExecResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
14
|
+
"""Create an ExecResult."""
|
|
15
|
+
from ...types import ExecResult
|
|
16
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def handle_colon(
|
|
20
|
+
ctx: "InterpreterContext", args: list[str]
|
|
21
|
+
) -> "ExecResult":
|
|
22
|
+
"""Execute the : (colon) builtin - null command, always succeeds."""
|
|
23
|
+
return _result("", "", 0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def handle_true(
|
|
27
|
+
ctx: "InterpreterContext", args: list[str]
|
|
28
|
+
) -> "ExecResult":
|
|
29
|
+
"""Execute the true builtin - always succeeds."""
|
|
30
|
+
return _result("", "", 0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def handle_false(
|
|
34
|
+
ctx: "InterpreterContext", args: list[str]
|
|
35
|
+
) -> "ExecResult":
|
|
36
|
+
"""Execute the false builtin - always fails."""
|
|
37
|
+
return _result("", "", 1)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def handle_type(
|
|
41
|
+
ctx: "InterpreterContext", args: list[str]
|
|
42
|
+
) -> "ExecResult":
|
|
43
|
+
"""Execute the type builtin - display information about command type.
|
|
44
|
+
|
|
45
|
+
Usage: type [-afptP] name [name ...]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
-a Display all locations containing an executable named name
|
|
49
|
+
-f Suppress shell function lookup
|
|
50
|
+
-p Display path to executable (like which)
|
|
51
|
+
-P Force path search even for builtins
|
|
52
|
+
-t Output a single word: alias, keyword, function, builtin, file, or ''
|
|
53
|
+
"""
|
|
54
|
+
from .alias import get_aliases
|
|
55
|
+
|
|
56
|
+
# Parse options
|
|
57
|
+
show_all = False
|
|
58
|
+
no_functions = False
|
|
59
|
+
path_only = False
|
|
60
|
+
force_path = False
|
|
61
|
+
type_only = False
|
|
62
|
+
names = []
|
|
63
|
+
|
|
64
|
+
i = 0
|
|
65
|
+
while i < len(args):
|
|
66
|
+
arg = args[i]
|
|
67
|
+
if arg == "--":
|
|
68
|
+
names.extend(args[i + 1:])
|
|
69
|
+
break
|
|
70
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
71
|
+
for c in arg[1:]:
|
|
72
|
+
if c == "a":
|
|
73
|
+
show_all = True
|
|
74
|
+
elif c == "f":
|
|
75
|
+
no_functions = True
|
|
76
|
+
elif c == "p":
|
|
77
|
+
path_only = True
|
|
78
|
+
elif c == "P":
|
|
79
|
+
force_path = True
|
|
80
|
+
elif c == "t":
|
|
81
|
+
type_only = True
|
|
82
|
+
else:
|
|
83
|
+
return _result("", f"bash: type: -{c}: invalid option\n", 1)
|
|
84
|
+
else:
|
|
85
|
+
names.append(arg)
|
|
86
|
+
i += 1
|
|
87
|
+
|
|
88
|
+
if not names:
|
|
89
|
+
return _result("", "bash: type: usage: type [-afptP] name [name ...]\n", 1)
|
|
90
|
+
|
|
91
|
+
# Keywords
|
|
92
|
+
keywords = {
|
|
93
|
+
"if", "then", "else", "elif", "fi", "case", "esac", "for", "select",
|
|
94
|
+
"while", "until", "do", "done", "in", "function", "time", "coproc",
|
|
95
|
+
"{", "}", "!", "[[", "]]"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Get builtins
|
|
99
|
+
from . import BUILTINS
|
|
100
|
+
|
|
101
|
+
# Get aliases
|
|
102
|
+
aliases = get_aliases(ctx)
|
|
103
|
+
|
|
104
|
+
# Get functions
|
|
105
|
+
functions = getattr(ctx.state, 'functions', {})
|
|
106
|
+
|
|
107
|
+
output = []
|
|
108
|
+
exit_code = 0
|
|
109
|
+
|
|
110
|
+
for name in names:
|
|
111
|
+
found = False
|
|
112
|
+
|
|
113
|
+
# Check alias (unless -f)
|
|
114
|
+
if not no_functions and name in aliases:
|
|
115
|
+
found = True
|
|
116
|
+
if type_only:
|
|
117
|
+
output.append("alias")
|
|
118
|
+
elif path_only:
|
|
119
|
+
pass # -p doesn't show aliases
|
|
120
|
+
else:
|
|
121
|
+
output.append(f"{name} is aliased to `{aliases[name]}'")
|
|
122
|
+
if not show_all:
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
# Check keyword
|
|
126
|
+
if name in keywords:
|
|
127
|
+
found = True
|
|
128
|
+
if type_only:
|
|
129
|
+
output.append("keyword")
|
|
130
|
+
elif not path_only:
|
|
131
|
+
output.append(f"{name} is a shell keyword")
|
|
132
|
+
if not show_all:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Check function (unless -f or -P)
|
|
136
|
+
if not no_functions and not force_path and name in functions:
|
|
137
|
+
found = True
|
|
138
|
+
if type_only:
|
|
139
|
+
output.append("function")
|
|
140
|
+
elif not path_only:
|
|
141
|
+
output.append(f"{name} is a function")
|
|
142
|
+
if not show_all:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
# Check builtin (unless -P)
|
|
146
|
+
if not force_path and name in BUILTINS:
|
|
147
|
+
found = True
|
|
148
|
+
if type_only:
|
|
149
|
+
output.append("builtin")
|
|
150
|
+
elif not path_only:
|
|
151
|
+
output.append(f"{name} is a shell builtin")
|
|
152
|
+
if not show_all:
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# Check command registry
|
|
156
|
+
from ...commands import COMMAND_NAMES
|
|
157
|
+
if name in COMMAND_NAMES:
|
|
158
|
+
found = True
|
|
159
|
+
if type_only:
|
|
160
|
+
output.append("file")
|
|
161
|
+
elif path_only:
|
|
162
|
+
output.append(name)
|
|
163
|
+
else:
|
|
164
|
+
output.append(f"{name} is {name}")
|
|
165
|
+
if not show_all:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if not found:
|
|
169
|
+
if type_only:
|
|
170
|
+
output.append("")
|
|
171
|
+
else:
|
|
172
|
+
output.append(f"bash: type: {name}: not found")
|
|
173
|
+
exit_code = 1
|
|
174
|
+
|
|
175
|
+
if output:
|
|
176
|
+
return _result("\n".join(output) + "\n", "", exit_code)
|
|
177
|
+
return _result("", "", exit_code)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def handle_command(
|
|
181
|
+
ctx: "InterpreterContext", args: list[str]
|
|
182
|
+
) -> "ExecResult":
|
|
183
|
+
"""Execute the command builtin - run command bypassing functions.
|
|
184
|
+
|
|
185
|
+
Usage: command [-pVv] command [arguments ...]
|
|
186
|
+
|
|
187
|
+
Options:
|
|
188
|
+
-p Use a default path to search for command
|
|
189
|
+
-v Display description of command (like type)
|
|
190
|
+
-V Display verbose description of command
|
|
191
|
+
"""
|
|
192
|
+
# Parse options
|
|
193
|
+
describe = False
|
|
194
|
+
verbose = False
|
|
195
|
+
use_default_path = False
|
|
196
|
+
cmd_args = []
|
|
197
|
+
|
|
198
|
+
i = 0
|
|
199
|
+
while i < len(args):
|
|
200
|
+
arg = args[i]
|
|
201
|
+
if arg == "--":
|
|
202
|
+
cmd_args = args[i + 1:]
|
|
203
|
+
break
|
|
204
|
+
elif arg.startswith("-") and len(arg) > 1 and not cmd_args:
|
|
205
|
+
for c in arg[1:]:
|
|
206
|
+
if c == "p":
|
|
207
|
+
use_default_path = True
|
|
208
|
+
elif c == "v":
|
|
209
|
+
describe = True
|
|
210
|
+
elif c == "V":
|
|
211
|
+
verbose = True
|
|
212
|
+
else:
|
|
213
|
+
return _result("", f"bash: command: -{c}: invalid option\n", 1)
|
|
214
|
+
else:
|
|
215
|
+
cmd_args = args[i:]
|
|
216
|
+
break
|
|
217
|
+
i += 1
|
|
218
|
+
|
|
219
|
+
if not cmd_args:
|
|
220
|
+
if describe or verbose:
|
|
221
|
+
return _result("", "", 0)
|
|
222
|
+
return _result("", "", 0)
|
|
223
|
+
|
|
224
|
+
cmd_name = cmd_args[0]
|
|
225
|
+
|
|
226
|
+
# Handle -v or -V: describe the command
|
|
227
|
+
if describe or verbose:
|
|
228
|
+
from . import BUILTINS
|
|
229
|
+
from ...commands import COMMAND_NAMES
|
|
230
|
+
|
|
231
|
+
if cmd_name in BUILTINS:
|
|
232
|
+
if verbose:
|
|
233
|
+
return _result(f"{cmd_name} is a shell builtin\n", "", 0)
|
|
234
|
+
else:
|
|
235
|
+
return _result(f"{cmd_name}\n", "", 0)
|
|
236
|
+
elif cmd_name in COMMAND_NAMES:
|
|
237
|
+
if verbose:
|
|
238
|
+
return _result(f"{cmd_name} is {cmd_name}\n", "", 0)
|
|
239
|
+
else:
|
|
240
|
+
return _result(f"{cmd_name}\n", "", 0)
|
|
241
|
+
else:
|
|
242
|
+
return _result("", f"bash: command: {cmd_name}: not found\n", 1)
|
|
243
|
+
|
|
244
|
+
# Execute the command, bypassing functions
|
|
245
|
+
# Store current function state and temporarily hide the function
|
|
246
|
+
functions = getattr(ctx.state, 'functions', {})
|
|
247
|
+
hidden_func = functions.pop(cmd_name, None)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
# Build command string with proper quoting
|
|
251
|
+
def shell_quote(s: str) -> str:
|
|
252
|
+
if not s or any(c in s for c in ' \t\n\'"\\$`!'):
|
|
253
|
+
return "'" + s.replace("'", "'\\''") + "'"
|
|
254
|
+
return s
|
|
255
|
+
|
|
256
|
+
cmd_str = " ".join(shell_quote(a) for a in cmd_args)
|
|
257
|
+
result = await ctx.exec_fn(cmd_str, None, None)
|
|
258
|
+
return result
|
|
259
|
+
finally:
|
|
260
|
+
# Restore function if it was hidden
|
|
261
|
+
if hidden_func is not None:
|
|
262
|
+
functions[cmd_name] = hidden_func
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
async def handle_builtin(
|
|
266
|
+
ctx: "InterpreterContext", args: list[str]
|
|
267
|
+
) -> "ExecResult":
|
|
268
|
+
"""Execute the builtin builtin - run shell builtin directly.
|
|
269
|
+
|
|
270
|
+
Usage: builtin [shell-builtin [args]]
|
|
271
|
+
"""
|
|
272
|
+
if not args:
|
|
273
|
+
return _result("", "", 0)
|
|
274
|
+
|
|
275
|
+
builtin_name = args[0]
|
|
276
|
+
builtin_args = args[1:]
|
|
277
|
+
|
|
278
|
+
from . import BUILTINS
|
|
279
|
+
|
|
280
|
+
if builtin_name not in BUILTINS:
|
|
281
|
+
return _result("", f"bash: builtin: {builtin_name}: not a shell builtin\n", 1)
|
|
282
|
+
|
|
283
|
+
handler = BUILTINS[builtin_name]
|
|
284
|
+
return await handler(ctx, builtin_args)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def handle_exec(
|
|
288
|
+
ctx: "InterpreterContext", args: list[str]
|
|
289
|
+
) -> "ExecResult":
|
|
290
|
+
"""Execute the exec builtin - replace shell with command.
|
|
291
|
+
|
|
292
|
+
Usage: exec [-cl] [-a name] [command [arguments ...]]
|
|
293
|
+
|
|
294
|
+
In a sandboxed environment, this just executes the command normally
|
|
295
|
+
since we can't actually replace the process.
|
|
296
|
+
|
|
297
|
+
Options:
|
|
298
|
+
-c Execute command with empty environment
|
|
299
|
+
-l Pass dash as zeroth argument (login shell)
|
|
300
|
+
-a name Pass name as zeroth argument
|
|
301
|
+
"""
|
|
302
|
+
# Parse options
|
|
303
|
+
clear_env = False
|
|
304
|
+
login_shell = False
|
|
305
|
+
arg0_name = None
|
|
306
|
+
cmd_args = []
|
|
307
|
+
|
|
308
|
+
i = 0
|
|
309
|
+
while i < len(args):
|
|
310
|
+
arg = args[i]
|
|
311
|
+
if arg == "--":
|
|
312
|
+
cmd_args = args[i + 1:]
|
|
313
|
+
break
|
|
314
|
+
elif arg == "-c" and not cmd_args:
|
|
315
|
+
clear_env = True
|
|
316
|
+
elif arg == "-l" and not cmd_args:
|
|
317
|
+
login_shell = True
|
|
318
|
+
elif arg == "-a" and not cmd_args and i + 1 < len(args):
|
|
319
|
+
i += 1
|
|
320
|
+
arg0_name = args[i]
|
|
321
|
+
elif arg.startswith("-") and not cmd_args:
|
|
322
|
+
# Combined options
|
|
323
|
+
for c in arg[1:]:
|
|
324
|
+
if c == "c":
|
|
325
|
+
clear_env = True
|
|
326
|
+
elif c == "l":
|
|
327
|
+
login_shell = True
|
|
328
|
+
else:
|
|
329
|
+
return _result("", f"bash: exec: -{c}: invalid option\n", 1)
|
|
330
|
+
else:
|
|
331
|
+
cmd_args = args[i:]
|
|
332
|
+
break
|
|
333
|
+
i += 1
|
|
334
|
+
|
|
335
|
+
# If no command, exec just affects redirections (which we don't handle here)
|
|
336
|
+
if not cmd_args:
|
|
337
|
+
return _result("", "", 0)
|
|
338
|
+
|
|
339
|
+
# In sandboxed mode, just execute the command
|
|
340
|
+
def shell_quote(s: str) -> str:
|
|
341
|
+
if not s or any(c in s for c in ' \t\n\'"\\$`!'):
|
|
342
|
+
return "'" + s.replace("'", "'\\''") + "'"
|
|
343
|
+
return s
|
|
344
|
+
|
|
345
|
+
cmd_str = " ".join(shell_quote(a) for a in cmd_args)
|
|
346
|
+
result = await ctx.exec_fn(cmd_str, None, None)
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def handle_wait(
|
|
351
|
+
ctx: "InterpreterContext", args: list[str]
|
|
352
|
+
) -> "ExecResult":
|
|
353
|
+
"""Execute the wait builtin - wait for background jobs.
|
|
354
|
+
|
|
355
|
+
Usage: wait [-fn] [-p var] [id ...]
|
|
356
|
+
|
|
357
|
+
In a sandboxed environment without true background jobs,
|
|
358
|
+
this is mostly a no-op but returns success.
|
|
359
|
+
|
|
360
|
+
Options:
|
|
361
|
+
-f Wait for job termination (not just state change)
|
|
362
|
+
-n Wait for any job to complete
|
|
363
|
+
-p var Store PID in var
|
|
364
|
+
"""
|
|
365
|
+
# Parse options
|
|
366
|
+
i = 0
|
|
367
|
+
while i < len(args):
|
|
368
|
+
arg = args[i]
|
|
369
|
+
if arg == "--":
|
|
370
|
+
break
|
|
371
|
+
elif arg.startswith("-"):
|
|
372
|
+
# Accept but ignore options since we don't have real job control
|
|
373
|
+
if arg == "-p" and i + 1 < len(args):
|
|
374
|
+
i += 1
|
|
375
|
+
i += 1
|
|
376
|
+
|
|
377
|
+
# No real job control in sandboxed environment, return success
|
|
378
|
+
return _result("", "", 0)
|