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
|
@@ -23,7 +23,7 @@ import re
|
|
|
23
23
|
from typing import TYPE_CHECKING, Optional
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from ..types import InterpreterContext
|
|
26
|
+
from ..types import InterpreterContext, VariableStore
|
|
27
27
|
from ...types import ExecResult
|
|
28
28
|
|
|
29
29
|
|
|
@@ -102,6 +102,10 @@ async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecRes
|
|
|
102
102
|
|
|
103
103
|
i += 1
|
|
104
104
|
|
|
105
|
+
# Handle -f or -F options (functions)
|
|
106
|
+
if options["function"] or options["func_names"]:
|
|
107
|
+
return _handle_functions(ctx, names, options)
|
|
108
|
+
|
|
105
109
|
# Print mode: show variable declarations
|
|
106
110
|
if options["print"]:
|
|
107
111
|
return _print_declarations(ctx, names, options)
|
|
@@ -137,6 +141,32 @@ async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecRes
|
|
|
137
141
|
exit_code = 1
|
|
138
142
|
continue
|
|
139
143
|
|
|
144
|
+
# Handle nameref declaration
|
|
145
|
+
from ..types import VariableStore
|
|
146
|
+
if options["nameref"] and isinstance(ctx.state.env, VariableStore):
|
|
147
|
+
if value_str is not None:
|
|
148
|
+
ctx.state.env.set_nameref(name, value_str)
|
|
149
|
+
else:
|
|
150
|
+
# declare -n without value just marks as nameref type
|
|
151
|
+
ctx.state.env.set_attribute(name, "n")
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
# Handle readonly attribute
|
|
155
|
+
if options["readonly"] and isinstance(ctx.state.env, VariableStore):
|
|
156
|
+
ctx.state.env.set_attribute(name, "r")
|
|
157
|
+
ctx.state.readonly_vars.add(name) if hasattr(ctx.state, 'readonly_vars') else None
|
|
158
|
+
if value_str is not None:
|
|
159
|
+
ctx.state.env[name] = value_str
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Handle export attribute
|
|
163
|
+
if options["export"]:
|
|
164
|
+
if isinstance(ctx.state.env, VariableStore):
|
|
165
|
+
ctx.state.env.set_attribute(name, "x")
|
|
166
|
+
if value_str is not None:
|
|
167
|
+
ctx.state.env[name] = value_str
|
|
168
|
+
continue
|
|
169
|
+
|
|
140
170
|
# Handle array declaration
|
|
141
171
|
if options["array"] or options["assoc"]:
|
|
142
172
|
# Initialize array if not already set
|
|
@@ -157,6 +187,16 @@ async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecRes
|
|
|
157
187
|
ctx.state.env[f"{name}_0"] = value_str
|
|
158
188
|
else:
|
|
159
189
|
# Regular variable
|
|
190
|
+
# Set attributes via metadata
|
|
191
|
+
from ..types import VariableStore
|
|
192
|
+
if isinstance(ctx.state.env, VariableStore):
|
|
193
|
+
if options["integer"]:
|
|
194
|
+
ctx.state.env.set_attribute(name, "i")
|
|
195
|
+
if options["lowercase"]:
|
|
196
|
+
ctx.state.env.set_attribute(name, "l")
|
|
197
|
+
if options["uppercase"]:
|
|
198
|
+
ctx.state.env.set_attribute(name, "u")
|
|
199
|
+
|
|
160
200
|
if value_str is not None:
|
|
161
201
|
# Apply transformations
|
|
162
202
|
if options["integer"]:
|
|
@@ -177,7 +217,7 @@ async def handle_declare(ctx: "InterpreterContext", args: list[str]) -> "ExecRes
|
|
|
177
217
|
else:
|
|
178
218
|
ctx.state.env[name] = value_str
|
|
179
219
|
elif name not in ctx.state.env:
|
|
180
|
-
# Declare without value - just set type info
|
|
220
|
+
# Declare without value - just set type info (legacy compat)
|
|
181
221
|
if options["integer"]:
|
|
182
222
|
ctx.state.env[f"{name}__is_integer"] = "1"
|
|
183
223
|
if options["lowercase"]:
|
|
@@ -271,8 +311,10 @@ def _parse_array_assignment(ctx: "InterpreterContext", name: str, inner: str, is
|
|
|
271
311
|
|
|
272
312
|
|
|
273
313
|
def _eval_integer(expr: str, ctx: "InterpreterContext") -> int:
|
|
274
|
-
"""Evaluate
|
|
275
|
-
|
|
314
|
+
"""Evaluate an integer expression for declare -i."""
|
|
315
|
+
from ..expansion import evaluate_arithmetic_sync
|
|
316
|
+
from ...parser.parser import Parser
|
|
317
|
+
|
|
276
318
|
expr = expr.strip()
|
|
277
319
|
|
|
278
320
|
# Try direct integer
|
|
@@ -289,48 +331,303 @@ def _eval_integer(expr: str, ctx: "InterpreterContext") -> int:
|
|
|
289
331
|
except ValueError:
|
|
290
332
|
return 0
|
|
291
333
|
|
|
334
|
+
# Try arithmetic expression evaluation
|
|
335
|
+
try:
|
|
336
|
+
parser = Parser()
|
|
337
|
+
arith_expr = parser._parse_arith_comma(expr)
|
|
338
|
+
return evaluate_arithmetic_sync(ctx, arith_expr)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
292
342
|
return 0
|
|
293
343
|
|
|
294
344
|
|
|
345
|
+
def _get_attr_flags(env, name: str, ctx: "InterpreterContext") -> str:
|
|
346
|
+
"""Get the declare attribute flags for a variable."""
|
|
347
|
+
from ..types import VariableStore
|
|
348
|
+
flags = ""
|
|
349
|
+
if isinstance(env, VariableStore):
|
|
350
|
+
attrs = env.get_attributes(name)
|
|
351
|
+
for attr in "rxilunt":
|
|
352
|
+
if attr in attrs:
|
|
353
|
+
flags += attr
|
|
354
|
+
elif name in ctx.state.readonly_vars:
|
|
355
|
+
flags += "r"
|
|
356
|
+
# Check array type
|
|
357
|
+
is_array = env.get(f"{name}__is_array")
|
|
358
|
+
if is_array == "assoc":
|
|
359
|
+
flags += "A"
|
|
360
|
+
elif is_array:
|
|
361
|
+
flags += "a"
|
|
362
|
+
return flags
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _format_array_decl(env, name: str, flags: str) -> str:
|
|
366
|
+
"""Format an array variable declaration."""
|
|
367
|
+
is_assoc = env.get(f"{name}__is_array") == "assoc"
|
|
368
|
+
prefix = f"{name}_"
|
|
369
|
+
elements = []
|
|
370
|
+
for key in sorted(env.keys()):
|
|
371
|
+
if key.startswith(prefix) and not key.startswith(f"{name}__"):
|
|
372
|
+
idx_part = key[len(prefix):]
|
|
373
|
+
val = env[key]
|
|
374
|
+
elements.append((idx_part, val))
|
|
375
|
+
|
|
376
|
+
if not is_assoc:
|
|
377
|
+
# Sort indexed arrays by numeric index
|
|
378
|
+
try:
|
|
379
|
+
elements.sort(key=lambda x: int(x[0]))
|
|
380
|
+
except ValueError:
|
|
381
|
+
pass
|
|
382
|
+
|
|
383
|
+
flag_str = f"-{flags}" if flags else "-a"
|
|
384
|
+
pairs = " ".join(f'[{idx}]="{val}"' for idx, val in elements)
|
|
385
|
+
if elements:
|
|
386
|
+
return f"declare {flag_str} {name}=({pairs})"
|
|
387
|
+
else:
|
|
388
|
+
return f"declare {flag_str} {name}=()"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _format_var_decl(env, name: str, ctx: "InterpreterContext") -> str | None:
|
|
392
|
+
"""Format a single variable declaration. Returns None if not found."""
|
|
393
|
+
from ..types import VariableStore
|
|
394
|
+
flags = _get_attr_flags(env, name, ctx)
|
|
395
|
+
|
|
396
|
+
# Check for nameref
|
|
397
|
+
if isinstance(env, VariableStore) and env.is_nameref(name):
|
|
398
|
+
meta = env._metadata.get(name)
|
|
399
|
+
target = meta.nameref_target if meta else ""
|
|
400
|
+
flag_str = f"-{flags}" if flags else "-n"
|
|
401
|
+
return f'declare {flag_str} {name}="{target}"'
|
|
402
|
+
|
|
403
|
+
# Check for array
|
|
404
|
+
is_array = env.get(f"{name}__is_array")
|
|
405
|
+
if is_array:
|
|
406
|
+
return _format_array_decl(env, name, flags)
|
|
407
|
+
|
|
408
|
+
# Regular variable
|
|
409
|
+
if name in env:
|
|
410
|
+
val = env[name]
|
|
411
|
+
flag_str = f"-{flags}" if flags else "--"
|
|
412
|
+
return f'declare {flag_str} {name}="{val}"'
|
|
413
|
+
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
295
417
|
def _print_declarations(ctx: "InterpreterContext", names: list[str], options: dict) -> "ExecResult":
|
|
296
418
|
"""Print variable declarations."""
|
|
297
419
|
lines = []
|
|
420
|
+
env = ctx.state.env
|
|
421
|
+
exit_code = 0
|
|
298
422
|
|
|
299
423
|
if not names:
|
|
300
|
-
# Print all
|
|
301
|
-
|
|
302
|
-
|
|
424
|
+
# Print all variables
|
|
425
|
+
# Collect unique variable names (skip internal keys)
|
|
426
|
+
seen = set()
|
|
427
|
+
for key in sorted(env.keys()):
|
|
428
|
+
if "__" in key:
|
|
429
|
+
# Extract base name from array keys like arr_0, arr__is_array
|
|
430
|
+
continue
|
|
431
|
+
if key.startswith("_"):
|
|
432
|
+
continue
|
|
433
|
+
if key in ("?", "#", "$", "!", "-", "*", "@"):
|
|
303
434
|
continue
|
|
304
|
-
|
|
435
|
+
# Skip positional params and single-digit keys
|
|
436
|
+
if key.isdigit():
|
|
305
437
|
continue
|
|
306
438
|
|
|
307
|
-
|
|
308
|
-
|
|
439
|
+
seen.add(key)
|
|
440
|
+
|
|
441
|
+
# Also add arrays
|
|
442
|
+
for key in env.keys():
|
|
443
|
+
if key.endswith("__is_array"):
|
|
444
|
+
arr_name = key[:-len("__is_array")]
|
|
445
|
+
if not arr_name.startswith("_"):
|
|
446
|
+
seen.add(arr_name)
|
|
447
|
+
|
|
448
|
+
for name in sorted(seen):
|
|
449
|
+
decl = _format_var_decl(env, name, ctx)
|
|
450
|
+
if decl:
|
|
451
|
+
lines.append(decl)
|
|
309
452
|
else:
|
|
310
453
|
for name in names:
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
lines.append(
|
|
454
|
+
decl = _format_var_decl(env, name, ctx)
|
|
455
|
+
if decl:
|
|
456
|
+
lines.append(decl)
|
|
314
457
|
else:
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
lines.append(f'declare -{("A" if is_array == "assoc" else "a")} {name}')
|
|
458
|
+
# Variable not found
|
|
459
|
+
lines_err = f"bash: declare: {name}: not found\n"
|
|
460
|
+
exit_code = 1
|
|
319
461
|
|
|
320
|
-
|
|
462
|
+
stdout = "\n".join(lines) + "\n" if lines else ""
|
|
463
|
+
stderr = ""
|
|
464
|
+
if exit_code != 0 and not lines:
|
|
465
|
+
stderr = f"bash: declare: {names[0]}: not found\n" if names else ""
|
|
466
|
+
return _result(stdout, stderr, exit_code)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _handle_functions(ctx: "InterpreterContext", names: list[str], options: dict) -> "ExecResult":
|
|
470
|
+
"""Handle declare -f (list function bodies) and -F (list function names)."""
|
|
471
|
+
functions = ctx.state.functions
|
|
472
|
+
lines = []
|
|
473
|
+
exit_code = 0
|
|
474
|
+
|
|
475
|
+
if options["func_names"]:
|
|
476
|
+
# -F: list function names only
|
|
477
|
+
if names:
|
|
478
|
+
for name in names:
|
|
479
|
+
if name in functions:
|
|
480
|
+
lines.append(f"declare -f {name}")
|
|
481
|
+
else:
|
|
482
|
+
exit_code = 1
|
|
483
|
+
else:
|
|
484
|
+
for name in sorted(functions.keys()):
|
|
485
|
+
lines.append(f"declare -f {name}")
|
|
486
|
+
else:
|
|
487
|
+
# -f: list function definitions
|
|
488
|
+
if names:
|
|
489
|
+
for name in names:
|
|
490
|
+
if name in functions:
|
|
491
|
+
lines.append(_format_function_def(name, functions[name]))
|
|
492
|
+
else:
|
|
493
|
+
exit_code = 1
|
|
494
|
+
else:
|
|
495
|
+
for name in sorted(functions.keys()):
|
|
496
|
+
lines.append(_format_function_def(name, functions[name]))
|
|
497
|
+
|
|
498
|
+
# If also -p, treat as print mode for functions
|
|
499
|
+
if options["print"] and not options["func_names"]:
|
|
500
|
+
# declare -fp: same as declare -f
|
|
501
|
+
pass
|
|
502
|
+
|
|
503
|
+
stdout = "\n".join(lines) + "\n" if lines else ""
|
|
504
|
+
return _result(stdout, "", exit_code)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _format_function_def(name: str, func_node) -> str:
|
|
508
|
+
"""Format a function definition for output."""
|
|
509
|
+
# Try to reconstruct the function body from the AST
|
|
510
|
+
# For simplicity, output a placeholder that matches bash format
|
|
511
|
+
body = _ast_to_source(func_node.body) if func_node.body else " :"
|
|
512
|
+
return f"{name} () \n{{\n{body}\n}}"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _ast_to_source(node) -> str:
|
|
516
|
+
"""Best-effort AST to source code conversion."""
|
|
517
|
+
# This is a simplified reconstruction - bash's declare -f does perfect roundtrip
|
|
518
|
+
# but we can't easily reconstruct from AST without storing the original source
|
|
519
|
+
from ...ast.types import GroupNode, SimpleCommandNode, StatementNode
|
|
520
|
+
parts = []
|
|
521
|
+
|
|
522
|
+
body_stmts = []
|
|
523
|
+
if hasattr(node, 'body'):
|
|
524
|
+
body_stmts = node.body
|
|
525
|
+
elif hasattr(node, 'statements'):
|
|
526
|
+
body_stmts = node.statements
|
|
527
|
+
|
|
528
|
+
if not body_stmts:
|
|
529
|
+
return " :"
|
|
530
|
+
|
|
531
|
+
for stmt in body_stmts:
|
|
532
|
+
line = _stmt_to_source(stmt)
|
|
533
|
+
if line:
|
|
534
|
+
parts.append(f" {line}")
|
|
535
|
+
|
|
536
|
+
return "\n".join(parts) if parts else " :"
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _stmt_to_source(node) -> str:
|
|
540
|
+
"""Convert a statement node to source."""
|
|
541
|
+
from ...ast.types import SimpleCommandNode
|
|
542
|
+
if hasattr(node, 'pipelines'):
|
|
543
|
+
cmd_parts = []
|
|
544
|
+
for i, pipeline in enumerate(node.pipelines):
|
|
545
|
+
if i > 0 and i - 1 < len(node.operators):
|
|
546
|
+
cmd_parts.append(node.operators[i - 1])
|
|
547
|
+
pipeline_str = _pipeline_to_source(pipeline)
|
|
548
|
+
cmd_parts.append(pipeline_str)
|
|
549
|
+
return " ".join(cmd_parts)
|
|
550
|
+
return ""
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _pipeline_to_source(node) -> str:
|
|
554
|
+
"""Convert a pipeline node to source."""
|
|
555
|
+
parts = []
|
|
556
|
+
for i, cmd in enumerate(node.commands):
|
|
557
|
+
if i > 0:
|
|
558
|
+
parts.append("|")
|
|
559
|
+
parts.append(_cmd_to_source(cmd))
|
|
560
|
+
result = " ".join(parts)
|
|
561
|
+
if node.negated:
|
|
562
|
+
result = f"! {result}"
|
|
563
|
+
return result
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _cmd_to_source(node) -> str:
|
|
567
|
+
"""Convert a command node to source."""
|
|
568
|
+
from ...ast.types import SimpleCommandNode
|
|
569
|
+
if isinstance(node, SimpleCommandNode) or (hasattr(node, 'type') and node.type == "SimpleCommand"):
|
|
570
|
+
parts = []
|
|
571
|
+
if node.name:
|
|
572
|
+
parts.append(_word_to_source(node.name))
|
|
573
|
+
for arg in node.args:
|
|
574
|
+
parts.append(_word_to_source(arg))
|
|
575
|
+
return " ".join(parts)
|
|
576
|
+
return ""
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _word_to_source(word) -> str:
|
|
580
|
+
"""Convert a word node to approximate source."""
|
|
581
|
+
parts = []
|
|
582
|
+
for part in word.parts:
|
|
583
|
+
if hasattr(part, 'value'):
|
|
584
|
+
parts.append(part.value)
|
|
585
|
+
elif hasattr(part, 'parts'):
|
|
586
|
+
inner = ""
|
|
587
|
+
for p in part.parts:
|
|
588
|
+
if hasattr(p, 'value'):
|
|
589
|
+
inner += p.value
|
|
590
|
+
elif hasattr(p, 'parameter'):
|
|
591
|
+
inner += f"${p.parameter}"
|
|
592
|
+
if part.type == "DoubleQuoted":
|
|
593
|
+
parts.append(f'"{inner}"')
|
|
594
|
+
elif part.type == "SingleQuoted":
|
|
595
|
+
parts.append(f"'{inner}'")
|
|
596
|
+
else:
|
|
597
|
+
parts.append(inner)
|
|
598
|
+
elif hasattr(part, 'parameter'):
|
|
599
|
+
parts.append(f"${part.parameter}")
|
|
600
|
+
return "".join(parts)
|
|
321
601
|
|
|
322
602
|
|
|
323
603
|
def _list_variables(ctx: "InterpreterContext", options: dict) -> "ExecResult":
|
|
324
604
|
"""List variables with matching attributes."""
|
|
605
|
+
env = ctx.state.env
|
|
325
606
|
lines = []
|
|
326
607
|
|
|
327
|
-
|
|
328
|
-
|
|
608
|
+
# Collect unique variable names
|
|
609
|
+
seen = set()
|
|
610
|
+
for key in sorted(env.keys()):
|
|
611
|
+
if "__" in key:
|
|
329
612
|
continue
|
|
330
|
-
if
|
|
613
|
+
if key.startswith("_"):
|
|
331
614
|
continue
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
615
|
+
if key in ("?", "#", "$", "!", "-", "*", "@"):
|
|
616
|
+
continue
|
|
617
|
+
if key.isdigit():
|
|
618
|
+
continue
|
|
619
|
+
seen.add(key)
|
|
620
|
+
|
|
621
|
+
# Also add arrays
|
|
622
|
+
for key in env.keys():
|
|
623
|
+
if key.endswith("__is_array"):
|
|
624
|
+
arr_name = key[:-len("__is_array")]
|
|
625
|
+
if not arr_name.startswith("_"):
|
|
626
|
+
seen.add(arr_name)
|
|
627
|
+
|
|
628
|
+
for name in sorted(seen):
|
|
629
|
+
decl = _format_var_decl(env, name, ctx)
|
|
630
|
+
if decl:
|
|
631
|
+
lines.append(decl)
|
|
335
632
|
|
|
336
633
|
return _result("\n".join(lines) + "\n" if lines else "", "", 0)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Getopts builtin implementation.
|
|
2
|
+
|
|
3
|
+
Usage: getopts optstring name [args]
|
|
4
|
+
|
|
5
|
+
Parse positional parameters as options.
|
|
6
|
+
|
|
7
|
+
The optstring contains the option letters to be recognized.
|
|
8
|
+
If a letter is followed by a colon, the option requires an argument.
|
|
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
|
+
async def handle_getopts(ctx: "InterpreterContext", args: list[str]) -> "ExecResult":
|
|
19
|
+
"""Execute the getopts builtin."""
|
|
20
|
+
from ...types import ExecResult
|
|
21
|
+
|
|
22
|
+
if len(args) < 2:
|
|
23
|
+
return ExecResult(
|
|
24
|
+
stdout="",
|
|
25
|
+
stderr="bash: getopts: usage: getopts optstring name [arg ...]\n",
|
|
26
|
+
exit_code=2,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
optstring = args[0]
|
|
30
|
+
name = args[1]
|
|
31
|
+
|
|
32
|
+
# Validate variable name
|
|
33
|
+
import re
|
|
34
|
+
valid_name = bool(re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name))
|
|
35
|
+
|
|
36
|
+
# Get the arguments to parse (from args[2:] or positional params)
|
|
37
|
+
if len(args) > 2:
|
|
38
|
+
parse_args = args[2:]
|
|
39
|
+
else:
|
|
40
|
+
# Use positional parameters
|
|
41
|
+
parse_args = []
|
|
42
|
+
i = 1
|
|
43
|
+
while str(i) in ctx.state.env:
|
|
44
|
+
parse_args.append(ctx.state.env[str(i)])
|
|
45
|
+
i += 1
|
|
46
|
+
|
|
47
|
+
# Get current OPTIND (1-based)
|
|
48
|
+
try:
|
|
49
|
+
optind = int(ctx.state.env.get("OPTIND", "1"))
|
|
50
|
+
except ValueError:
|
|
51
|
+
optind = 1
|
|
52
|
+
|
|
53
|
+
# Check if we've exhausted all arguments
|
|
54
|
+
if optind > len(parse_args):
|
|
55
|
+
ctx.state.env[name] = "?"
|
|
56
|
+
return ExecResult(stdout="", stderr="", exit_code=1)
|
|
57
|
+
|
|
58
|
+
current_arg = parse_args[optind - 1]
|
|
59
|
+
|
|
60
|
+
# Check if current arg is an option
|
|
61
|
+
if not current_arg.startswith("-") or current_arg == "-" or current_arg == "--":
|
|
62
|
+
ctx.state.env[name] = "?"
|
|
63
|
+
if current_arg == "--":
|
|
64
|
+
ctx.state.env["OPTIND"] = str(optind + 1)
|
|
65
|
+
return ExecResult(stdout="", stderr="", exit_code=1)
|
|
66
|
+
|
|
67
|
+
# Get the option character
|
|
68
|
+
# Handle multi-character processing within a single -abc argument
|
|
69
|
+
optchar_idx_key = "__getopts_charpos"
|
|
70
|
+
try:
|
|
71
|
+
charpos = int(ctx.state.env.get(optchar_idx_key, "1"))
|
|
72
|
+
except ValueError:
|
|
73
|
+
charpos = 1
|
|
74
|
+
|
|
75
|
+
if charpos >= len(current_arg):
|
|
76
|
+
# Move to next argument
|
|
77
|
+
optind += 1
|
|
78
|
+
charpos = 1
|
|
79
|
+
if optind > len(parse_args):
|
|
80
|
+
ctx.state.env[name] = "?"
|
|
81
|
+
ctx.state.env["OPTIND"] = str(optind)
|
|
82
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
83
|
+
return ExecResult(stdout="", stderr="", exit_code=1)
|
|
84
|
+
current_arg = parse_args[optind - 1]
|
|
85
|
+
if not current_arg.startswith("-") or current_arg == "-" or current_arg == "--":
|
|
86
|
+
ctx.state.env[name] = "?"
|
|
87
|
+
ctx.state.env["OPTIND"] = str(optind + (1 if current_arg == "--" else 0))
|
|
88
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
89
|
+
return ExecResult(stdout="", stderr="", exit_code=1)
|
|
90
|
+
|
|
91
|
+
opt_char = current_arg[charpos]
|
|
92
|
+
|
|
93
|
+
# Check if this option is valid
|
|
94
|
+
opt_idx = optstring.find(opt_char)
|
|
95
|
+
stderr = ""
|
|
96
|
+
|
|
97
|
+
if opt_idx == -1:
|
|
98
|
+
# Invalid option
|
|
99
|
+
ctx.state.env[name] = "?"
|
|
100
|
+
if optstring.startswith(":"):
|
|
101
|
+
# Silent mode: OPTARG = the invalid option character
|
|
102
|
+
ctx.state.env["OPTARG"] = opt_char
|
|
103
|
+
else:
|
|
104
|
+
# Normal mode: OPTARG is unset, print error
|
|
105
|
+
ctx.state.env.pop("OPTARG", None)
|
|
106
|
+
stderr = f"bash: getopts: illegal option -- {opt_char}\n"
|
|
107
|
+
|
|
108
|
+
# Advance position
|
|
109
|
+
if charpos + 1 < len(current_arg):
|
|
110
|
+
ctx.state.env[optchar_idx_key] = str(charpos + 1)
|
|
111
|
+
else:
|
|
112
|
+
ctx.state.env["OPTIND"] = str(optind + 1)
|
|
113
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
114
|
+
|
|
115
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=0)
|
|
116
|
+
|
|
117
|
+
# Valid option - check if it requires an argument
|
|
118
|
+
needs_arg = opt_idx + 1 < len(optstring) and optstring[opt_idx + 1] == ":"
|
|
119
|
+
|
|
120
|
+
if needs_arg:
|
|
121
|
+
# Check for argument
|
|
122
|
+
if charpos + 1 < len(current_arg):
|
|
123
|
+
# Argument is the rest of this token
|
|
124
|
+
ctx.state.env["OPTARG"] = current_arg[charpos + 1:]
|
|
125
|
+
ctx.state.env["OPTIND"] = str(optind + 1)
|
|
126
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
127
|
+
elif optind < len(parse_args):
|
|
128
|
+
# Argument is the next token
|
|
129
|
+
ctx.state.env["OPTARG"] = parse_args[optind]
|
|
130
|
+
ctx.state.env["OPTIND"] = str(optind + 2)
|
|
131
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
132
|
+
else:
|
|
133
|
+
# Missing argument
|
|
134
|
+
if optstring.startswith(":"):
|
|
135
|
+
ctx.state.env[name] = ":"
|
|
136
|
+
ctx.state.env["OPTARG"] = opt_char
|
|
137
|
+
else:
|
|
138
|
+
ctx.state.env[name] = "?"
|
|
139
|
+
ctx.state.env.pop("OPTARG", None)
|
|
140
|
+
stderr = f"bash: getopts: option requires an argument -- {opt_char}\n"
|
|
141
|
+
ctx.state.env["OPTIND"] = str(optind + 1)
|
|
142
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
143
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=0)
|
|
144
|
+
else:
|
|
145
|
+
# No argument needed
|
|
146
|
+
ctx.state.env.pop("OPTARG", None)
|
|
147
|
+
if charpos + 1 < len(current_arg):
|
|
148
|
+
# More options in this token
|
|
149
|
+
ctx.state.env[optchar_idx_key] = str(charpos + 1)
|
|
150
|
+
else:
|
|
151
|
+
# Move to next argument
|
|
152
|
+
ctx.state.env["OPTIND"] = str(optind + 1)
|
|
153
|
+
ctx.state.env.pop(optchar_idx_key, None)
|
|
154
|
+
|
|
155
|
+
if valid_name:
|
|
156
|
+
ctx.state.env[name] = opt_char
|
|
157
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=0)
|
|
158
|
+
else:
|
|
159
|
+
return ExecResult(
|
|
160
|
+
stdout="",
|
|
161
|
+
stderr=stderr + f"bash: getopts: `{name}': not a valid identifier\n",
|
|
162
|
+
exit_code=1,
|
|
163
|
+
)
|
|
@@ -23,7 +23,7 @@ async def handle_let(
|
|
|
23
23
|
ctx: "InterpreterContext", args: list[str]
|
|
24
24
|
) -> "ExecResult":
|
|
25
25
|
"""Execute the let builtin."""
|
|
26
|
-
from ..expansion import
|
|
26
|
+
from ..expansion import evaluate_arithmetic
|
|
27
27
|
from ...parser.parser import Parser
|
|
28
28
|
|
|
29
29
|
if not args:
|
|
@@ -36,7 +36,7 @@ async def handle_let(
|
|
|
36
36
|
try:
|
|
37
37
|
# Parse and evaluate the arithmetic expression
|
|
38
38
|
arith_expr = parser._parse_arithmetic_expression(expr_str)
|
|
39
|
-
last_value =
|
|
39
|
+
last_value = await evaluate_arithmetic(ctx, arith_expr)
|
|
40
40
|
except Exception as e:
|
|
41
41
|
return _result("", f"bash: let: {expr_str}: syntax error\n", 1)
|
|
42
42
|
|