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,154 @@
|
|
|
1
|
+
"""Alias builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: alias [-p] [name[=value] ...]
|
|
4
|
+
|
|
5
|
+
Define or display aliases.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-p Print all aliases in reusable format
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..types import InterpreterContext
|
|
15
|
+
from ...types import ExecResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
19
|
+
"""Create an ExecResult."""
|
|
20
|
+
from ...types import ExecResult
|
|
21
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_aliases(ctx: "InterpreterContext") -> dict[str, str]:
|
|
25
|
+
"""Get the aliases dictionary from context."""
|
|
26
|
+
# Store aliases in a special env variable as a serialized format
|
|
27
|
+
# Or use a dedicated attribute on state
|
|
28
|
+
if not hasattr(ctx.state, '_aliases'):
|
|
29
|
+
ctx.state._aliases = {}
|
|
30
|
+
return ctx.state._aliases
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_alias(ctx: "InterpreterContext", name: str, value: str) -> None:
|
|
34
|
+
"""Set an alias."""
|
|
35
|
+
aliases = get_aliases(ctx)
|
|
36
|
+
aliases[name] = value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def unset_alias(ctx: "InterpreterContext", name: str) -> bool:
|
|
40
|
+
"""Unset an alias. Returns True if it existed."""
|
|
41
|
+
aliases = get_aliases(ctx)
|
|
42
|
+
if name in aliases:
|
|
43
|
+
del aliases[name]
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def handle_alias(
|
|
49
|
+
ctx: "InterpreterContext", args: list[str]
|
|
50
|
+
) -> "ExecResult":
|
|
51
|
+
"""Execute the alias builtin."""
|
|
52
|
+
# Parse options
|
|
53
|
+
print_format = False
|
|
54
|
+
names = []
|
|
55
|
+
|
|
56
|
+
i = 0
|
|
57
|
+
while i < len(args):
|
|
58
|
+
arg = args[i]
|
|
59
|
+
if arg == "-p":
|
|
60
|
+
print_format = True
|
|
61
|
+
elif arg == "--":
|
|
62
|
+
names.extend(args[i + 1:])
|
|
63
|
+
break
|
|
64
|
+
elif arg.startswith("-"):
|
|
65
|
+
return _result("", f"bash: alias: {arg}: invalid option\n", 1)
|
|
66
|
+
else:
|
|
67
|
+
names.append(arg)
|
|
68
|
+
i += 1
|
|
69
|
+
|
|
70
|
+
aliases = get_aliases(ctx)
|
|
71
|
+
|
|
72
|
+
# No arguments: show all aliases
|
|
73
|
+
if not names:
|
|
74
|
+
output = []
|
|
75
|
+
for name in sorted(aliases.keys()):
|
|
76
|
+
value = aliases[name]
|
|
77
|
+
output.append(f"alias {name}='{value}'")
|
|
78
|
+
if output:
|
|
79
|
+
return _result("\n".join(output) + "\n", "", 0)
|
|
80
|
+
return _result("", "", 0)
|
|
81
|
+
|
|
82
|
+
# Process arguments
|
|
83
|
+
output = []
|
|
84
|
+
exit_code = 0
|
|
85
|
+
|
|
86
|
+
for arg in names:
|
|
87
|
+
if "=" in arg:
|
|
88
|
+
# Define alias: name=value
|
|
89
|
+
eq_pos = arg.index("=")
|
|
90
|
+
name = arg[:eq_pos]
|
|
91
|
+
value = arg[eq_pos + 1:]
|
|
92
|
+
set_alias(ctx, name, value)
|
|
93
|
+
else:
|
|
94
|
+
# Show alias
|
|
95
|
+
name = arg
|
|
96
|
+
if name in aliases:
|
|
97
|
+
value = aliases[name]
|
|
98
|
+
output.append(f"alias {name}='{value}'")
|
|
99
|
+
else:
|
|
100
|
+
output.append(f"bash: alias: {name}: not found")
|
|
101
|
+
exit_code = 1
|
|
102
|
+
|
|
103
|
+
if output:
|
|
104
|
+
return _result("\n".join(output) + "\n", "", exit_code)
|
|
105
|
+
return _result("", "", exit_code)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def handle_unalias(
|
|
109
|
+
ctx: "InterpreterContext", args: list[str]
|
|
110
|
+
) -> "ExecResult":
|
|
111
|
+
"""Execute the unalias builtin.
|
|
112
|
+
|
|
113
|
+
Usage: unalias [-a] name [name ...]
|
|
114
|
+
|
|
115
|
+
Options:
|
|
116
|
+
-a Remove all aliases
|
|
117
|
+
"""
|
|
118
|
+
# Parse options
|
|
119
|
+
remove_all = False
|
|
120
|
+
names = []
|
|
121
|
+
|
|
122
|
+
i = 0
|
|
123
|
+
while i < len(args):
|
|
124
|
+
arg = args[i]
|
|
125
|
+
if arg == "-a":
|
|
126
|
+
remove_all = True
|
|
127
|
+
elif arg == "--":
|
|
128
|
+
names.extend(args[i + 1:])
|
|
129
|
+
break
|
|
130
|
+
elif arg.startswith("-"):
|
|
131
|
+
return _result("", f"bash: unalias: {arg}: invalid option\n", 1)
|
|
132
|
+
else:
|
|
133
|
+
names.append(arg)
|
|
134
|
+
i += 1
|
|
135
|
+
|
|
136
|
+
aliases = get_aliases(ctx)
|
|
137
|
+
|
|
138
|
+
if remove_all:
|
|
139
|
+
aliases.clear()
|
|
140
|
+
return _result("", "", 0)
|
|
141
|
+
|
|
142
|
+
if not names:
|
|
143
|
+
return _result("", "bash: unalias: usage: unalias [-a] name [name ...]\n", 1)
|
|
144
|
+
|
|
145
|
+
exit_code = 0
|
|
146
|
+
stderr_parts = []
|
|
147
|
+
|
|
148
|
+
for name in names:
|
|
149
|
+
if not unset_alias(ctx, name):
|
|
150
|
+
stderr_parts.append(f"bash: unalias: {name}: not found")
|
|
151
|
+
exit_code = 1
|
|
152
|
+
|
|
153
|
+
stderr = "\n".join(stderr_parts) + "\n" if stderr_parts else ""
|
|
154
|
+
return _result("", stderr, exit_code)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Cd builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: cd [dir]
|
|
4
|
+
cd -
|
|
5
|
+
|
|
6
|
+
Change the current working directory to dir. If dir is not specified,
|
|
7
|
+
change to $HOME. If dir is -, change to $OLDPWD.
|
|
8
|
+
"""
|
|
9
|
+
|
|
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_cd(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
18
|
+
"""Execute the cd builtin."""
|
|
19
|
+
from ...types import ExecResult
|
|
20
|
+
|
|
21
|
+
# Determine target directory
|
|
22
|
+
if not args:
|
|
23
|
+
# cd with no args goes to HOME
|
|
24
|
+
target = ctx.state.env.get("HOME", "/")
|
|
25
|
+
elif args[0] == "-":
|
|
26
|
+
# cd - goes to previous directory
|
|
27
|
+
target = ctx.state.previous_dir
|
|
28
|
+
if not target:
|
|
29
|
+
return ExecResult(
|
|
30
|
+
stdout="",
|
|
31
|
+
stderr="bash: cd: OLDPWD not set\n",
|
|
32
|
+
exit_code=1,
|
|
33
|
+
)
|
|
34
|
+
else:
|
|
35
|
+
target = args[0]
|
|
36
|
+
|
|
37
|
+
# Resolve the path
|
|
38
|
+
new_dir = ctx.fs.resolve_path(ctx.state.cwd, target)
|
|
39
|
+
|
|
40
|
+
# Verify directory exists
|
|
41
|
+
try:
|
|
42
|
+
exists = await ctx.fs.exists(new_dir)
|
|
43
|
+
if not exists:
|
|
44
|
+
return ExecResult(
|
|
45
|
+
stdout="",
|
|
46
|
+
stderr=f"bash: cd: {target}: No such file or directory\n",
|
|
47
|
+
exit_code=1,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
is_dir = await ctx.fs.is_directory(new_dir)
|
|
51
|
+
if not is_dir:
|
|
52
|
+
return ExecResult(
|
|
53
|
+
stdout="",
|
|
54
|
+
stderr=f"bash: cd: {target}: Not a directory\n",
|
|
55
|
+
exit_code=1,
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
return ExecResult(
|
|
59
|
+
stdout="",
|
|
60
|
+
stderr=f"bash: cd: {target}: {e}\n",
|
|
61
|
+
exit_code=1,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Update state
|
|
65
|
+
old_dir = ctx.state.cwd
|
|
66
|
+
ctx.state.previous_dir = old_dir
|
|
67
|
+
ctx.state.cwd = new_dir
|
|
68
|
+
ctx.state.env["OLDPWD"] = old_dir
|
|
69
|
+
ctx.state.env["PWD"] = new_dir
|
|
70
|
+
|
|
71
|
+
# If cd - was used, print the new directory
|
|
72
|
+
stdout = ""
|
|
73
|
+
if args and args[0] == "-":
|
|
74
|
+
stdout = new_dir + "\n"
|
|
75
|
+
|
|
76
|
+
return ExecResult(stdout=stdout, stderr="", exit_code=0)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Control flow builtins: break, continue, return, exit.
|
|
2
|
+
|
|
3
|
+
These builtins control the flow of script execution.
|
|
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
|
+
from ..errors import BreakError, ContinueError, ReturnError, ExitError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def handle_break(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
16
|
+
"""Execute the break builtin.
|
|
17
|
+
|
|
18
|
+
Usage: break [n]
|
|
19
|
+
|
|
20
|
+
Exit from within a for, while, until, or select loop.
|
|
21
|
+
If n is specified, break out of n enclosing loops.
|
|
22
|
+
"""
|
|
23
|
+
levels = 1
|
|
24
|
+
if args:
|
|
25
|
+
try:
|
|
26
|
+
levels = int(args[0])
|
|
27
|
+
if levels < 1:
|
|
28
|
+
levels = 1
|
|
29
|
+
except ValueError:
|
|
30
|
+
from ...types import ExecResult
|
|
31
|
+
return ExecResult(
|
|
32
|
+
stdout="",
|
|
33
|
+
stderr=f"bash: break: {args[0]}: numeric argument required\n",
|
|
34
|
+
exit_code=1,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Check if we're in a loop
|
|
38
|
+
if ctx.state.loop_depth < 1:
|
|
39
|
+
from ...types import ExecResult
|
|
40
|
+
return ExecResult(
|
|
41
|
+
stdout="",
|
|
42
|
+
stderr="", # bash doesn't print error for break outside loop
|
|
43
|
+
exit_code=0,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
raise BreakError(levels=levels)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def handle_continue(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
50
|
+
"""Execute the continue builtin.
|
|
51
|
+
|
|
52
|
+
Usage: continue [n]
|
|
53
|
+
|
|
54
|
+
Resume the next iteration of an enclosing for, while, until, or select loop.
|
|
55
|
+
If n is specified, resume at the nth enclosing loop.
|
|
56
|
+
"""
|
|
57
|
+
levels = 1
|
|
58
|
+
if args:
|
|
59
|
+
try:
|
|
60
|
+
levels = int(args[0])
|
|
61
|
+
if levels < 1:
|
|
62
|
+
levels = 1
|
|
63
|
+
except ValueError:
|
|
64
|
+
from ...types import ExecResult
|
|
65
|
+
return ExecResult(
|
|
66
|
+
stdout="",
|
|
67
|
+
stderr=f"bash: continue: {args[0]}: numeric argument required\n",
|
|
68
|
+
exit_code=1,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Check if we're in a loop
|
|
72
|
+
if ctx.state.loop_depth < 1:
|
|
73
|
+
from ...types import ExecResult
|
|
74
|
+
return ExecResult(
|
|
75
|
+
stdout="",
|
|
76
|
+
stderr="", # bash doesn't print error for continue outside loop
|
|
77
|
+
exit_code=0,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
raise ContinueError(levels=levels)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def handle_return(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
84
|
+
"""Execute the return builtin.
|
|
85
|
+
|
|
86
|
+
Usage: return [n]
|
|
87
|
+
|
|
88
|
+
Return from a shell function or sourced script.
|
|
89
|
+
n is the return value (0-255). If n is omitted, the return value is
|
|
90
|
+
the exit status of the last command executed.
|
|
91
|
+
"""
|
|
92
|
+
exit_code = ctx.state.last_exit_code
|
|
93
|
+
if args:
|
|
94
|
+
try:
|
|
95
|
+
exit_code = int(args[0]) & 255 # Mask to 0-255
|
|
96
|
+
except ValueError:
|
|
97
|
+
from ...types import ExecResult
|
|
98
|
+
return ExecResult(
|
|
99
|
+
stdout="",
|
|
100
|
+
stderr=f"bash: return: {args[0]}: numeric argument required\n",
|
|
101
|
+
exit_code=1,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
raise ReturnError(exit_code)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def handle_exit(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
108
|
+
"""Execute the exit builtin.
|
|
109
|
+
|
|
110
|
+
Usage: exit [n]
|
|
111
|
+
|
|
112
|
+
Exit the shell with status n. If n is omitted, the exit status is
|
|
113
|
+
that of the last command executed.
|
|
114
|
+
"""
|
|
115
|
+
exit_code = ctx.state.last_exit_code
|
|
116
|
+
if args:
|
|
117
|
+
try:
|
|
118
|
+
exit_code = int(args[0]) & 255 # Mask to 0-255
|
|
119
|
+
except ValueError:
|
|
120
|
+
from ...types import ExecResult
|
|
121
|
+
return ExecResult(
|
|
122
|
+
stdout="",
|
|
123
|
+
stderr=f"bash: exit: {args[0]}: numeric argument required\n",
|
|
124
|
+
exit_code=1,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
raise ExitError(exit_code)
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Declare/typeset builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: declare [-aAfFgiIlnrtux] [-p] [name[=value] ...]
|
|
4
|
+
typeset [-aAfFgiIlnrtux] [-p] [name[=value] ...]
|
|
5
|
+
|
|
6
|
+
Options:
|
|
7
|
+
-a indexed array
|
|
8
|
+
-A associative array
|
|
9
|
+
-f functions only
|
|
10
|
+
-F function names only
|
|
11
|
+
-g global scope (in function context)
|
|
12
|
+
-i integer attribute
|
|
13
|
+
-l lowercase
|
|
14
|
+
-n nameref
|
|
15
|
+
-p print declarations
|
|
16
|
+
-r readonly
|
|
17
|
+
-t trace (functions)
|
|
18
|
+
-u uppercase
|
|
19
|
+
-x export
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import TYPE_CHECKING, Optional
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..types import InterpreterContext
|
|
27
|
+
from ...types import ExecResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
31
|
+
"""Create an ExecResult."""
|
|
32
|
+
from ...types import ExecResult
|
|
33
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
37
|
+
"""Execute the declare/typeset builtin."""
|
|
38
|
+
# Parse options
|
|
39
|
+
options = {
|
|
40
|
+
"array": False, # -a: indexed array
|
|
41
|
+
"assoc": False, # -A: associative array
|
|
42
|
+
"function": False, # -f: functions
|
|
43
|
+
"func_names": False, # -F: function names only
|
|
44
|
+
"global": False, # -g: global scope
|
|
45
|
+
"integer": False, # -i: integer
|
|
46
|
+
"lowercase": False, # -l: lowercase
|
|
47
|
+
"nameref": False, # -n: nameref
|
|
48
|
+
"print": False, # -p: print declarations
|
|
49
|
+
"readonly": False, # -r: readonly
|
|
50
|
+
"trace": False, # -t: trace
|
|
51
|
+
"uppercase": False, # -u: uppercase
|
|
52
|
+
"export": False, # -x: export
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
names: list[str] = []
|
|
56
|
+
|
|
57
|
+
i = 0
|
|
58
|
+
while i < len(args):
|
|
59
|
+
arg = args[i]
|
|
60
|
+
|
|
61
|
+
if arg == "--":
|
|
62
|
+
names.extend(args[i + 1:])
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if arg.startswith("-") and len(arg) > 1 and arg[1] != "-":
|
|
66
|
+
# Parse short options
|
|
67
|
+
for c in arg[1:]:
|
|
68
|
+
if c == "a":
|
|
69
|
+
options["array"] = True
|
|
70
|
+
elif c == "A":
|
|
71
|
+
options["assoc"] = True
|
|
72
|
+
elif c == "f":
|
|
73
|
+
options["function"] = True
|
|
74
|
+
elif c == "F":
|
|
75
|
+
options["func_names"] = True
|
|
76
|
+
elif c == "g":
|
|
77
|
+
options["global"] = True
|
|
78
|
+
elif c == "i":
|
|
79
|
+
options["integer"] = True
|
|
80
|
+
elif c == "l":
|
|
81
|
+
options["lowercase"] = True
|
|
82
|
+
elif c == "n":
|
|
83
|
+
options["nameref"] = True
|
|
84
|
+
elif c == "p":
|
|
85
|
+
options["print"] = True
|
|
86
|
+
elif c == "r":
|
|
87
|
+
options["readonly"] = True
|
|
88
|
+
elif c == "t":
|
|
89
|
+
options["trace"] = True
|
|
90
|
+
elif c == "u":
|
|
91
|
+
options["uppercase"] = True
|
|
92
|
+
elif c == "x":
|
|
93
|
+
options["export"] = True
|
|
94
|
+
else:
|
|
95
|
+
return _result(
|
|
96
|
+
"",
|
|
97
|
+
f"bash: declare: -{c}: invalid option\n",
|
|
98
|
+
2
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
names.append(arg)
|
|
102
|
+
|
|
103
|
+
i += 1
|
|
104
|
+
|
|
105
|
+
# Print mode: show variable declarations
|
|
106
|
+
if options["print"]:
|
|
107
|
+
return _print_declarations(ctx, names, options)
|
|
108
|
+
|
|
109
|
+
# No names: list variables with matching attributes
|
|
110
|
+
if not names:
|
|
111
|
+
return _list_variables(ctx, options)
|
|
112
|
+
|
|
113
|
+
# Process each name/assignment
|
|
114
|
+
exit_code = 0
|
|
115
|
+
stderr_parts = []
|
|
116
|
+
|
|
117
|
+
for name_arg in names:
|
|
118
|
+
# Parse name and optional value
|
|
119
|
+
if "=" in name_arg:
|
|
120
|
+
eq_idx = name_arg.index("=")
|
|
121
|
+
name = name_arg[:eq_idx]
|
|
122
|
+
value_str = name_arg[eq_idx + 1:]
|
|
123
|
+
else:
|
|
124
|
+
name = name_arg
|
|
125
|
+
value_str = None
|
|
126
|
+
|
|
127
|
+
# Handle array subscript in name: arr[idx]
|
|
128
|
+
subscript = None
|
|
129
|
+
if "[" in name and name.endswith("]"):
|
|
130
|
+
bracket_idx = name.index("[")
|
|
131
|
+
subscript = name[bracket_idx + 1:-1]
|
|
132
|
+
name = name[:bracket_idx]
|
|
133
|
+
|
|
134
|
+
# Validate identifier
|
|
135
|
+
if not re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", name):
|
|
136
|
+
stderr_parts.append(f"bash: declare: `{name_arg}': not a valid identifier\n")
|
|
137
|
+
exit_code = 1
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
# Handle array declaration
|
|
141
|
+
if options["array"] or options["assoc"]:
|
|
142
|
+
# Initialize array if not already set
|
|
143
|
+
array_key = f"{name}__is_array"
|
|
144
|
+
if array_key not in ctx.state.env:
|
|
145
|
+
ctx.state.env[array_key] = "assoc" if options["assoc"] else "indexed"
|
|
146
|
+
|
|
147
|
+
if value_str is not None:
|
|
148
|
+
# Parse array assignment: (a b c) or ([0]=a [1]=b)
|
|
149
|
+
if value_str.startswith("(") and value_str.endswith(")"):
|
|
150
|
+
inner = value_str[1:-1].strip()
|
|
151
|
+
_parse_array_assignment(ctx, name, inner, options["assoc"])
|
|
152
|
+
elif subscript is not None:
|
|
153
|
+
# arr[idx]=value
|
|
154
|
+
ctx.state.env[f"{name}_{subscript}"] = value_str
|
|
155
|
+
else:
|
|
156
|
+
# Simple value assignment to array[0]
|
|
157
|
+
ctx.state.env[f"{name}_0"] = value_str
|
|
158
|
+
else:
|
|
159
|
+
# Regular variable
|
|
160
|
+
if value_str is not None:
|
|
161
|
+
# Apply transformations
|
|
162
|
+
if options["integer"]:
|
|
163
|
+
# Evaluate as integer
|
|
164
|
+
try:
|
|
165
|
+
value_str = str(_eval_integer(value_str, ctx))
|
|
166
|
+
except Exception:
|
|
167
|
+
value_str = "0"
|
|
168
|
+
|
|
169
|
+
if options["lowercase"]:
|
|
170
|
+
value_str = value_str.lower()
|
|
171
|
+
elif options["uppercase"]:
|
|
172
|
+
value_str = value_str.upper()
|
|
173
|
+
|
|
174
|
+
if subscript is not None:
|
|
175
|
+
# Array element
|
|
176
|
+
ctx.state.env[f"{name}_{subscript}"] = value_str
|
|
177
|
+
else:
|
|
178
|
+
ctx.state.env[name] = value_str
|
|
179
|
+
elif name not in ctx.state.env:
|
|
180
|
+
# Declare without value - just set type info
|
|
181
|
+
if options["integer"]:
|
|
182
|
+
ctx.state.env[f"{name}__is_integer"] = "1"
|
|
183
|
+
if options["lowercase"]:
|
|
184
|
+
ctx.state.env[f"{name}__is_lower"] = "1"
|
|
185
|
+
if options["uppercase"]:
|
|
186
|
+
ctx.state.env[f"{name}__is_upper"] = "1"
|
|
187
|
+
|
|
188
|
+
return _result("", "".join(stderr_parts), exit_code)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _parse_array_assignment(ctx: "InterpreterContext", name: str, inner: str, is_assoc: bool) -> None:
|
|
192
|
+
"""Parse and assign array values from (a b c) or ([key]=value ...) syntax."""
|
|
193
|
+
# Clear existing array elements
|
|
194
|
+
to_remove = [k for k in ctx.state.env if k.startswith(f"{name}_") and not k.startswith(f"{name}__")]
|
|
195
|
+
for k in to_remove:
|
|
196
|
+
del ctx.state.env[k]
|
|
197
|
+
|
|
198
|
+
# Simple word splitting for now - doesn't handle all quoting cases
|
|
199
|
+
idx = 0
|
|
200
|
+
i = 0
|
|
201
|
+
|
|
202
|
+
while i < len(inner):
|
|
203
|
+
# Skip whitespace
|
|
204
|
+
while i < len(inner) and inner[i] in " \t":
|
|
205
|
+
i += 1
|
|
206
|
+
|
|
207
|
+
if i >= len(inner):
|
|
208
|
+
break
|
|
209
|
+
|
|
210
|
+
# Check for [key]=value syntax
|
|
211
|
+
if inner[i] == "[":
|
|
212
|
+
# Find closing bracket
|
|
213
|
+
j = i + 1
|
|
214
|
+
while j < len(inner) and inner[j] != "]":
|
|
215
|
+
j += 1
|
|
216
|
+
if j < len(inner) and j + 1 < len(inner) and inner[j + 1] == "=":
|
|
217
|
+
key = inner[i + 1:j]
|
|
218
|
+
# Find value
|
|
219
|
+
value_start = j + 2
|
|
220
|
+
value_end = value_start
|
|
221
|
+
in_quote = None
|
|
222
|
+
while value_end < len(inner):
|
|
223
|
+
c = inner[value_end]
|
|
224
|
+
if in_quote:
|
|
225
|
+
if c == in_quote:
|
|
226
|
+
in_quote = None
|
|
227
|
+
value_end += 1
|
|
228
|
+
elif c in "\"'":
|
|
229
|
+
in_quote = c
|
|
230
|
+
value_end += 1
|
|
231
|
+
elif c in " \t":
|
|
232
|
+
break
|
|
233
|
+
else:
|
|
234
|
+
value_end += 1
|
|
235
|
+
|
|
236
|
+
value = inner[value_start:value_end]
|
|
237
|
+
# Remove surrounding quotes if present
|
|
238
|
+
if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
|
|
239
|
+
value = value[1:-1]
|
|
240
|
+
|
|
241
|
+
ctx.state.env[f"{name}_{key}"] = value
|
|
242
|
+
i = value_end
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Simple value - assign to next index
|
|
246
|
+
value_start = i
|
|
247
|
+
value_end = i
|
|
248
|
+
in_quote = None
|
|
249
|
+
while value_end < len(inner):
|
|
250
|
+
c = inner[value_end]
|
|
251
|
+
if in_quote:
|
|
252
|
+
if c == in_quote:
|
|
253
|
+
in_quote = None
|
|
254
|
+
value_end += 1
|
|
255
|
+
elif c in "\"'":
|
|
256
|
+
in_quote = c
|
|
257
|
+
value_end += 1
|
|
258
|
+
elif c in " \t":
|
|
259
|
+
break
|
|
260
|
+
else:
|
|
261
|
+
value_end += 1
|
|
262
|
+
|
|
263
|
+
value = inner[value_start:value_end]
|
|
264
|
+
# Remove surrounding quotes if present
|
|
265
|
+
if len(value) >= 2 and value[0] in "\"'" and value[-1] == value[0]:
|
|
266
|
+
value = value[1:-1]
|
|
267
|
+
|
|
268
|
+
ctx.state.env[f"{name}_{idx}"] = value
|
|
269
|
+
idx += 1
|
|
270
|
+
i = value_end
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _eval_integer(expr: str, ctx: "InterpreterContext") -> int:
|
|
274
|
+
"""Evaluate a simple integer expression."""
|
|
275
|
+
# Handle variable references
|
|
276
|
+
expr = expr.strip()
|
|
277
|
+
|
|
278
|
+
# Try direct integer
|
|
279
|
+
try:
|
|
280
|
+
return int(expr)
|
|
281
|
+
except ValueError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
# Try variable reference
|
|
285
|
+
if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", expr):
|
|
286
|
+
val = ctx.state.env.get(expr, "0")
|
|
287
|
+
try:
|
|
288
|
+
return int(val)
|
|
289
|
+
except ValueError:
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
return 0
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _print_declarations(ctx: "InterpreterContext", names: list[str], options: dict) -> "ExecResult":
|
|
296
|
+
"""Print variable declarations."""
|
|
297
|
+
lines = []
|
|
298
|
+
|
|
299
|
+
if not names:
|
|
300
|
+
# Print all matching variables
|
|
301
|
+
for name in sorted(ctx.state.env.keys()):
|
|
302
|
+
if name.startswith("_") or "__" in name:
|
|
303
|
+
continue
|
|
304
|
+
if name in ("?", "#", "$", "!", "-", "*", "@"):
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
val = ctx.state.env[name]
|
|
308
|
+
lines.append(f'declare -- {name}="{val}"')
|
|
309
|
+
else:
|
|
310
|
+
for name in names:
|
|
311
|
+
if name in ctx.state.env:
|
|
312
|
+
val = ctx.state.env[name]
|
|
313
|
+
lines.append(f'declare -- {name}="{val}"')
|
|
314
|
+
else:
|
|
315
|
+
# Check if it's an array
|
|
316
|
+
is_array = ctx.state.env.get(f"{name}__is_array")
|
|
317
|
+
if is_array:
|
|
318
|
+
lines.append(f'declare -{("A" if is_array == "assoc" else "a")} {name}')
|
|
319
|
+
|
|
320
|
+
return _result("\n".join(lines) + "\n" if lines else "", "", 0)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _list_variables(ctx: "InterpreterContext", options: dict) -> "ExecResult":
|
|
324
|
+
"""List variables with matching attributes."""
|
|
325
|
+
lines = []
|
|
326
|
+
|
|
327
|
+
for name in sorted(ctx.state.env.keys()):
|
|
328
|
+
if name.startswith("_") or "__" in name:
|
|
329
|
+
continue
|
|
330
|
+
if name in ("?", "#", "$", "!", "-", "*", "@"):
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
val = ctx.state.env[name]
|
|
334
|
+
lines.append(f'declare -- {name}="{val}"')
|
|
335
|
+
|
|
336
|
+
return _result("\n".join(lines) + "\n" if lines else "", "", 0)
|