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,80 @@
|
|
|
1
|
+
"""Readonly builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: readonly [-p] [name[=value] ...]
|
|
4
|
+
|
|
5
|
+
Marks variables as readonly. Once a variable is marked readonly, it cannot
|
|
6
|
+
be reassigned or unset.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-p Display all readonly variables
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..types import InterpreterContext
|
|
16
|
+
from ...types import ExecResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
20
|
+
"""Create an ExecResult."""
|
|
21
|
+
from ...types import ExecResult
|
|
22
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def handle_readonly(
|
|
26
|
+
ctx: "InterpreterContext", args: list[str]
|
|
27
|
+
) -> "ExecResult":
|
|
28
|
+
"""Execute the readonly builtin."""
|
|
29
|
+
# Parse options
|
|
30
|
+
show_all = False
|
|
31
|
+
names = []
|
|
32
|
+
|
|
33
|
+
i = 0
|
|
34
|
+
while i < len(args):
|
|
35
|
+
arg = args[i]
|
|
36
|
+
if arg == "-p":
|
|
37
|
+
show_all = True
|
|
38
|
+
elif arg == "--":
|
|
39
|
+
names.extend(args[i + 1:])
|
|
40
|
+
break
|
|
41
|
+
elif arg.startswith("-"):
|
|
42
|
+
# Unknown option - ignore for now
|
|
43
|
+
pass
|
|
44
|
+
else:
|
|
45
|
+
names.append(arg)
|
|
46
|
+
i += 1
|
|
47
|
+
|
|
48
|
+
# If no names and -p or no args, show all readonly variables
|
|
49
|
+
if not names or show_all:
|
|
50
|
+
output = []
|
|
51
|
+
readonly_vars = ctx.state.env.get("__readonly__", "").split()
|
|
52
|
+
for var in sorted(readonly_vars):
|
|
53
|
+
if var in ctx.state.env:
|
|
54
|
+
value = ctx.state.env[var]
|
|
55
|
+
output.append(f"declare -r {var}=\"{value}\"")
|
|
56
|
+
else:
|
|
57
|
+
output.append(f"declare -r {var}")
|
|
58
|
+
if output:
|
|
59
|
+
return _result("\n".join(output) + "\n", "", 0)
|
|
60
|
+
return _result("", "", 0)
|
|
61
|
+
|
|
62
|
+
# Mark variables as readonly
|
|
63
|
+
readonly_set = set(ctx.state.env.get("__readonly__", "").split())
|
|
64
|
+
|
|
65
|
+
for name_value in names:
|
|
66
|
+
if "=" in name_value:
|
|
67
|
+
name, value = name_value.split("=", 1)
|
|
68
|
+
# Check if already readonly
|
|
69
|
+
if name in readonly_set:
|
|
70
|
+
return _result("", f"bash: readonly: {name}: readonly variable\n", 1)
|
|
71
|
+
ctx.state.env[name] = value
|
|
72
|
+
else:
|
|
73
|
+
name = name_value
|
|
74
|
+
|
|
75
|
+
readonly_set.add(name)
|
|
76
|
+
|
|
77
|
+
# Store readonly set
|
|
78
|
+
ctx.state.env["__readonly__"] = " ".join(sorted(readonly_set))
|
|
79
|
+
|
|
80
|
+
return _result("", "", 0)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Set and shift builtin implementations.
|
|
2
|
+
|
|
3
|
+
set - Set or unset shell options and positional parameters.
|
|
4
|
+
|
|
5
|
+
Usage: set [options] [-- arg ...]
|
|
6
|
+
set +o
|
|
7
|
+
set -o [option]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
-e errexit Exit immediately if a command exits with non-zero status
|
|
11
|
+
-u nounset Treat unset variables as an error when substituting
|
|
12
|
+
-x xtrace Print commands and their arguments as they are executed
|
|
13
|
+
-v verbose Print shell input lines as they are read
|
|
14
|
+
-o pipefail Return exit status of last failing command in pipeline
|
|
15
|
+
|
|
16
|
+
shift - Shift positional parameters.
|
|
17
|
+
|
|
18
|
+
Usage: shift [n]
|
|
19
|
+
|
|
20
|
+
Shift positional parameters to the left by n (default 1).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from ..types import InterpreterContext
|
|
27
|
+
from ...types import ExecResult
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def handle_set(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
31
|
+
"""Execute the set builtin."""
|
|
32
|
+
from ...types import ExecResult
|
|
33
|
+
|
|
34
|
+
# No arguments: print all variables
|
|
35
|
+
if not args:
|
|
36
|
+
lines = []
|
|
37
|
+
for k, v in sorted(ctx.state.env.items()):
|
|
38
|
+
# Skip internal variables
|
|
39
|
+
if k.startswith("PIPESTATUS_") or k == "?":
|
|
40
|
+
continue
|
|
41
|
+
lines.append(f"{k}='{v}'")
|
|
42
|
+
return ExecResult(stdout="\n".join(lines) + "\n", stderr="", exit_code=0)
|
|
43
|
+
|
|
44
|
+
i = 0
|
|
45
|
+
while i < len(args):
|
|
46
|
+
arg = args[i]
|
|
47
|
+
|
|
48
|
+
# Handle -- which starts positional parameters
|
|
49
|
+
if arg == "--":
|
|
50
|
+
# Set positional parameters from remaining args
|
|
51
|
+
new_params = args[i + 1:]
|
|
52
|
+
_set_positional_params(ctx, new_params)
|
|
53
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
54
|
+
|
|
55
|
+
# Handle -o option
|
|
56
|
+
if arg == "-o":
|
|
57
|
+
if i + 1 < len(args):
|
|
58
|
+
i += 1
|
|
59
|
+
opt_name = args[i]
|
|
60
|
+
result = _set_option(ctx, opt_name, True)
|
|
61
|
+
if result:
|
|
62
|
+
return result
|
|
63
|
+
else:
|
|
64
|
+
# List all options
|
|
65
|
+
stdout = _list_options(ctx)
|
|
66
|
+
return ExecResult(stdout=stdout, stderr="", exit_code=0)
|
|
67
|
+
|
|
68
|
+
# Handle +o option
|
|
69
|
+
elif arg == "+o":
|
|
70
|
+
if i + 1 < len(args):
|
|
71
|
+
i += 1
|
|
72
|
+
opt_name = args[i]
|
|
73
|
+
result = _set_option(ctx, opt_name, False)
|
|
74
|
+
if result:
|
|
75
|
+
return result
|
|
76
|
+
else:
|
|
77
|
+
# List all options in a format that can be re-input
|
|
78
|
+
stdout = _list_options_script(ctx)
|
|
79
|
+
return ExecResult(stdout=stdout, stderr="", exit_code=0)
|
|
80
|
+
|
|
81
|
+
# Handle short options like -e, -u, -x, -v
|
|
82
|
+
elif arg.startswith("-") and len(arg) > 1 and arg[1] != "-":
|
|
83
|
+
for c in arg[1:]:
|
|
84
|
+
result = _set_short_option(ctx, c, True)
|
|
85
|
+
if result:
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
elif arg.startswith("+") and len(arg) > 1:
|
|
89
|
+
for c in arg[1:]:
|
|
90
|
+
result = _set_short_option(ctx, c, False)
|
|
91
|
+
if result:
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
# Treat as positional parameter
|
|
95
|
+
else:
|
|
96
|
+
new_params = args[i:]
|
|
97
|
+
_set_positional_params(ctx, new_params)
|
|
98
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
99
|
+
|
|
100
|
+
i += 1
|
|
101
|
+
|
|
102
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _set_positional_params(ctx: "InterpreterContext", params: list[str]) -> None:
|
|
106
|
+
"""Set positional parameters $1, $2, etc."""
|
|
107
|
+
# Clear existing positional parameters
|
|
108
|
+
i = 1
|
|
109
|
+
while str(i) in ctx.state.env:
|
|
110
|
+
del ctx.state.env[str(i)]
|
|
111
|
+
i += 1
|
|
112
|
+
|
|
113
|
+
# Set new positional parameters
|
|
114
|
+
for i, param in enumerate(params, start=1):
|
|
115
|
+
ctx.state.env[str(i)] = param
|
|
116
|
+
|
|
117
|
+
# Update $# (number of positional parameters)
|
|
118
|
+
ctx.state.env["#"] = str(len(params))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _set_option(ctx: "InterpreterContext", name: str, enable: bool) -> "ExecResult | None":
|
|
122
|
+
"""Set a named option. Returns error result if invalid option."""
|
|
123
|
+
from ...types import ExecResult
|
|
124
|
+
|
|
125
|
+
options = ctx.state.options
|
|
126
|
+
if name == "errexit":
|
|
127
|
+
options.errexit = enable
|
|
128
|
+
elif name == "nounset":
|
|
129
|
+
options.nounset = enable
|
|
130
|
+
elif name == "xtrace":
|
|
131
|
+
options.xtrace = enable
|
|
132
|
+
elif name == "verbose":
|
|
133
|
+
options.verbose = enable
|
|
134
|
+
elif name == "pipefail":
|
|
135
|
+
options.pipefail = enable
|
|
136
|
+
else:
|
|
137
|
+
return ExecResult(
|
|
138
|
+
stdout="",
|
|
139
|
+
stderr=f"bash: set: {name}: invalid option name\n",
|
|
140
|
+
exit_code=1,
|
|
141
|
+
)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _set_short_option(ctx: "InterpreterContext", char: str, enable: bool) -> "ExecResult | None":
|
|
146
|
+
"""Set a short option like -e. Returns error result if invalid."""
|
|
147
|
+
from ...types import ExecResult
|
|
148
|
+
|
|
149
|
+
options = ctx.state.options
|
|
150
|
+
if char == "e":
|
|
151
|
+
options.errexit = enable
|
|
152
|
+
elif char == "u":
|
|
153
|
+
options.nounset = enable
|
|
154
|
+
elif char == "x":
|
|
155
|
+
options.xtrace = enable
|
|
156
|
+
elif char == "v":
|
|
157
|
+
options.verbose = enable
|
|
158
|
+
else:
|
|
159
|
+
return ExecResult(
|
|
160
|
+
stdout="",
|
|
161
|
+
stderr=f"bash: set: -{char}: invalid option\n",
|
|
162
|
+
exit_code=1,
|
|
163
|
+
)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _list_options(ctx: "InterpreterContext") -> str:
|
|
168
|
+
"""List all options in human-readable format."""
|
|
169
|
+
options = ctx.state.options
|
|
170
|
+
lines = [
|
|
171
|
+
f"errexit {'on' if options.errexit else 'off'}",
|
|
172
|
+
f"nounset {'on' if options.nounset else 'off'}",
|
|
173
|
+
f"pipefail {'on' if options.pipefail else 'off'}",
|
|
174
|
+
f"verbose {'on' if options.verbose else 'off'}",
|
|
175
|
+
f"xtrace {'on' if options.xtrace else 'off'}",
|
|
176
|
+
]
|
|
177
|
+
return "\n".join(lines) + "\n"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _list_options_script(ctx: "InterpreterContext") -> str:
|
|
181
|
+
"""List options in re-inputable script format."""
|
|
182
|
+
options = ctx.state.options
|
|
183
|
+
lines = [
|
|
184
|
+
f"set {'-' if options.errexit else '+'}o errexit",
|
|
185
|
+
f"set {'-' if options.nounset else '+'}o nounset",
|
|
186
|
+
f"set {'-' if options.pipefail else '+'}o pipefail",
|
|
187
|
+
f"set {'-' if options.verbose else '+'}o verbose",
|
|
188
|
+
f"set {'-' if options.xtrace else '+'}o xtrace",
|
|
189
|
+
]
|
|
190
|
+
return "\n".join(lines) + "\n"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def handle_shift(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
194
|
+
"""Execute the shift builtin."""
|
|
195
|
+
from ...types import ExecResult
|
|
196
|
+
|
|
197
|
+
# Default shift count is 1
|
|
198
|
+
n = 1
|
|
199
|
+
if args:
|
|
200
|
+
try:
|
|
201
|
+
n = int(args[0])
|
|
202
|
+
except ValueError:
|
|
203
|
+
return ExecResult(
|
|
204
|
+
stdout="",
|
|
205
|
+
stderr=f"bash: shift: {args[0]}: numeric argument required\n",
|
|
206
|
+
exit_code=1,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if n < 0:
|
|
210
|
+
return ExecResult(
|
|
211
|
+
stdout="",
|
|
212
|
+
stderr=f"bash: shift: {n}: shift count out of range\n",
|
|
213
|
+
exit_code=1,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Get current positional parameters
|
|
217
|
+
param_count = int(ctx.state.env.get("#", "0"))
|
|
218
|
+
|
|
219
|
+
if n > param_count:
|
|
220
|
+
return ExecResult(
|
|
221
|
+
stdout="",
|
|
222
|
+
stderr=f"bash: shift: {n}: shift count out of range\n",
|
|
223
|
+
exit_code=1,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Collect remaining parameters
|
|
227
|
+
new_params = []
|
|
228
|
+
for i in range(n + 1, param_count + 1):
|
|
229
|
+
new_params.append(ctx.state.env.get(str(i), ""))
|
|
230
|
+
|
|
231
|
+
# Set new positional parameters
|
|
232
|
+
_set_positional_params(ctx, new_params)
|
|
233
|
+
|
|
234
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Shopt builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: shopt [-pqsu] [optname ...]
|
|
4
|
+
|
|
5
|
+
Shell options control various behaviors of the shell.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-s Enable (set) each optname
|
|
9
|
+
-u Disable (unset) each optname
|
|
10
|
+
-q Quiet mode, suppress output (exit status indicates if option is set)
|
|
11
|
+
-p Print options in a form that can be reused as input
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..types import InterpreterContext
|
|
18
|
+
from ...types import ExecResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Default states for shell options
|
|
22
|
+
DEFAULT_SHOPTS = {
|
|
23
|
+
# Bash common options
|
|
24
|
+
"expand_aliases": True,
|
|
25
|
+
"extglob": False,
|
|
26
|
+
"extquote": True,
|
|
27
|
+
"failglob": False,
|
|
28
|
+
"globstar": False,
|
|
29
|
+
"nocasematch": False,
|
|
30
|
+
"nullglob": False,
|
|
31
|
+
"dotglob": False,
|
|
32
|
+
"lastpipe": False,
|
|
33
|
+
"xpg_echo": False,
|
|
34
|
+
"progcomp": True,
|
|
35
|
+
"histappend": False,
|
|
36
|
+
"extdebug": False,
|
|
37
|
+
"inherit_errexit": False,
|
|
38
|
+
"command_sub_errexit": False,
|
|
39
|
+
"process_sub_fail": False,
|
|
40
|
+
# Strict options (for oil/ysh compatibility)
|
|
41
|
+
"strict_array": False,
|
|
42
|
+
"strict_arith": False,
|
|
43
|
+
"strict_argv": False,
|
|
44
|
+
"strict_arg_parse": False,
|
|
45
|
+
"strict_control_flow": False,
|
|
46
|
+
"strict_nameref": False,
|
|
47
|
+
"strict_word_eval": False,
|
|
48
|
+
"strict_tilde": False,
|
|
49
|
+
"strict_status": False,
|
|
50
|
+
"strict_binding": False,
|
|
51
|
+
"strict": False,
|
|
52
|
+
# Other options
|
|
53
|
+
"eval_unsafe_arith": False,
|
|
54
|
+
"ysh": False,
|
|
55
|
+
"compat_array": False,
|
|
56
|
+
"nounset": False,
|
|
57
|
+
"parse_at": False,
|
|
58
|
+
"simple_eval_builtin": False,
|
|
59
|
+
"no_last_fork": False,
|
|
60
|
+
"no_fork_last": False,
|
|
61
|
+
"no_dash_glob": False,
|
|
62
|
+
"globskipdots": True,
|
|
63
|
+
"ignore_shopt_not_impl": False,
|
|
64
|
+
"ignore_flags_not_impl": False,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> "ExecResult":
|
|
69
|
+
"""Create an ExecResult."""
|
|
70
|
+
from ...types import ExecResult
|
|
71
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_shopts(ctx: "InterpreterContext") -> dict[str, bool]:
|
|
75
|
+
"""Get the shell options dictionary from context."""
|
|
76
|
+
# Store shopts in env with __shopt__ prefix
|
|
77
|
+
shopts = {}
|
|
78
|
+
for name, default in DEFAULT_SHOPTS.items():
|
|
79
|
+
key = f"__shopt_{name}__"
|
|
80
|
+
if key in ctx.state.env:
|
|
81
|
+
shopts[name] = ctx.state.env[key] == "1"
|
|
82
|
+
else:
|
|
83
|
+
shopts[name] = default
|
|
84
|
+
return shopts
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _set_shopt(ctx: "InterpreterContext", name: str, value: bool) -> None:
|
|
88
|
+
"""Set a shell option."""
|
|
89
|
+
ctx.state.env[f"__shopt_{name}__"] = "1" if value else "0"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def handle_shopt(
|
|
93
|
+
ctx: "InterpreterContext", args: list[str]
|
|
94
|
+
) -> "ExecResult":
|
|
95
|
+
"""Execute the shopt builtin."""
|
|
96
|
+
# Parse options
|
|
97
|
+
set_mode = False
|
|
98
|
+
unset_mode = False
|
|
99
|
+
quiet_mode = False
|
|
100
|
+
print_mode = False
|
|
101
|
+
optnames = []
|
|
102
|
+
|
|
103
|
+
i = 0
|
|
104
|
+
while i < len(args):
|
|
105
|
+
arg = args[i]
|
|
106
|
+
if arg == "-s":
|
|
107
|
+
set_mode = True
|
|
108
|
+
elif arg == "-u":
|
|
109
|
+
unset_mode = True
|
|
110
|
+
elif arg == "-q":
|
|
111
|
+
quiet_mode = True
|
|
112
|
+
elif arg == "-p":
|
|
113
|
+
print_mode = True
|
|
114
|
+
elif arg == "--":
|
|
115
|
+
optnames.extend(args[i + 1:])
|
|
116
|
+
break
|
|
117
|
+
elif arg.startswith("-"):
|
|
118
|
+
# Handle combined flags like -su
|
|
119
|
+
for c in arg[1:]:
|
|
120
|
+
if c == "s":
|
|
121
|
+
set_mode = True
|
|
122
|
+
elif c == "u":
|
|
123
|
+
unset_mode = True
|
|
124
|
+
elif c == "q":
|
|
125
|
+
quiet_mode = True
|
|
126
|
+
elif c == "p":
|
|
127
|
+
print_mode = True
|
|
128
|
+
else:
|
|
129
|
+
return _result("", f"bash: shopt: -{c}: invalid option\n", 1)
|
|
130
|
+
else:
|
|
131
|
+
optnames.append(arg)
|
|
132
|
+
i += 1
|
|
133
|
+
|
|
134
|
+
shopts = _get_shopts(ctx)
|
|
135
|
+
|
|
136
|
+
# If both -s and -u specified, error
|
|
137
|
+
if set_mode and unset_mode:
|
|
138
|
+
return _result("", "bash: shopt: cannot set and unset options simultaneously\n", 1)
|
|
139
|
+
|
|
140
|
+
# No optnames and no -s/-u: show all options or those matching filter
|
|
141
|
+
if not optnames and not set_mode and not unset_mode:
|
|
142
|
+
output = []
|
|
143
|
+
for name in sorted(DEFAULT_SHOPTS.keys()):
|
|
144
|
+
state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
|
|
145
|
+
state_str = "on" if state else "off"
|
|
146
|
+
if print_mode:
|
|
147
|
+
prefix = "-s" if state else "-u"
|
|
148
|
+
output.append(f"shopt {prefix} {name}")
|
|
149
|
+
else:
|
|
150
|
+
output.append(f"{name}\t{state_str}")
|
|
151
|
+
if not quiet_mode:
|
|
152
|
+
return _result("\n".join(output) + "\n" if output else "", "", 0)
|
|
153
|
+
return _result("", "", 0)
|
|
154
|
+
|
|
155
|
+
# With -s/-u but no optnames: show options that match the state
|
|
156
|
+
if not optnames and (set_mode or unset_mode) and not quiet_mode:
|
|
157
|
+
target_state = set_mode
|
|
158
|
+
output = []
|
|
159
|
+
for name in sorted(DEFAULT_SHOPTS.keys()):
|
|
160
|
+
state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
|
|
161
|
+
if state == target_state:
|
|
162
|
+
state_str = "on" if state else "off"
|
|
163
|
+
if print_mode:
|
|
164
|
+
prefix = "-s" if state else "-u"
|
|
165
|
+
output.append(f"shopt {prefix} {name}")
|
|
166
|
+
else:
|
|
167
|
+
output.append(f"{name}\t{state_str}")
|
|
168
|
+
return _result("\n".join(output) + "\n" if output else "", "", 0)
|
|
169
|
+
|
|
170
|
+
# Process optnames
|
|
171
|
+
exit_code = 0
|
|
172
|
+
output = []
|
|
173
|
+
|
|
174
|
+
for name in optnames:
|
|
175
|
+
# Check if option exists
|
|
176
|
+
if name not in DEFAULT_SHOPTS:
|
|
177
|
+
# Unknown option - but if ignore_shopt_not_impl is set, silently ignore
|
|
178
|
+
if shopts.get("ignore_shopt_not_impl", False):
|
|
179
|
+
continue
|
|
180
|
+
return _result("", f"bash: shopt: {name}: invalid shell option name\n", 1)
|
|
181
|
+
|
|
182
|
+
if set_mode:
|
|
183
|
+
_set_shopt(ctx, name, True)
|
|
184
|
+
elif unset_mode:
|
|
185
|
+
_set_shopt(ctx, name, False)
|
|
186
|
+
else:
|
|
187
|
+
# Query mode
|
|
188
|
+
state = shopts.get(name, DEFAULT_SHOPTS.get(name, False))
|
|
189
|
+
if not state:
|
|
190
|
+
exit_code = 1
|
|
191
|
+
if not quiet_mode:
|
|
192
|
+
state_str = "on" if state else "off"
|
|
193
|
+
if print_mode:
|
|
194
|
+
prefix = "-s" if state else "-u"
|
|
195
|
+
output.append(f"shopt {prefix} {name}")
|
|
196
|
+
else:
|
|
197
|
+
output.append(f"{name}\t{state_str}")
|
|
198
|
+
|
|
199
|
+
if output and not quiet_mode:
|
|
200
|
+
return _result("\n".join(output) + "\n", "", exit_code)
|
|
201
|
+
return _result("", "", exit_code)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Source and eval builtin implementations.
|
|
2
|
+
|
|
3
|
+
source (or .) - Execute commands from a file in the current shell.
|
|
4
|
+
|
|
5
|
+
Usage: source filename [arguments]
|
|
6
|
+
. filename [arguments]
|
|
7
|
+
|
|
8
|
+
Read and execute commands from filename in the current shell environment.
|
|
9
|
+
If arguments are supplied, they become the positional parameters when
|
|
10
|
+
filename is executed.
|
|
11
|
+
|
|
12
|
+
eval - Construct a command by concatenating arguments.
|
|
13
|
+
|
|
14
|
+
Usage: eval [arg ...]
|
|
15
|
+
|
|
16
|
+
Concatenate arguments into a single command, then execute.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from ..types import InterpreterContext
|
|
23
|
+
from ...types import ExecResult
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def handle_source(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
27
|
+
"""Execute the source / . builtin."""
|
|
28
|
+
from ...types import ExecResult
|
|
29
|
+
|
|
30
|
+
if not args:
|
|
31
|
+
return ExecResult(
|
|
32
|
+
stdout="",
|
|
33
|
+
stderr="bash: source: filename argument required\n",
|
|
34
|
+
exit_code=2,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
filename = args[0]
|
|
38
|
+
script_args = args[1:]
|
|
39
|
+
|
|
40
|
+
# Resolve path
|
|
41
|
+
path = ctx.fs.resolve_path(ctx.state.cwd, filename)
|
|
42
|
+
|
|
43
|
+
# Read file
|
|
44
|
+
try:
|
|
45
|
+
content = await ctx.fs.read_file(path)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
return ExecResult(
|
|
48
|
+
stdout="",
|
|
49
|
+
stderr=f"bash: source: {filename}: No such file or directory\n",
|
|
50
|
+
exit_code=1,
|
|
51
|
+
)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
return ExecResult(
|
|
54
|
+
stdout="",
|
|
55
|
+
stderr=f"bash: source: {filename}: {e}\n",
|
|
56
|
+
exit_code=1,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Check source depth limit
|
|
60
|
+
ctx.state.source_depth += 1
|
|
61
|
+
if ctx.state.source_depth > ctx.limits.max_call_depth:
|
|
62
|
+
ctx.state.source_depth -= 1
|
|
63
|
+
return ExecResult(
|
|
64
|
+
stdout="",
|
|
65
|
+
stderr="bash: source: maximum source depth exceeded\n",
|
|
66
|
+
exit_code=1,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Save and set positional parameters if arguments provided
|
|
70
|
+
saved_params = {}
|
|
71
|
+
saved_count = None
|
|
72
|
+
if script_args:
|
|
73
|
+
# Save current positional parameters
|
|
74
|
+
i = 1
|
|
75
|
+
while str(i) in ctx.state.env:
|
|
76
|
+
saved_params[str(i)] = ctx.state.env[str(i)]
|
|
77
|
+
i += 1
|
|
78
|
+
saved_count = ctx.state.env.get("#", "0")
|
|
79
|
+
|
|
80
|
+
# Clear and set new positional parameters
|
|
81
|
+
i = 1
|
|
82
|
+
while str(i) in ctx.state.env:
|
|
83
|
+
del ctx.state.env[str(i)]
|
|
84
|
+
i += 1
|
|
85
|
+
|
|
86
|
+
for i, arg in enumerate(script_args, start=1):
|
|
87
|
+
ctx.state.env[str(i)] = arg
|
|
88
|
+
ctx.state.env["#"] = str(len(script_args))
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
# Parse and execute
|
|
92
|
+
from ...parser import parse
|
|
93
|
+
|
|
94
|
+
ast = parse(content)
|
|
95
|
+
result = await ctx.execute_script(ast)
|
|
96
|
+
return result
|
|
97
|
+
finally:
|
|
98
|
+
ctx.state.source_depth -= 1
|
|
99
|
+
|
|
100
|
+
# Restore positional parameters if we changed them
|
|
101
|
+
if saved_count is not None:
|
|
102
|
+
# Clear current positional parameters
|
|
103
|
+
i = 1
|
|
104
|
+
while str(i) in ctx.state.env:
|
|
105
|
+
del ctx.state.env[str(i)]
|
|
106
|
+
i += 1
|
|
107
|
+
|
|
108
|
+
# Restore saved parameters
|
|
109
|
+
for k, v in saved_params.items():
|
|
110
|
+
ctx.state.env[k] = v
|
|
111
|
+
ctx.state.env["#"] = saved_count
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def handle_eval(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
115
|
+
"""Execute the eval builtin."""
|
|
116
|
+
from ...types import ExecResult
|
|
117
|
+
|
|
118
|
+
if not args:
|
|
119
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
120
|
+
|
|
121
|
+
# Concatenate arguments with spaces
|
|
122
|
+
command = " ".join(args)
|
|
123
|
+
|
|
124
|
+
# Parse and execute
|
|
125
|
+
try:
|
|
126
|
+
from ...parser import parse
|
|
127
|
+
|
|
128
|
+
ast = parse(command)
|
|
129
|
+
result = await ctx.execute_script(ast)
|
|
130
|
+
return result
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return ExecResult(
|
|
133
|
+
stdout="",
|
|
134
|
+
stderr=f"bash: eval: {e}\n",
|
|
135
|
+
exit_code=1,
|
|
136
|
+
)
|