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
just_bash/ast/factory.py
CHANGED
|
@@ -78,6 +78,7 @@ def simple_command(
|
|
|
78
78
|
args: Sequence[WordNode] | None = None,
|
|
79
79
|
assignments: Sequence[AssignmentNode] | None = None,
|
|
80
80
|
redirections: Sequence[RedirectionNode] | None = None,
|
|
81
|
+
line: int | None = None,
|
|
81
82
|
) -> SimpleCommandNode:
|
|
82
83
|
"""Create a simple command node."""
|
|
83
84
|
return SimpleCommandNode(
|
|
@@ -85,6 +86,7 @@ def simple_command(
|
|
|
85
86
|
args=tuple(args) if args else (),
|
|
86
87
|
assignments=tuple(assignments) if assignments else (),
|
|
87
88
|
redirections=tuple(redirections) if redirections else (),
|
|
89
|
+
line=line,
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
|
|
@@ -205,7 +207,7 @@ def for_node(
|
|
|
205
207
|
"""Create a for node."""
|
|
206
208
|
return ForNode(
|
|
207
209
|
variable=variable,
|
|
208
|
-
words=tuple(words) if words else None,
|
|
210
|
+
words=tuple(words) if words is not None else None,
|
|
209
211
|
body=tuple(body),
|
|
210
212
|
redirections=tuple(redirections) if redirections else (),
|
|
211
213
|
)
|
just_bash/bash.py
CHANGED
|
@@ -28,8 +28,8 @@ import nest_asyncio # type: ignore[import-untyped]
|
|
|
28
28
|
|
|
29
29
|
from .commands import create_command_registry
|
|
30
30
|
from .fs import InMemoryFs
|
|
31
|
-
from .interpreter import Interpreter, InterpreterState, ShellOptions
|
|
32
|
-
from .parser import parse
|
|
31
|
+
from .interpreter import ExitError, Interpreter, InterpreterState, ShellOptions, VariableStore
|
|
32
|
+
from .parser import parse, unescape_html_entities
|
|
33
33
|
from .types import (
|
|
34
34
|
Command,
|
|
35
35
|
ExecResult,
|
|
@@ -59,6 +59,7 @@ class Bash:
|
|
|
59
59
|
errexit: bool = False,
|
|
60
60
|
pipefail: bool = False,
|
|
61
61
|
nounset: bool = False,
|
|
62
|
+
unescape_html: bool = True,
|
|
62
63
|
):
|
|
63
64
|
"""Initialize the Bash interpreter.
|
|
64
65
|
|
|
@@ -73,6 +74,9 @@ class Bash:
|
|
|
73
74
|
errexit: Enable errexit (set -e) mode.
|
|
74
75
|
pipefail: Enable pipefail mode.
|
|
75
76
|
nounset: Enable nounset (set -u) mode.
|
|
77
|
+
unescape_html: Unescape HTML entities in operator positions (default True).
|
|
78
|
+
This helps LLM-generated commands work correctly when they contain
|
|
79
|
+
< instead of <, > instead of >, etc.
|
|
76
80
|
"""
|
|
77
81
|
# Set up filesystem
|
|
78
82
|
if fs is not None:
|
|
@@ -89,15 +93,21 @@ class Bash:
|
|
|
89
93
|
# Set up network config
|
|
90
94
|
self._network = network
|
|
91
95
|
|
|
96
|
+
# Set up HTML unescaping
|
|
97
|
+
self._unescape_html = unescape_html
|
|
98
|
+
|
|
92
99
|
# Set up initial state
|
|
93
|
-
default_env = {
|
|
100
|
+
default_env = VariableStore({
|
|
94
101
|
"PATH": "/usr/local/bin:/usr/bin:/bin",
|
|
95
102
|
"HOME": "/home/user",
|
|
96
103
|
"USER": "user",
|
|
97
104
|
"SHELL": "/bin/bash",
|
|
98
105
|
"PWD": cwd,
|
|
99
106
|
"?": "0",
|
|
100
|
-
|
|
107
|
+
"SHLVL": "1",
|
|
108
|
+
"BASH_VERSION": "5.0.0(1)-release",
|
|
109
|
+
"OPTIND": "1",
|
|
110
|
+
})
|
|
101
111
|
if env:
|
|
102
112
|
default_env.update(env)
|
|
103
113
|
|
|
@@ -152,6 +162,10 @@ class Bash:
|
|
|
152
162
|
Returns:
|
|
153
163
|
ExecResult with stdout, stderr, exit_code, and final env.
|
|
154
164
|
"""
|
|
165
|
+
# Preprocess HTML entities if enabled
|
|
166
|
+
if self._unescape_html:
|
|
167
|
+
script = unescape_html_entities(script)
|
|
168
|
+
|
|
155
169
|
# Parse the script
|
|
156
170
|
ast = parse(script)
|
|
157
171
|
|
|
@@ -163,7 +177,15 @@ class Bash:
|
|
|
163
177
|
self._interpreter.state.env["PWD"] = cwd
|
|
164
178
|
|
|
165
179
|
# Execute
|
|
166
|
-
|
|
180
|
+
try:
|
|
181
|
+
return await self._interpreter.execute_script(ast)
|
|
182
|
+
except ExitError as error:
|
|
183
|
+
return ExecResult(
|
|
184
|
+
stdout=error.stdout,
|
|
185
|
+
stderr=error.stderr,
|
|
186
|
+
exit_code=error.exit_code,
|
|
187
|
+
env=dict(self._interpreter.state.env),
|
|
188
|
+
)
|
|
167
189
|
|
|
168
190
|
def run(
|
|
169
191
|
self,
|
|
@@ -208,7 +230,7 @@ class Bash:
|
|
|
208
230
|
commands=self._commands,
|
|
209
231
|
limits=self._limits,
|
|
210
232
|
state=InterpreterState(
|
|
211
|
-
env=
|
|
233
|
+
env=self._initial_state.env.copy() if isinstance(self._initial_state.env, VariableStore) else VariableStore(self._initial_state.env),
|
|
212
234
|
cwd=self._initial_state.cwd,
|
|
213
235
|
previous_dir=self._initial_state.previous_dir,
|
|
214
236
|
options=ShellOptions(
|
just_bash/commands/awk/awk.py
CHANGED
|
@@ -22,6 +22,8 @@ Variables:
|
|
|
22
22
|
FS field separator
|
|
23
23
|
OFS output field separator
|
|
24
24
|
ORS output record separator
|
|
25
|
+
RSTART start of match (set by match())
|
|
26
|
+
RLENGTH length of match (set by match())
|
|
25
27
|
|
|
26
28
|
Built-in functions:
|
|
27
29
|
length(s) string length
|
|
@@ -30,13 +32,35 @@ Built-in functions:
|
|
|
30
32
|
split(s,a,fs) split s into array a
|
|
31
33
|
sub(r,s) substitute first match
|
|
32
34
|
gsub(r,s) substitute all matches
|
|
35
|
+
match(s,r) find regex r in s, set RSTART/RLENGTH
|
|
33
36
|
tolower(s) convert to lowercase
|
|
34
37
|
toupper(s) convert to uppercase
|
|
35
|
-
|
|
38
|
+
sprintf(fmt,args...) return formatted string
|
|
39
|
+
printf(fmt,args...) formatted print
|
|
36
40
|
print print current line
|
|
41
|
+
|
|
42
|
+
Math functions:
|
|
43
|
+
int(x) truncate to integer
|
|
44
|
+
sqrt(x) square root
|
|
45
|
+
sin(x) sine
|
|
46
|
+
cos(x) cosine
|
|
47
|
+
log(x) natural logarithm
|
|
48
|
+
exp(x) exponential
|
|
49
|
+
atan2(y,x) arctangent of y/x
|
|
50
|
+
|
|
51
|
+
Random functions:
|
|
52
|
+
rand() random number 0 <= n < 1
|
|
53
|
+
srand([seed]) seed random generator
|
|
54
|
+
|
|
55
|
+
Time functions:
|
|
56
|
+
systime() current epoch timestamp
|
|
57
|
+
strftime(fmt,ts) format timestamp
|
|
37
58
|
"""
|
|
38
59
|
|
|
60
|
+
import math
|
|
61
|
+
import random
|
|
39
62
|
import re
|
|
63
|
+
import time
|
|
40
64
|
from dataclasses import dataclass, field
|
|
41
65
|
from typing import Any
|
|
42
66
|
from ...types import CommandContext, ExecResult
|
|
@@ -50,6 +74,7 @@ class AwkRule:
|
|
|
50
74
|
action: str
|
|
51
75
|
is_regex: bool = False
|
|
52
76
|
regex: re.Pattern | None = None
|
|
77
|
+
negate: bool = False # ! pattern negation
|
|
53
78
|
|
|
54
79
|
|
|
55
80
|
@dataclass
|
|
@@ -60,6 +85,7 @@ class AwkState:
|
|
|
60
85
|
output: str = ""
|
|
61
86
|
next_record: bool = False
|
|
62
87
|
exit_program: bool = False
|
|
88
|
+
rng: random.Random = field(default_factory=random.Random)
|
|
63
89
|
|
|
64
90
|
|
|
65
91
|
class AwkCommand:
|
|
@@ -262,12 +288,27 @@ class AwkCommand:
|
|
|
262
288
|
is_regex = False
|
|
263
289
|
regex = None
|
|
264
290
|
|
|
291
|
+
negate_pattern = False
|
|
292
|
+
|
|
265
293
|
if program[pos:].startswith("BEGIN"):
|
|
266
294
|
pattern = "BEGIN"
|
|
267
295
|
pos += 5
|
|
268
296
|
elif program[pos:].startswith("END"):
|
|
269
297
|
pattern = "END"
|
|
270
298
|
pos += 3
|
|
299
|
+
elif program[pos] == "!" and pos + 1 < len(program) and program[pos + 1] == "/":
|
|
300
|
+
# Negated regex pattern
|
|
301
|
+
negate_pattern = True
|
|
302
|
+
pos += 1 # skip '!', now pos is at '/'
|
|
303
|
+
end = self._find_regex_end(program, pos + 1)
|
|
304
|
+
if end != -1:
|
|
305
|
+
pattern = program[pos + 1:end]
|
|
306
|
+
is_regex = True
|
|
307
|
+
try:
|
|
308
|
+
regex = re.compile(pattern)
|
|
309
|
+
except re.error as e:
|
|
310
|
+
raise ValueError(f"invalid regex: {e}")
|
|
311
|
+
pos = end + 1
|
|
271
312
|
elif program[pos] == "/":
|
|
272
313
|
# Regex pattern
|
|
273
314
|
end = self._find_regex_end(program, pos + 1)
|
|
@@ -324,10 +365,10 @@ class AwkCommand:
|
|
|
324
365
|
pos += 1
|
|
325
366
|
|
|
326
367
|
action = program[start:pos - 1].strip()
|
|
327
|
-
rules.append(AwkRule(pattern=pattern, action=action, is_regex=is_regex, regex=regex))
|
|
368
|
+
rules.append(AwkRule(pattern=pattern, action=action, is_regex=is_regex, regex=regex, negate=negate_pattern))
|
|
328
369
|
else:
|
|
329
370
|
# Default action is print $0
|
|
330
|
-
rules.append(AwkRule(pattern=pattern, action="print", is_regex=is_regex, regex=regex))
|
|
371
|
+
rules.append(AwkRule(pattern=pattern, action="print", is_regex=is_regex, regex=regex, negate=negate_pattern))
|
|
331
372
|
|
|
332
373
|
return rules
|
|
333
374
|
|
|
@@ -349,7 +390,8 @@ class AwkCommand:
|
|
|
349
390
|
return True
|
|
350
391
|
|
|
351
392
|
if rule.is_regex and rule.regex:
|
|
352
|
-
|
|
393
|
+
result = bool(rule.regex.search(line))
|
|
394
|
+
return not result if rule.negate else result
|
|
353
395
|
|
|
354
396
|
# Expression pattern
|
|
355
397
|
pattern = rule.pattern
|
|
@@ -468,7 +510,15 @@ class AwkCommand:
|
|
|
468
510
|
if current.strip():
|
|
469
511
|
statements.append(current.strip())
|
|
470
512
|
|
|
471
|
-
|
|
513
|
+
# Merge 'else' parts back into their preceding 'if' statement
|
|
514
|
+
merged = []
|
|
515
|
+
for stmt in statements:
|
|
516
|
+
if stmt.startswith("else") and merged and merged[-1].lstrip().startswith("if"):
|
|
517
|
+
merged[-1] = merged[-1] + "; " + stmt
|
|
518
|
+
else:
|
|
519
|
+
merged.append(stmt)
|
|
520
|
+
|
|
521
|
+
return merged
|
|
472
522
|
|
|
473
523
|
def _execute_statement(
|
|
474
524
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
@@ -596,6 +646,28 @@ class AwkCommand:
|
|
|
596
646
|
pass
|
|
597
647
|
return
|
|
598
648
|
|
|
649
|
+
# Handle delete statement
|
|
650
|
+
if stmt.startswith("delete "):
|
|
651
|
+
target = stmt[7:].strip()
|
|
652
|
+
match = re.match(r"(\w+)\[(.+)\]", target)
|
|
653
|
+
if match:
|
|
654
|
+
arr_name = match.group(1)
|
|
655
|
+
idx = self._eval_expr(match.group(2).strip(), state, line, fields)
|
|
656
|
+
key = f"{arr_name}[{idx}]"
|
|
657
|
+
state.variables.pop(key, None)
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
# Handle match() as a statement (for side effects on RSTART/RLENGTH)
|
|
661
|
+
if stmt.startswith("match("):
|
|
662
|
+
# Just evaluate it - _eval_expr will set RSTART/RLENGTH
|
|
663
|
+
self._eval_expr(stmt, state, line, fields)
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
# Handle srand() as a statement
|
|
667
|
+
if stmt.startswith("srand(") or stmt == "srand()":
|
|
668
|
+
self._eval_expr(stmt, state, line, fields)
|
|
669
|
+
return
|
|
670
|
+
|
|
599
671
|
# Handle assignment
|
|
600
672
|
if "=" in stmt and not stmt.startswith("if") and "==" not in stmt and "!=" not in stmt:
|
|
601
673
|
# Handle += -= *= /=
|
|
@@ -621,6 +693,16 @@ class AwkCommand:
|
|
|
621
693
|
state.variables[var] = current / val if val != 0 else 0
|
|
622
694
|
return
|
|
623
695
|
|
|
696
|
+
# Array element assignment: arr[idx] = val
|
|
697
|
+
match = re.match(r"(\w+)\[(.+?)\]\s*=\s*(.+)", stmt)
|
|
698
|
+
if match:
|
|
699
|
+
arr_name = match.group(1)
|
|
700
|
+
idx = self._eval_expr(match.group(2).strip(), state, line, fields)
|
|
701
|
+
val = self._eval_expr(match.group(3).strip(), state, line, fields)
|
|
702
|
+
key = f"{arr_name}[{idx}]"
|
|
703
|
+
state.variables[key] = val
|
|
704
|
+
return
|
|
705
|
+
|
|
624
706
|
# Simple assignment
|
|
625
707
|
match = re.match(r"(\w+)\s*=\s*(.+)", stmt)
|
|
626
708
|
if match:
|
|
@@ -639,6 +721,10 @@ class AwkCommand:
|
|
|
639
721
|
fields.append("")
|
|
640
722
|
if field_num > 0:
|
|
641
723
|
fields[field_num - 1] = str(val)
|
|
724
|
+
# Reconstruct $0 from modified fields
|
|
725
|
+
ofs = state.variables.get("OFS", " ")
|
|
726
|
+
state.variables["__line__"] = ofs.join(fields)
|
|
727
|
+
state.variables["NF"] = len(fields)
|
|
642
728
|
return
|
|
643
729
|
|
|
644
730
|
# Handle increment/decrement
|
|
@@ -773,7 +859,6 @@ class AwkCommand:
|
|
|
773
859
|
if expr.startswith("sqrt("):
|
|
774
860
|
match = re.match(r"sqrt\((.+)\)", expr)
|
|
775
861
|
if match:
|
|
776
|
-
import math
|
|
777
862
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
778
863
|
try:
|
|
779
864
|
return math.sqrt(float(arg))
|
|
@@ -784,7 +869,6 @@ class AwkCommand:
|
|
|
784
869
|
if expr.startswith("sin("):
|
|
785
870
|
match = re.match(r"sin\((.+)\)", expr)
|
|
786
871
|
if match:
|
|
787
|
-
import math
|
|
788
872
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
789
873
|
try:
|
|
790
874
|
return math.sin(float(arg))
|
|
@@ -795,7 +879,6 @@ class AwkCommand:
|
|
|
795
879
|
if expr.startswith("cos("):
|
|
796
880
|
match = re.match(r"cos\((.+)\)", expr)
|
|
797
881
|
if match:
|
|
798
|
-
import math
|
|
799
882
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
800
883
|
try:
|
|
801
884
|
return math.cos(float(arg))
|
|
@@ -806,7 +889,6 @@ class AwkCommand:
|
|
|
806
889
|
if expr.startswith("log("):
|
|
807
890
|
match = re.match(r"log\((.+)\)", expr)
|
|
808
891
|
if match:
|
|
809
|
-
import math
|
|
810
892
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
811
893
|
try:
|
|
812
894
|
return math.log(float(arg))
|
|
@@ -817,7 +899,6 @@ class AwkCommand:
|
|
|
817
899
|
if expr.startswith("exp("):
|
|
818
900
|
match = re.match(r"exp\((.+)\)", expr)
|
|
819
901
|
if match:
|
|
820
|
-
import math
|
|
821
902
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
822
903
|
try:
|
|
823
904
|
return math.exp(float(arg))
|
|
@@ -845,6 +926,104 @@ class AwkCommand:
|
|
|
845
926
|
return len(parts)
|
|
846
927
|
return 0
|
|
847
928
|
|
|
929
|
+
# rand() - return random number 0 <= n < 1
|
|
930
|
+
if expr == "rand()":
|
|
931
|
+
return state.rng.random()
|
|
932
|
+
|
|
933
|
+
# srand([seed]) - seed the random number generator
|
|
934
|
+
if expr.startswith("srand(") or expr == "srand()":
|
|
935
|
+
match = re.match(r"srand\(([^)]*)\)", expr)
|
|
936
|
+
if match:
|
|
937
|
+
seed_str = match.group(1).strip()
|
|
938
|
+
if seed_str:
|
|
939
|
+
seed = self._eval_expr(seed_str, state, line, fields)
|
|
940
|
+
try:
|
|
941
|
+
state.rng.seed(int(float(seed)))
|
|
942
|
+
except (ValueError, TypeError):
|
|
943
|
+
state.rng.seed(int(time.time()))
|
|
944
|
+
else:
|
|
945
|
+
state.rng.seed(int(time.time()))
|
|
946
|
+
return 0 # srand returns previous seed, but we just return 0
|
|
947
|
+
|
|
948
|
+
# sprintf(fmt, args...) - return formatted string
|
|
949
|
+
if expr.startswith("sprintf("):
|
|
950
|
+
match = re.match(r"sprintf\((.+)\)", expr)
|
|
951
|
+
if match:
|
|
952
|
+
args = self._split_args(match.group(1))
|
|
953
|
+
if args:
|
|
954
|
+
fmt = str(self._eval_expr(args[0], state, line, fields))
|
|
955
|
+
values = [self._eval_expr(a, state, line, fields) for a in args[1:]]
|
|
956
|
+
return self._format_string(fmt, values)
|
|
957
|
+
return ""
|
|
958
|
+
|
|
959
|
+
# match(s, r) - return position of regex match, set RSTART and RLENGTH
|
|
960
|
+
if expr.startswith("match("):
|
|
961
|
+
match_call = re.match(r"match\((.+)\)", expr)
|
|
962
|
+
if match_call:
|
|
963
|
+
args = self._split_args(match_call.group(1))
|
|
964
|
+
if len(args) >= 2:
|
|
965
|
+
s = str(self._eval_expr(args[0], state, line, fields))
|
|
966
|
+
pattern = args[1].strip()
|
|
967
|
+
# Handle /regex/ syntax
|
|
968
|
+
if pattern.startswith("/") and pattern.endswith("/"):
|
|
969
|
+
pattern = pattern[1:-1]
|
|
970
|
+
try:
|
|
971
|
+
regex_match = re.search(pattern, s)
|
|
972
|
+
if regex_match:
|
|
973
|
+
pos = regex_match.start() + 1 # 1-based
|
|
974
|
+
length = regex_match.end() - regex_match.start()
|
|
975
|
+
state.variables["RSTART"] = pos
|
|
976
|
+
state.variables["RLENGTH"] = length
|
|
977
|
+
return pos
|
|
978
|
+
else:
|
|
979
|
+
state.variables["RSTART"] = 0
|
|
980
|
+
state.variables["RLENGTH"] = -1
|
|
981
|
+
return 0
|
|
982
|
+
except re.error:
|
|
983
|
+
state.variables["RSTART"] = 0
|
|
984
|
+
state.variables["RLENGTH"] = -1
|
|
985
|
+
return 0
|
|
986
|
+
return 0
|
|
987
|
+
|
|
988
|
+
# atan2(y, x) - arctangent of y/x
|
|
989
|
+
if expr.startswith("atan2("):
|
|
990
|
+
match = re.match(r"atan2\((.+)\)", expr)
|
|
991
|
+
if match:
|
|
992
|
+
args = self._split_args(match.group(1))
|
|
993
|
+
if len(args) >= 2:
|
|
994
|
+
y = self._eval_expr(args[0], state, line, fields)
|
|
995
|
+
x = self._eval_expr(args[1], state, line, fields)
|
|
996
|
+
try:
|
|
997
|
+
return math.atan2(float(y), float(x))
|
|
998
|
+
except (ValueError, TypeError):
|
|
999
|
+
return 0
|
|
1000
|
+
return 0
|
|
1001
|
+
|
|
1002
|
+
# systime() - return current epoch timestamp
|
|
1003
|
+
if expr == "systime()":
|
|
1004
|
+
return int(time.time())
|
|
1005
|
+
|
|
1006
|
+
# strftime(fmt, timestamp) - format timestamp as string
|
|
1007
|
+
if expr.startswith("strftime("):
|
|
1008
|
+
match = re.match(r"strftime\((.+)\)", expr)
|
|
1009
|
+
if match:
|
|
1010
|
+
args = self._split_args(match.group(1))
|
|
1011
|
+
if args:
|
|
1012
|
+
fmt = str(self._eval_expr(args[0], state, line, fields))
|
|
1013
|
+
if len(args) >= 2:
|
|
1014
|
+
timestamp = self._eval_expr(args[1], state, line, fields)
|
|
1015
|
+
try:
|
|
1016
|
+
timestamp = int(float(timestamp))
|
|
1017
|
+
except (ValueError, TypeError):
|
|
1018
|
+
timestamp = int(time.time())
|
|
1019
|
+
else:
|
|
1020
|
+
timestamp = int(time.time())
|
|
1021
|
+
try:
|
|
1022
|
+
return time.strftime(fmt, time.localtime(timestamp))
|
|
1023
|
+
except (ValueError, OSError):
|
|
1024
|
+
return ""
|
|
1025
|
+
return ""
|
|
1026
|
+
|
|
848
1027
|
# Arithmetic - check for operators (including with spaces like "2 + 3")
|
|
849
1028
|
for op in ["+", "-", "*", "/", "%"]:
|
|
850
1029
|
if op in expr:
|
|
@@ -1010,29 +1189,137 @@ class AwkCommand:
|
|
|
1010
1189
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
1011
1190
|
) -> None:
|
|
1012
1191
|
"""Execute an if statement."""
|
|
1013
|
-
#
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1192
|
+
# Find the condition by matching balanced parentheses
|
|
1193
|
+
if not stmt.startswith("if"):
|
|
1194
|
+
return
|
|
1195
|
+
|
|
1196
|
+
# Find opening paren
|
|
1197
|
+
paren_start = stmt.find("(")
|
|
1198
|
+
if paren_start == -1:
|
|
1199
|
+
return
|
|
1200
|
+
|
|
1201
|
+
# Find matching closing paren
|
|
1202
|
+
depth = 1
|
|
1203
|
+
pos = paren_start + 1
|
|
1204
|
+
while pos < len(stmt) and depth > 0:
|
|
1205
|
+
if stmt[pos] == "(":
|
|
1206
|
+
depth += 1
|
|
1207
|
+
elif stmt[pos] == ")":
|
|
1208
|
+
depth -= 1
|
|
1209
|
+
pos += 1
|
|
1210
|
+
|
|
1211
|
+
if depth != 0:
|
|
1212
|
+
return
|
|
1213
|
+
|
|
1214
|
+
condition = stmt[paren_start + 1:pos - 1]
|
|
1215
|
+
rest = stmt[pos:].strip()
|
|
1216
|
+
|
|
1217
|
+
# Check for braced then-action
|
|
1218
|
+
if rest.startswith("{"):
|
|
1219
|
+
# Find matching closing brace
|
|
1220
|
+
brace_depth = 1
|
|
1221
|
+
brace_pos = 1
|
|
1222
|
+
while brace_pos < len(rest) and brace_depth > 0:
|
|
1223
|
+
if rest[brace_pos] == "{":
|
|
1224
|
+
brace_depth += 1
|
|
1225
|
+
elif rest[brace_pos] == "}":
|
|
1226
|
+
brace_depth -= 1
|
|
1227
|
+
brace_pos += 1
|
|
1228
|
+
|
|
1229
|
+
then_action = rest[1:brace_pos - 1]
|
|
1230
|
+
after_then = rest[brace_pos:].strip()
|
|
1231
|
+
|
|
1232
|
+
# Check for else
|
|
1233
|
+
else_action = None
|
|
1234
|
+
if after_then.startswith("else"):
|
|
1235
|
+
else_rest = after_then[4:].strip()
|
|
1236
|
+
if else_rest.startswith("{"):
|
|
1237
|
+
# Find matching brace for else
|
|
1238
|
+
brace_depth = 1
|
|
1239
|
+
brace_pos = 1
|
|
1240
|
+
while brace_pos < len(else_rest) and brace_depth > 0:
|
|
1241
|
+
if else_rest[brace_pos] == "{":
|
|
1242
|
+
brace_depth += 1
|
|
1243
|
+
elif else_rest[brace_pos] == "}":
|
|
1244
|
+
brace_depth -= 1
|
|
1245
|
+
brace_pos += 1
|
|
1246
|
+
else_action = else_rest[1:brace_pos - 1]
|
|
1019
1247
|
|
|
1020
1248
|
if self._eval_condition(condition, state, fields, line):
|
|
1021
1249
|
self._execute_action(then_action, state, fields, line)
|
|
1022
1250
|
elif else_action:
|
|
1023
1251
|
self._execute_action(else_action, state, fields, line)
|
|
1252
|
+
else:
|
|
1253
|
+
# No braces - check for else clause separated by ;
|
|
1254
|
+
then_action = rest
|
|
1255
|
+
else_action = None
|
|
1256
|
+
|
|
1257
|
+
# Look for "; else" pattern (not inside strings)
|
|
1258
|
+
else_idx = -1
|
|
1259
|
+
in_str = False
|
|
1260
|
+
esc = False
|
|
1261
|
+
for ci in range(len(rest)):
|
|
1262
|
+
if esc:
|
|
1263
|
+
esc = False
|
|
1264
|
+
continue
|
|
1265
|
+
if rest[ci] == "\\":
|
|
1266
|
+
esc = True
|
|
1267
|
+
continue
|
|
1268
|
+
if rest[ci] == '"':
|
|
1269
|
+
in_str = not in_str
|
|
1270
|
+
continue
|
|
1271
|
+
if not in_str and rest[ci] == ";" and rest[ci + 1:].lstrip().startswith("else"):
|
|
1272
|
+
else_idx = ci
|
|
1273
|
+
break
|
|
1274
|
+
|
|
1275
|
+
if else_idx != -1:
|
|
1276
|
+
then_action = rest[:else_idx].strip()
|
|
1277
|
+
else_part = rest[else_idx + 1:].strip()
|
|
1278
|
+
if else_part.startswith("else"):
|
|
1279
|
+
else_action = else_part[4:].strip()
|
|
1280
|
+
|
|
1281
|
+
if self._eval_condition(condition, state, fields, line):
|
|
1282
|
+
self._execute_statement(then_action, state, fields, line)
|
|
1283
|
+
elif else_action:
|
|
1284
|
+
self._execute_statement(else_action, state, fields, line)
|
|
1024
1285
|
|
|
1025
1286
|
def _execute_for(
|
|
1026
1287
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
1027
1288
|
) -> None:
|
|
1028
1289
|
"""Execute a for statement."""
|
|
1290
|
+
# Parse: for (var in array) body
|
|
1291
|
+
match = re.match(r"for\s*\(\s*(\w+)\s+in\s+(\w+)\s*\)\s*(.*)", stmt, re.DOTALL)
|
|
1292
|
+
if match:
|
|
1293
|
+
var = match.group(1)
|
|
1294
|
+
arr_name = match.group(2)
|
|
1295
|
+
body = match.group(3).strip()
|
|
1296
|
+
if body.startswith("{") and body.endswith("}"):
|
|
1297
|
+
body = body[1:-1]
|
|
1298
|
+
|
|
1299
|
+
# Find all keys for this array
|
|
1300
|
+
keys = []
|
|
1301
|
+
prefix = f"{arr_name}["
|
|
1302
|
+
for k in state.variables:
|
|
1303
|
+
if isinstance(k, str) and k.startswith(prefix) and k.endswith("]"):
|
|
1304
|
+
keys.append(k[len(prefix):-1])
|
|
1305
|
+
|
|
1306
|
+
for key in keys:
|
|
1307
|
+
state.variables[var] = key
|
|
1308
|
+
self._execute_action(body, state, fields, line)
|
|
1309
|
+
return
|
|
1310
|
+
|
|
1029
1311
|
# Parse: for (init; condition; update) { action }
|
|
1030
1312
|
match = re.match(r"for\s*\((.+?);(.+?);(.+?)\)\s*\{(.+?)\}", stmt, re.DOTALL)
|
|
1313
|
+
if not match:
|
|
1314
|
+
# Try without braces: for (init; condition; update) statement
|
|
1315
|
+
match = re.match(r"for\s*\((.+?);(.+?);(.+?)\)\s*(.*)", stmt, re.DOTALL)
|
|
1031
1316
|
if match:
|
|
1032
1317
|
init = match.group(1).strip()
|
|
1033
1318
|
condition = match.group(2).strip()
|
|
1034
1319
|
update = match.group(3).strip()
|
|
1035
|
-
action = match.group(4)
|
|
1320
|
+
action = match.group(4).strip()
|
|
1321
|
+
if action.startswith("{") and action.endswith("}"):
|
|
1322
|
+
action = action[1:-1]
|
|
1036
1323
|
|
|
1037
1324
|
# Execute init
|
|
1038
1325
|
self._execute_statement(init, state, fields, line)
|
|
@@ -1166,3 +1453,61 @@ class AwkCommand:
|
|
|
1166
1453
|
return str(int(n))
|
|
1167
1454
|
return str(n)
|
|
1168
1455
|
return str(n)
|
|
1456
|
+
|
|
1457
|
+
def _format_string(self, fmt: str, values: list[Any]) -> str:
|
|
1458
|
+
"""Format a string using printf-style format specifiers."""
|
|
1459
|
+
result = ""
|
|
1460
|
+
i = 0
|
|
1461
|
+
val_idx = 0
|
|
1462
|
+
|
|
1463
|
+
while i < len(fmt):
|
|
1464
|
+
if fmt[i] == "\\" and i + 1 < len(fmt):
|
|
1465
|
+
c = fmt[i + 1]
|
|
1466
|
+
if c == "n":
|
|
1467
|
+
result += "\n"
|
|
1468
|
+
elif c == "t":
|
|
1469
|
+
result += "\t"
|
|
1470
|
+
elif c == "\\":
|
|
1471
|
+
result += "\\"
|
|
1472
|
+
else:
|
|
1473
|
+
result += c
|
|
1474
|
+
i += 2
|
|
1475
|
+
elif fmt[i] == "%" and i + 1 < len(fmt):
|
|
1476
|
+
# Parse format spec
|
|
1477
|
+
j = i + 1
|
|
1478
|
+
while j < len(fmt) and fmt[j] in "-+0 #":
|
|
1479
|
+
j += 1
|
|
1480
|
+
while j < len(fmt) and fmt[j].isdigit():
|
|
1481
|
+
j += 1
|
|
1482
|
+
if j < len(fmt) and fmt[j] == ".":
|
|
1483
|
+
j += 1
|
|
1484
|
+
while j < len(fmt) and fmt[j].isdigit():
|
|
1485
|
+
j += 1
|
|
1486
|
+
if j < len(fmt):
|
|
1487
|
+
spec = fmt[i:j + 1]
|
|
1488
|
+
conv = fmt[j]
|
|
1489
|
+
if conv == "%":
|
|
1490
|
+
result += "%"
|
|
1491
|
+
elif val_idx < len(values):
|
|
1492
|
+
val = values[val_idx]
|
|
1493
|
+
val_idx += 1
|
|
1494
|
+
try:
|
|
1495
|
+
if conv in "diouxX":
|
|
1496
|
+
result += spec % int(float(val))
|
|
1497
|
+
elif conv in "eEfFgG":
|
|
1498
|
+
result += spec % float(val)
|
|
1499
|
+
elif conv == "s":
|
|
1500
|
+
result += spec % str(val)
|
|
1501
|
+
else:
|
|
1502
|
+
result += spec % val
|
|
1503
|
+
except (ValueError, TypeError):
|
|
1504
|
+
result += str(val)
|
|
1505
|
+
i = j + 1
|
|
1506
|
+
else:
|
|
1507
|
+
result += fmt[i]
|
|
1508
|
+
i += 1
|
|
1509
|
+
else:
|
|
1510
|
+
result += fmt[i]
|
|
1511
|
+
i += 1
|
|
1512
|
+
|
|
1513
|
+
return result
|
just_bash/commands/cat/cat.py
CHANGED
|
@@ -117,8 +117,12 @@ class CatCommand:
|
|
|
117
117
|
|
|
118
118
|
for file in files:
|
|
119
119
|
try:
|
|
120
|
-
if file == "-":
|
|
120
|
+
if file == "-" or file == "/dev/stdin":
|
|
121
121
|
content = ctx.stdin
|
|
122
|
+
elif file == "/dev/stdout":
|
|
123
|
+
content = ""
|
|
124
|
+
elif file == "/dev/stderr":
|
|
125
|
+
content = ""
|
|
122
126
|
else:
|
|
123
127
|
# Resolve path
|
|
124
128
|
path = ctx.fs.resolve_path(ctx.cwd, file)
|