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,381 @@
|
|
|
1
|
+
"""Control Flow Execution.
|
|
2
|
+
|
|
3
|
+
Handles control flow constructs:
|
|
4
|
+
- if/elif/else
|
|
5
|
+
- for loops
|
|
6
|
+
- C-style for loops
|
|
7
|
+
- while loops
|
|
8
|
+
- until loops
|
|
9
|
+
- case statements
|
|
10
|
+
- break/continue
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from ..ast.types import (
|
|
16
|
+
IfNode,
|
|
17
|
+
ForNode,
|
|
18
|
+
CStyleForNode,
|
|
19
|
+
WhileNode,
|
|
20
|
+
UntilNode,
|
|
21
|
+
CaseNode,
|
|
22
|
+
)
|
|
23
|
+
from ..types import ExecResult
|
|
24
|
+
from .errors import BreakError, ContinueError, ExecutionLimitError
|
|
25
|
+
from .expansion import expand_word_async, expand_word_with_glob, evaluate_arithmetic
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .types import InterpreterContext
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _result(stdout: str, stderr: str, exit_code: int) -> ExecResult:
|
|
32
|
+
"""Create an ExecResult."""
|
|
33
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _failure(stderr: str) -> ExecResult:
|
|
37
|
+
"""Create a failed result."""
|
|
38
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=1)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def execute_if(ctx: "InterpreterContext", node: IfNode) -> ExecResult:
|
|
42
|
+
"""Execute an if statement."""
|
|
43
|
+
stdout = ""
|
|
44
|
+
stderr = ""
|
|
45
|
+
|
|
46
|
+
for clause in node.clauses:
|
|
47
|
+
# Execute condition
|
|
48
|
+
cond_result = await execute_condition(ctx, clause.condition)
|
|
49
|
+
stdout += cond_result.stdout
|
|
50
|
+
stderr += cond_result.stderr
|
|
51
|
+
|
|
52
|
+
if cond_result.exit_code == 0:
|
|
53
|
+
# Condition is true - execute body
|
|
54
|
+
return await execute_statements(ctx, clause.body, stdout, stderr)
|
|
55
|
+
|
|
56
|
+
# No condition matched - check for else
|
|
57
|
+
if node.else_body:
|
|
58
|
+
return await execute_statements(ctx, node.else_body, stdout, stderr)
|
|
59
|
+
|
|
60
|
+
return _result(stdout, stderr, 0)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def execute_for(ctx: "InterpreterContext", node: ForNode) -> ExecResult:
|
|
64
|
+
"""Execute a for loop."""
|
|
65
|
+
stdout = ""
|
|
66
|
+
stderr = ""
|
|
67
|
+
exit_code = 0
|
|
68
|
+
iterations = 0
|
|
69
|
+
|
|
70
|
+
# Validate variable name
|
|
71
|
+
import re
|
|
72
|
+
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', node.variable):
|
|
73
|
+
return _failure(f"bash: `{node.variable}': not a valid identifier\n")
|
|
74
|
+
|
|
75
|
+
# Get words to iterate over
|
|
76
|
+
words: list[str] = []
|
|
77
|
+
if node.words is None:
|
|
78
|
+
# Iterate over positional parameters
|
|
79
|
+
params = ctx.state.env.get("@", "").split()
|
|
80
|
+
words = [p for p in params if p]
|
|
81
|
+
elif len(node.words) == 0:
|
|
82
|
+
words = []
|
|
83
|
+
else:
|
|
84
|
+
for word in node.words:
|
|
85
|
+
expanded = await expand_word_with_glob(ctx, word)
|
|
86
|
+
words.extend(expanded["values"])
|
|
87
|
+
|
|
88
|
+
ctx.state.loop_depth += 1
|
|
89
|
+
try:
|
|
90
|
+
for value in words:
|
|
91
|
+
iterations += 1
|
|
92
|
+
if iterations > ctx.limits.max_loop_iterations:
|
|
93
|
+
raise ExecutionLimitError(
|
|
94
|
+
f"for loop: too many iterations ({ctx.limits.max_loop_iterations})",
|
|
95
|
+
"iterations",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
ctx.state.env[node.variable] = value
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
for stmt in node.body:
|
|
102
|
+
result = await ctx.execute_statement(stmt)
|
|
103
|
+
stdout += result.stdout
|
|
104
|
+
stderr += result.stderr
|
|
105
|
+
exit_code = result.exit_code
|
|
106
|
+
except BreakError as e:
|
|
107
|
+
stdout += e.stdout
|
|
108
|
+
stderr += e.stderr
|
|
109
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
110
|
+
e.levels -= 1
|
|
111
|
+
e.stdout = stdout
|
|
112
|
+
e.stderr = stderr
|
|
113
|
+
raise
|
|
114
|
+
break
|
|
115
|
+
except ContinueError as e:
|
|
116
|
+
stdout += e.stdout
|
|
117
|
+
stderr += e.stderr
|
|
118
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
119
|
+
e.levels -= 1
|
|
120
|
+
e.stdout = stdout
|
|
121
|
+
e.stderr = stderr
|
|
122
|
+
raise
|
|
123
|
+
continue
|
|
124
|
+
finally:
|
|
125
|
+
ctx.state.loop_depth -= 1
|
|
126
|
+
|
|
127
|
+
return _result(stdout, stderr, exit_code)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def execute_c_style_for(ctx: "InterpreterContext", node: CStyleForNode) -> ExecResult:
|
|
131
|
+
"""Execute a C-style for loop: for ((init; cond; update))."""
|
|
132
|
+
stdout = ""
|
|
133
|
+
stderr = ""
|
|
134
|
+
exit_code = 0
|
|
135
|
+
iterations = 0
|
|
136
|
+
|
|
137
|
+
# Execute init
|
|
138
|
+
if node.init:
|
|
139
|
+
await evaluate_arithmetic(ctx, node.init.expression)
|
|
140
|
+
|
|
141
|
+
ctx.state.loop_depth += 1
|
|
142
|
+
try:
|
|
143
|
+
while True:
|
|
144
|
+
iterations += 1
|
|
145
|
+
if iterations > ctx.limits.max_loop_iterations:
|
|
146
|
+
raise ExecutionLimitError(
|
|
147
|
+
f"for loop: too many iterations ({ctx.limits.max_loop_iterations})",
|
|
148
|
+
"iterations",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Check condition
|
|
152
|
+
if node.condition:
|
|
153
|
+
cond_result = await evaluate_arithmetic(ctx, node.condition.expression)
|
|
154
|
+
if cond_result == 0:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
for stmt in node.body:
|
|
159
|
+
result = await ctx.execute_statement(stmt)
|
|
160
|
+
stdout += result.stdout
|
|
161
|
+
stderr += result.stderr
|
|
162
|
+
exit_code = result.exit_code
|
|
163
|
+
except BreakError as e:
|
|
164
|
+
stdout += e.stdout
|
|
165
|
+
stderr += e.stderr
|
|
166
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
167
|
+
e.levels -= 1
|
|
168
|
+
raise
|
|
169
|
+
break
|
|
170
|
+
except ContinueError as e:
|
|
171
|
+
stdout += e.stdout
|
|
172
|
+
stderr += e.stderr
|
|
173
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
174
|
+
e.levels -= 1
|
|
175
|
+
raise
|
|
176
|
+
# Still run update on continue
|
|
177
|
+
if node.update:
|
|
178
|
+
await evaluate_arithmetic(ctx, node.update.expression)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Execute update
|
|
182
|
+
if node.update:
|
|
183
|
+
await evaluate_arithmetic(ctx, node.update.expression)
|
|
184
|
+
finally:
|
|
185
|
+
ctx.state.loop_depth -= 1
|
|
186
|
+
|
|
187
|
+
return _result(stdout, stderr, exit_code)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def execute_while(ctx: "InterpreterContext", node: WhileNode, stdin: str = "") -> ExecResult:
|
|
191
|
+
"""Execute a while loop."""
|
|
192
|
+
stdout = ""
|
|
193
|
+
stderr = ""
|
|
194
|
+
exit_code = 0
|
|
195
|
+
iterations = 0
|
|
196
|
+
|
|
197
|
+
ctx.state.loop_depth += 1
|
|
198
|
+
try:
|
|
199
|
+
while True:
|
|
200
|
+
iterations += 1
|
|
201
|
+
if iterations > ctx.limits.max_loop_iterations:
|
|
202
|
+
raise ExecutionLimitError(
|
|
203
|
+
f"while loop: too many iterations ({ctx.limits.max_loop_iterations})",
|
|
204
|
+
"iterations",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Execute condition
|
|
208
|
+
saved_in_condition = ctx.state.in_condition
|
|
209
|
+
ctx.state.in_condition = True
|
|
210
|
+
try:
|
|
211
|
+
cond_result = await execute_condition(ctx, node.condition)
|
|
212
|
+
stdout += cond_result.stdout
|
|
213
|
+
stderr += cond_result.stderr
|
|
214
|
+
|
|
215
|
+
if cond_result.exit_code != 0:
|
|
216
|
+
break
|
|
217
|
+
finally:
|
|
218
|
+
ctx.state.in_condition = saved_in_condition
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
for stmt in node.body:
|
|
222
|
+
result = await ctx.execute_statement(stmt)
|
|
223
|
+
stdout += result.stdout
|
|
224
|
+
stderr += result.stderr
|
|
225
|
+
exit_code = result.exit_code
|
|
226
|
+
except BreakError as e:
|
|
227
|
+
stdout += e.stdout
|
|
228
|
+
stderr += e.stderr
|
|
229
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
230
|
+
e.levels -= 1
|
|
231
|
+
raise
|
|
232
|
+
break
|
|
233
|
+
except ContinueError as e:
|
|
234
|
+
stdout += e.stdout
|
|
235
|
+
stderr += e.stderr
|
|
236
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
237
|
+
e.levels -= 1
|
|
238
|
+
raise
|
|
239
|
+
continue
|
|
240
|
+
finally:
|
|
241
|
+
ctx.state.loop_depth -= 1
|
|
242
|
+
|
|
243
|
+
return _result(stdout, stderr, exit_code)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def execute_until(ctx: "InterpreterContext", node: UntilNode) -> ExecResult:
|
|
247
|
+
"""Execute an until loop."""
|
|
248
|
+
stdout = ""
|
|
249
|
+
stderr = ""
|
|
250
|
+
exit_code = 0
|
|
251
|
+
iterations = 0
|
|
252
|
+
|
|
253
|
+
ctx.state.loop_depth += 1
|
|
254
|
+
try:
|
|
255
|
+
while True:
|
|
256
|
+
iterations += 1
|
|
257
|
+
if iterations > ctx.limits.max_loop_iterations:
|
|
258
|
+
raise ExecutionLimitError(
|
|
259
|
+
f"until loop: too many iterations ({ctx.limits.max_loop_iterations})",
|
|
260
|
+
"iterations",
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Execute condition (until exits when condition is TRUE)
|
|
264
|
+
saved_in_condition = ctx.state.in_condition
|
|
265
|
+
ctx.state.in_condition = True
|
|
266
|
+
try:
|
|
267
|
+
cond_result = await execute_condition(ctx, node.condition)
|
|
268
|
+
stdout += cond_result.stdout
|
|
269
|
+
stderr += cond_result.stderr
|
|
270
|
+
|
|
271
|
+
if cond_result.exit_code == 0:
|
|
272
|
+
break # Condition became true, exit loop
|
|
273
|
+
finally:
|
|
274
|
+
ctx.state.in_condition = saved_in_condition
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
for stmt in node.body:
|
|
278
|
+
result = await ctx.execute_statement(stmt)
|
|
279
|
+
stdout += result.stdout
|
|
280
|
+
stderr += result.stderr
|
|
281
|
+
exit_code = result.exit_code
|
|
282
|
+
except BreakError as e:
|
|
283
|
+
stdout += e.stdout
|
|
284
|
+
stderr += e.stderr
|
|
285
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
286
|
+
e.levels -= 1
|
|
287
|
+
raise
|
|
288
|
+
break
|
|
289
|
+
except ContinueError as e:
|
|
290
|
+
stdout += e.stdout
|
|
291
|
+
stderr += e.stderr
|
|
292
|
+
if e.levels > 1 and ctx.state.loop_depth > 1:
|
|
293
|
+
e.levels -= 1
|
|
294
|
+
raise
|
|
295
|
+
continue
|
|
296
|
+
finally:
|
|
297
|
+
ctx.state.loop_depth -= 1
|
|
298
|
+
|
|
299
|
+
return _result(stdout, stderr, exit_code)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def execute_case(ctx: "InterpreterContext", node: CaseNode) -> ExecResult:
|
|
303
|
+
"""Execute a case statement."""
|
|
304
|
+
import fnmatch
|
|
305
|
+
|
|
306
|
+
stdout = ""
|
|
307
|
+
stderr = ""
|
|
308
|
+
exit_code = 0
|
|
309
|
+
|
|
310
|
+
# Expand the word to match against
|
|
311
|
+
word_value = await expand_word_async(ctx, node.word)
|
|
312
|
+
|
|
313
|
+
for case_item in node.items:
|
|
314
|
+
# Check each pattern
|
|
315
|
+
matched = False
|
|
316
|
+
for pattern in case_item.patterns:
|
|
317
|
+
pattern_value = await expand_word_async(ctx, pattern)
|
|
318
|
+
if fnmatch.fnmatch(word_value, pattern_value):
|
|
319
|
+
matched = True
|
|
320
|
+
break
|
|
321
|
+
|
|
322
|
+
if matched:
|
|
323
|
+
# Execute the body for this case
|
|
324
|
+
for stmt in case_item.body:
|
|
325
|
+
result = await ctx.execute_statement(stmt)
|
|
326
|
+
stdout += result.stdout
|
|
327
|
+
stderr += result.stderr
|
|
328
|
+
exit_code = result.exit_code
|
|
329
|
+
|
|
330
|
+
# Check terminator
|
|
331
|
+
if case_item.terminator == ";;":
|
|
332
|
+
# Normal termination - exit case
|
|
333
|
+
break
|
|
334
|
+
elif case_item.terminator == ";&":
|
|
335
|
+
# Fall through to next case body (without pattern check)
|
|
336
|
+
continue
|
|
337
|
+
elif case_item.terminator == ";;&":
|
|
338
|
+
# Continue checking patterns
|
|
339
|
+
continue
|
|
340
|
+
else:
|
|
341
|
+
break
|
|
342
|
+
|
|
343
|
+
return _result(stdout, stderr, exit_code)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
async def execute_condition(ctx: "InterpreterContext", condition: list) -> ExecResult:
|
|
347
|
+
"""Execute a condition (list of statements) and return the result."""
|
|
348
|
+
stdout = ""
|
|
349
|
+
stderr = ""
|
|
350
|
+
exit_code = 0
|
|
351
|
+
|
|
352
|
+
saved_in_condition = ctx.state.in_condition
|
|
353
|
+
ctx.state.in_condition = True
|
|
354
|
+
try:
|
|
355
|
+
for stmt in condition:
|
|
356
|
+
result = await ctx.execute_statement(stmt)
|
|
357
|
+
stdout += result.stdout
|
|
358
|
+
stderr += result.stderr
|
|
359
|
+
exit_code = result.exit_code
|
|
360
|
+
finally:
|
|
361
|
+
ctx.state.in_condition = saved_in_condition
|
|
362
|
+
|
|
363
|
+
return _result(stdout, stderr, exit_code)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def execute_statements(
|
|
367
|
+
ctx: "InterpreterContext",
|
|
368
|
+
statements: list,
|
|
369
|
+
stdout: str = "",
|
|
370
|
+
stderr: str = "",
|
|
371
|
+
) -> ExecResult:
|
|
372
|
+
"""Execute a list of statements."""
|
|
373
|
+
exit_code = 0
|
|
374
|
+
|
|
375
|
+
for stmt in statements:
|
|
376
|
+
result = await ctx.execute_statement(stmt)
|
|
377
|
+
stdout += result.stdout
|
|
378
|
+
stderr += result.stderr
|
|
379
|
+
exit_code = result.exit_code
|
|
380
|
+
|
|
381
|
+
return _result(stdout, stderr, exit_code)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Interpreter errors for just-bash."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InterpreterError(Exception):
|
|
5
|
+
"""Base class for interpreter errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str = ""):
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.stdout = ""
|
|
10
|
+
self.stderr = ""
|
|
11
|
+
|
|
12
|
+
def prepend_output(self, stdout: str, stderr: str) -> None:
|
|
13
|
+
"""Prepend accumulated output to error output."""
|
|
14
|
+
self.stdout = stdout + self.stdout
|
|
15
|
+
self.stderr = stderr + self.stderr
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ExitError(InterpreterError):
|
|
19
|
+
"""Raised when 'exit' builtin is called."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, exit_code: int, stdout: str = "", stderr: str = ""):
|
|
22
|
+
super().__init__(f"exit {exit_code}")
|
|
23
|
+
self.exit_code = exit_code
|
|
24
|
+
self.stdout = stdout
|
|
25
|
+
self.stderr = stderr
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReturnError(InterpreterError):
|
|
29
|
+
"""Raised when 'return' builtin is called."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, exit_code: int, stdout: str = "", stderr: str = ""):
|
|
32
|
+
super().__init__(f"return {exit_code}")
|
|
33
|
+
self.exit_code = exit_code
|
|
34
|
+
self.stdout = stdout
|
|
35
|
+
self.stderr = stderr
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BreakError(InterpreterError):
|
|
39
|
+
"""Raised when 'break' builtin is called."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, levels: int = 1, stdout: str = "", stderr: str = ""):
|
|
42
|
+
super().__init__(f"break {levels}")
|
|
43
|
+
self.levels = levels
|
|
44
|
+
self.stdout = stdout
|
|
45
|
+
self.stderr = stderr
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ContinueError(InterpreterError):
|
|
49
|
+
"""Raised when 'continue' builtin is called."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, levels: int = 1, stdout: str = "", stderr: str = ""):
|
|
52
|
+
super().__init__(f"continue {levels}")
|
|
53
|
+
self.levels = levels
|
|
54
|
+
self.stdout = stdout
|
|
55
|
+
self.stderr = stderr
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ErrexitError(InterpreterError):
|
|
59
|
+
"""Raised when errexit (set -e) terminates execution."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, exit_code: int, stdout: str = "", stderr: str = ""):
|
|
62
|
+
super().__init__(f"errexit with code {exit_code}")
|
|
63
|
+
self.exit_code = exit_code
|
|
64
|
+
self.stdout = stdout
|
|
65
|
+
self.stderr = stderr
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class NounsetError(InterpreterError):
|
|
69
|
+
"""Raised when nounset (set -u) encounters unset variable."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, variable: str, stdout: str = "", stderr: str = ""):
|
|
72
|
+
super().__init__(f"unbound variable: {variable}")
|
|
73
|
+
self.variable = variable
|
|
74
|
+
self.stdout = stdout
|
|
75
|
+
self.stderr = stderr
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class BadSubstitutionError(InterpreterError):
|
|
79
|
+
"""Raised for bad parameter substitution."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, message: str, stdout: str = "", stderr: str = ""):
|
|
82
|
+
super().__init__(message)
|
|
83
|
+
self.stdout = stdout
|
|
84
|
+
self.stderr = stderr
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class ArithmeticError(InterpreterError):
|
|
88
|
+
"""Raised for arithmetic evaluation errors."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, message: str, stdout: str = "", stderr: str = ""):
|
|
91
|
+
super().__init__(message)
|
|
92
|
+
self.stdout = stdout
|
|
93
|
+
self.stderr = stderr
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ExecutionLimitError(InterpreterError):
|
|
97
|
+
"""Raised when execution limits are exceeded."""
|
|
98
|
+
|
|
99
|
+
def __init__(self, message: str, limit_type: str):
|
|
100
|
+
super().__init__(message)
|
|
101
|
+
self.limit_type = limit_type
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SubshellExitError(InterpreterError):
|
|
105
|
+
"""Raised when a subshell exits."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, exit_code: int, stdout: str = "", stderr: str = ""):
|
|
108
|
+
super().__init__(f"subshell exit {exit_code}")
|
|
109
|
+
self.exit_code = exit_code
|
|
110
|
+
self.stdout = stdout
|
|
111
|
+
self.stderr = stderr
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def is_scope_exit_error(error: Exception) -> bool:
|
|
115
|
+
"""Check if an error should exit the current scope."""
|
|
116
|
+
return isinstance(error, (ExitError, ReturnError, BreakError, ContinueError))
|