just-bash 0.1.5__py3-none-any.whl → 0.1.10__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/ast/factory.py +3 -1
- just_bash/bash.py +28 -6
- just_bash/commands/awk/awk.py +362 -17
- just_bash/commands/cat/cat.py +5 -1
- just_bash/commands/echo/echo.py +33 -1
- just_bash/commands/grep/grep.py +141 -3
- just_bash/commands/od/od.py +144 -30
- just_bash/commands/printf/printf.py +289 -87
- just_bash/commands/pwd/pwd.py +32 -2
- just_bash/commands/read/read.py +243 -64
- just_bash/commands/readlink/readlink.py +3 -9
- just_bash/commands/registry.py +32 -0
- just_bash/commands/rmdir/__init__.py +5 -0
- just_bash/commands/rmdir/rmdir.py +160 -0
- just_bash/commands/sed/sed.py +142 -31
- just_bash/commands/shuf/__init__.py +5 -0
- just_bash/commands/shuf/shuf.py +242 -0
- just_bash/commands/stat/stat.py +9 -0
- just_bash/commands/time/__init__.py +5 -0
- just_bash/commands/time/time.py +74 -0
- just_bash/commands/touch/touch.py +118 -8
- just_bash/commands/whoami/__init__.py +5 -0
- just_bash/commands/whoami/whoami.py +18 -0
- just_bash/fs/in_memory_fs.py +22 -0
- just_bash/fs/overlay_fs.py +22 -1
- just_bash/interpreter/__init__.py +1 -1
- just_bash/interpreter/builtins/__init__.py +2 -0
- just_bash/interpreter/builtins/control.py +4 -8
- just_bash/interpreter/builtins/declare.py +321 -24
- just_bash/interpreter/builtins/getopts.py +163 -0
- just_bash/interpreter/builtins/let.py +2 -2
- just_bash/interpreter/builtins/local.py +71 -5
- just_bash/interpreter/builtins/misc.py +22 -6
- just_bash/interpreter/builtins/readonly.py +38 -10
- just_bash/interpreter/builtins/set.py +58 -8
- just_bash/interpreter/builtins/test.py +136 -19
- just_bash/interpreter/builtins/unset.py +62 -10
- just_bash/interpreter/conditionals.py +29 -4
- just_bash/interpreter/control_flow.py +61 -17
- just_bash/interpreter/expansion.py +1647 -104
- just_bash/interpreter/interpreter.py +436 -69
- just_bash/interpreter/types.py +263 -2
- just_bash/parser/__init__.py +2 -0
- just_bash/parser/lexer.py +295 -26
- just_bash/parser/parser.py +523 -64
- just_bash/types.py +11 -0
- {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/METADATA +40 -1
- {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/RECORD +49 -40
- {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/WHEEL +0 -0
|
@@ -164,9 +164,17 @@ def compare_strings(op: str, left: str, right: str, allow_pattern: bool = False)
|
|
|
164
164
|
def match_pattern(value: str, pattern: str) -> bool:
|
|
165
165
|
"""Match a value against a glob-style pattern.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Supports standard globs and extended glob patterns.
|
|
168
168
|
"""
|
|
169
|
-
|
|
169
|
+
import re as _re
|
|
170
|
+
# Use regex for extglob patterns, fnmatch for standard globs
|
|
171
|
+
if _re.search(r'[@?*+!]\(', pattern):
|
|
172
|
+
from .expansion import glob_to_regex
|
|
173
|
+
regex_pat = "^" + glob_to_regex(pattern) + "$"
|
|
174
|
+
try:
|
|
175
|
+
return bool(_re.match(regex_pat, value))
|
|
176
|
+
except _re.error:
|
|
177
|
+
pass
|
|
170
178
|
return fnmatch.fnmatch(value, pattern)
|
|
171
179
|
|
|
172
180
|
|
|
@@ -356,15 +364,32 @@ async def evaluate_binary_file_test(
|
|
|
356
364
|
|
|
357
365
|
def evaluate_variable_test(ctx: "InterpreterContext", name: str) -> bool:
|
|
358
366
|
"""Test if a variable is set (-v)."""
|
|
367
|
+
from .types import VariableStore
|
|
368
|
+
env = ctx.state.env
|
|
369
|
+
|
|
359
370
|
# Handle array element syntax: arr[idx]
|
|
360
371
|
if "[" in name and name.endswith("]"):
|
|
361
372
|
base = name[:name.index("[")]
|
|
362
373
|
idx = name[name.index("[") + 1:-1]
|
|
374
|
+
# Resolve nameref for array base
|
|
375
|
+
if isinstance(env, VariableStore) and env.is_nameref(base):
|
|
376
|
+
try:
|
|
377
|
+
base = env.resolve_nameref(base)
|
|
378
|
+
except ValueError:
|
|
379
|
+
return False
|
|
363
380
|
# Check if array element exists
|
|
364
381
|
key = f"{base}_{idx}"
|
|
365
|
-
return key in
|
|
382
|
+
return key in env
|
|
383
|
+
|
|
384
|
+
# Resolve nameref
|
|
385
|
+
if isinstance(env, VariableStore) and env.is_nameref(name):
|
|
386
|
+
try:
|
|
387
|
+
resolved = env.resolve_nameref(name)
|
|
388
|
+
return resolved in env
|
|
389
|
+
except ValueError:
|
|
390
|
+
return False
|
|
366
391
|
|
|
367
|
-
return name in
|
|
392
|
+
return name in env
|
|
368
393
|
|
|
369
394
|
|
|
370
395
|
def evaluate_shell_option(ctx: "InterpreterContext", option: str) -> bool:
|
|
@@ -21,8 +21,8 @@ from ..ast.types import (
|
|
|
21
21
|
CaseNode,
|
|
22
22
|
)
|
|
23
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
|
|
24
|
+
from .errors import BreakError, ContinueError, ExecutionLimitError, ExitError
|
|
25
|
+
from .expansion import expand_word_async, expand_word_for_case_pattern, expand_word_with_glob, evaluate_arithmetic
|
|
26
26
|
|
|
27
27
|
if TYPE_CHECKING:
|
|
28
28
|
from .types import InterpreterContext
|
|
@@ -75,9 +75,18 @@ async def execute_for(ctx: "InterpreterContext", node: ForNode) -> ExecResult:
|
|
|
75
75
|
# Get words to iterate over
|
|
76
76
|
words: list[str] = []
|
|
77
77
|
if node.words is None:
|
|
78
|
-
# Iterate over positional parameters
|
|
79
|
-
|
|
80
|
-
words = [
|
|
78
|
+
# Iterate over positional parameters ($1, $2, ...)
|
|
79
|
+
count = int(ctx.state.env.get("#", "0"))
|
|
80
|
+
words = []
|
|
81
|
+
for pi in range(1, count + 1):
|
|
82
|
+
val = ctx.state.env.get(str(pi), "")
|
|
83
|
+
if val:
|
|
84
|
+
words.append(val)
|
|
85
|
+
if not words:
|
|
86
|
+
# Fallback to $@ if positional params not set individually
|
|
87
|
+
at_val = ctx.state.env.get("@", "")
|
|
88
|
+
if at_val:
|
|
89
|
+
words = at_val.split()
|
|
81
90
|
elif len(node.words) == 0:
|
|
82
91
|
words = []
|
|
83
92
|
else:
|
|
@@ -121,6 +130,9 @@ async def execute_for(ctx: "InterpreterContext", node: ForNode) -> ExecResult:
|
|
|
121
130
|
e.stderr = stderr
|
|
122
131
|
raise
|
|
123
132
|
continue
|
|
133
|
+
except ExitError as e:
|
|
134
|
+
e.prepend_output(stdout, stderr)
|
|
135
|
+
raise
|
|
124
136
|
finally:
|
|
125
137
|
ctx.state.loop_depth -= 1
|
|
126
138
|
|
|
@@ -177,6 +189,9 @@ async def execute_c_style_for(ctx: "InterpreterContext", node: CStyleForNode) ->
|
|
|
177
189
|
if node.update:
|
|
178
190
|
await evaluate_arithmetic(ctx, node.update.expression)
|
|
179
191
|
continue
|
|
192
|
+
except ExitError as e:
|
|
193
|
+
e.prepend_output(stdout, stderr)
|
|
194
|
+
raise
|
|
180
195
|
|
|
181
196
|
# Execute update
|
|
182
197
|
if node.update:
|
|
@@ -237,6 +252,9 @@ async def execute_while(ctx: "InterpreterContext", node: WhileNode, stdin: str =
|
|
|
237
252
|
e.levels -= 1
|
|
238
253
|
raise
|
|
239
254
|
continue
|
|
255
|
+
except ExitError as e:
|
|
256
|
+
e.prepend_output(stdout, stderr)
|
|
257
|
+
raise
|
|
240
258
|
finally:
|
|
241
259
|
ctx.state.loop_depth -= 1
|
|
242
260
|
|
|
@@ -293,6 +311,9 @@ async def execute_until(ctx: "InterpreterContext", node: UntilNode) -> ExecResul
|
|
|
293
311
|
e.levels -= 1
|
|
294
312
|
raise
|
|
295
313
|
continue
|
|
314
|
+
except ExitError as e:
|
|
315
|
+
e.prepend_output(stdout, stderr)
|
|
316
|
+
raise
|
|
296
317
|
finally:
|
|
297
318
|
ctx.state.loop_depth -= 1
|
|
298
319
|
|
|
@@ -302,6 +323,8 @@ async def execute_until(ctx: "InterpreterContext", node: UntilNode) -> ExecResul
|
|
|
302
323
|
async def execute_case(ctx: "InterpreterContext", node: CaseNode) -> ExecResult:
|
|
303
324
|
"""Execute a case statement."""
|
|
304
325
|
import fnmatch
|
|
326
|
+
import re as _re
|
|
327
|
+
from .expansion import glob_to_regex
|
|
305
328
|
|
|
306
329
|
stdout = ""
|
|
307
330
|
stderr = ""
|
|
@@ -310,22 +333,42 @@ async def execute_case(ctx: "InterpreterContext", node: CaseNode) -> ExecResult:
|
|
|
310
333
|
# Expand the word to match against
|
|
311
334
|
word_value = await expand_word_async(ctx, node.word)
|
|
312
335
|
|
|
336
|
+
def _case_match(value: str, pattern: str) -> bool:
|
|
337
|
+
"""Match value against case pattern (supports extglob)."""
|
|
338
|
+
if _re.search(r'[@?*+!]\(', pattern):
|
|
339
|
+
regex_pat = "^" + glob_to_regex(pattern) + "$"
|
|
340
|
+
try:
|
|
341
|
+
return bool(_re.match(regex_pat, value))
|
|
342
|
+
except _re.error:
|
|
343
|
+
pass
|
|
344
|
+
return fnmatch.fnmatch(value, pattern)
|
|
345
|
+
|
|
346
|
+
fall_through = False
|
|
313
347
|
for case_item in node.items:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
348
|
+
if not fall_through:
|
|
349
|
+
# Check each pattern
|
|
350
|
+
matched = False
|
|
351
|
+
for pattern in case_item.patterns:
|
|
352
|
+
pattern_value = await expand_word_for_case_pattern(ctx, pattern)
|
|
353
|
+
if _case_match(word_value, pattern_value):
|
|
354
|
+
matched = True
|
|
355
|
+
break
|
|
356
|
+
else:
|
|
357
|
+
# ;& fall-through: execute without pattern check
|
|
358
|
+
matched = True
|
|
359
|
+
fall_through = False
|
|
321
360
|
|
|
322
361
|
if matched:
|
|
323
362
|
# Execute the body for this case
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
363
|
+
try:
|
|
364
|
+
for stmt in case_item.body:
|
|
365
|
+
result = await ctx.execute_statement(stmt)
|
|
366
|
+
stdout += result.stdout
|
|
367
|
+
stderr += result.stderr
|
|
368
|
+
exit_code = result.exit_code
|
|
369
|
+
except ExitError as e:
|
|
370
|
+
e.prepend_output(stdout, stderr)
|
|
371
|
+
raise
|
|
329
372
|
|
|
330
373
|
# Check terminator
|
|
331
374
|
if case_item.terminator == ";;":
|
|
@@ -333,6 +376,7 @@ async def execute_case(ctx: "InterpreterContext", node: CaseNode) -> ExecResult:
|
|
|
333
376
|
break
|
|
334
377
|
elif case_item.terminator == ";&":
|
|
335
378
|
# Fall through to next case body (without pattern check)
|
|
379
|
+
fall_through = True
|
|
336
380
|
continue
|
|
337
381
|
elif case_item.terminator == ";;&":
|
|
338
382
|
# Continue checking patterns
|