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.
Files changed (49) hide show
  1. just_bash/ast/factory.py +3 -1
  2. just_bash/bash.py +28 -6
  3. just_bash/commands/awk/awk.py +362 -17
  4. just_bash/commands/cat/cat.py +5 -1
  5. just_bash/commands/echo/echo.py +33 -1
  6. just_bash/commands/grep/grep.py +141 -3
  7. just_bash/commands/od/od.py +144 -30
  8. just_bash/commands/printf/printf.py +289 -87
  9. just_bash/commands/pwd/pwd.py +32 -2
  10. just_bash/commands/read/read.py +243 -64
  11. just_bash/commands/readlink/readlink.py +3 -9
  12. just_bash/commands/registry.py +32 -0
  13. just_bash/commands/rmdir/__init__.py +5 -0
  14. just_bash/commands/rmdir/rmdir.py +160 -0
  15. just_bash/commands/sed/sed.py +142 -31
  16. just_bash/commands/shuf/__init__.py +5 -0
  17. just_bash/commands/shuf/shuf.py +242 -0
  18. just_bash/commands/stat/stat.py +9 -0
  19. just_bash/commands/time/__init__.py +5 -0
  20. just_bash/commands/time/time.py +74 -0
  21. just_bash/commands/touch/touch.py +118 -8
  22. just_bash/commands/whoami/__init__.py +5 -0
  23. just_bash/commands/whoami/whoami.py +18 -0
  24. just_bash/fs/in_memory_fs.py +22 -0
  25. just_bash/fs/overlay_fs.py +22 -1
  26. just_bash/interpreter/__init__.py +1 -1
  27. just_bash/interpreter/builtins/__init__.py +2 -0
  28. just_bash/interpreter/builtins/control.py +4 -8
  29. just_bash/interpreter/builtins/declare.py +321 -24
  30. just_bash/interpreter/builtins/getopts.py +163 -0
  31. just_bash/interpreter/builtins/let.py +2 -2
  32. just_bash/interpreter/builtins/local.py +71 -5
  33. just_bash/interpreter/builtins/misc.py +22 -6
  34. just_bash/interpreter/builtins/readonly.py +38 -10
  35. just_bash/interpreter/builtins/set.py +58 -8
  36. just_bash/interpreter/builtins/test.py +136 -19
  37. just_bash/interpreter/builtins/unset.py +62 -10
  38. just_bash/interpreter/conditionals.py +29 -4
  39. just_bash/interpreter/control_flow.py +61 -17
  40. just_bash/interpreter/expansion.py +1647 -104
  41. just_bash/interpreter/interpreter.py +436 -69
  42. just_bash/interpreter/types.py +263 -2
  43. just_bash/parser/__init__.py +2 -0
  44. just_bash/parser/lexer.py +295 -26
  45. just_bash/parser/parser.py +523 -64
  46. just_bash/types.py +11 -0
  47. {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/METADATA +40 -1
  48. {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/RECORD +49 -40
  49. {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
- Converts glob pattern to regex for matching.
167
+ Supports standard globs and extended glob patterns.
168
168
  """
169
- # Use fnmatch for glob-style matching
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 ctx.state.env
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 ctx.state.env
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
- params = ctx.state.env.get("@", "").split()
80
- words = [p for p in params if p]
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
- # 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
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
- 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
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