just-bash 0.1.8__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 +112 -7
- just_bash/commands/cat/cat.py +5 -1
- just_bash/commands/echo/echo.py +33 -1
- just_bash/commands/grep/grep.py +30 -1
- 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 +24 -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/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 +14 -0
- 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 +424 -70
- 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.8.dist-info → just_bash-0.1.10.dist-info}/METADATA +40 -1
- {just_bash-0.1.8.dist-info → just_bash-0.1.10.dist-info}/RECORD +47 -40
- {just_bash-0.1.8.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
|
@@ -74,6 +74,7 @@ class AwkRule:
|
|
|
74
74
|
action: str
|
|
75
75
|
is_regex: bool = False
|
|
76
76
|
regex: re.Pattern | None = None
|
|
77
|
+
negate: bool = False # ! pattern negation
|
|
77
78
|
|
|
78
79
|
|
|
79
80
|
@dataclass
|
|
@@ -287,12 +288,27 @@ class AwkCommand:
|
|
|
287
288
|
is_regex = False
|
|
288
289
|
regex = None
|
|
289
290
|
|
|
291
|
+
negate_pattern = False
|
|
292
|
+
|
|
290
293
|
if program[pos:].startswith("BEGIN"):
|
|
291
294
|
pattern = "BEGIN"
|
|
292
295
|
pos += 5
|
|
293
296
|
elif program[pos:].startswith("END"):
|
|
294
297
|
pattern = "END"
|
|
295
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
|
|
296
312
|
elif program[pos] == "/":
|
|
297
313
|
# Regex pattern
|
|
298
314
|
end = self._find_regex_end(program, pos + 1)
|
|
@@ -349,10 +365,10 @@ class AwkCommand:
|
|
|
349
365
|
pos += 1
|
|
350
366
|
|
|
351
367
|
action = program[start:pos - 1].strip()
|
|
352
|
-
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))
|
|
353
369
|
else:
|
|
354
370
|
# Default action is print $0
|
|
355
|
-
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))
|
|
356
372
|
|
|
357
373
|
return rules
|
|
358
374
|
|
|
@@ -374,7 +390,8 @@ class AwkCommand:
|
|
|
374
390
|
return True
|
|
375
391
|
|
|
376
392
|
if rule.is_regex and rule.regex:
|
|
377
|
-
|
|
393
|
+
result = bool(rule.regex.search(line))
|
|
394
|
+
return not result if rule.negate else result
|
|
378
395
|
|
|
379
396
|
# Expression pattern
|
|
380
397
|
pattern = rule.pattern
|
|
@@ -493,7 +510,15 @@ class AwkCommand:
|
|
|
493
510
|
if current.strip():
|
|
494
511
|
statements.append(current.strip())
|
|
495
512
|
|
|
496
|
-
|
|
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
|
|
497
522
|
|
|
498
523
|
def _execute_statement(
|
|
499
524
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
@@ -621,6 +646,17 @@ class AwkCommand:
|
|
|
621
646
|
pass
|
|
622
647
|
return
|
|
623
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
|
+
|
|
624
660
|
# Handle match() as a statement (for side effects on RSTART/RLENGTH)
|
|
625
661
|
if stmt.startswith("match("):
|
|
626
662
|
# Just evaluate it - _eval_expr will set RSTART/RLENGTH
|
|
@@ -657,6 +693,16 @@ class AwkCommand:
|
|
|
657
693
|
state.variables[var] = current / val if val != 0 else 0
|
|
658
694
|
return
|
|
659
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
|
+
|
|
660
706
|
# Simple assignment
|
|
661
707
|
match = re.match(r"(\w+)\s*=\s*(.+)", stmt)
|
|
662
708
|
if match:
|
|
@@ -675,6 +721,10 @@ class AwkCommand:
|
|
|
675
721
|
fields.append("")
|
|
676
722
|
if field_num > 0:
|
|
677
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)
|
|
678
728
|
return
|
|
679
729
|
|
|
680
730
|
# Handle increment/decrement
|
|
@@ -1200,21 +1250,76 @@ class AwkCommand:
|
|
|
1200
1250
|
elif else_action:
|
|
1201
1251
|
self._execute_action(else_action, state, fields, line)
|
|
1202
1252
|
else:
|
|
1203
|
-
# No braces -
|
|
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
|
+
|
|
1204
1281
|
if self._eval_condition(condition, state, fields, line):
|
|
1205
|
-
self._execute_statement(
|
|
1282
|
+
self._execute_statement(then_action, state, fields, line)
|
|
1283
|
+
elif else_action:
|
|
1284
|
+
self._execute_statement(else_action, state, fields, line)
|
|
1206
1285
|
|
|
1207
1286
|
def _execute_for(
|
|
1208
1287
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
1209
1288
|
) -> None:
|
|
1210
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
|
+
|
|
1211
1311
|
# Parse: for (init; condition; update) { action }
|
|
1212
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)
|
|
1213
1316
|
if match:
|
|
1214
1317
|
init = match.group(1).strip()
|
|
1215
1318
|
condition = match.group(2).strip()
|
|
1216
1319
|
update = match.group(3).strip()
|
|
1217
|
-
action = match.group(4)
|
|
1320
|
+
action = match.group(4).strip()
|
|
1321
|
+
if action.startswith("{") and action.endswith("}"):
|
|
1322
|
+
action = action[1:-1]
|
|
1218
1323
|
|
|
1219
1324
|
# Execute init
|
|
1220
1325
|
self._execute_statement(init, state, fields, line)
|
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)
|
just_bash/commands/echo/echo.py
CHANGED
|
@@ -53,7 +53,7 @@ def _process_escapes(s: str) -> str:
|
|
|
53
53
|
octal += s[j]
|
|
54
54
|
j += 1
|
|
55
55
|
if octal:
|
|
56
|
-
result.append(chr(int(octal, 8)))
|
|
56
|
+
result.append(chr(int(octal, 8) & 0xFF))
|
|
57
57
|
else:
|
|
58
58
|
result.append("\0")
|
|
59
59
|
i = j
|
|
@@ -70,6 +70,38 @@ def _process_escapes(s: str) -> str:
|
|
|
70
70
|
else:
|
|
71
71
|
result.append(s[i])
|
|
72
72
|
i += 1
|
|
73
|
+
elif next_char == "u":
|
|
74
|
+
# Unicode escape: \uHHHH (4 hex digits)
|
|
75
|
+
hex_digits = ""
|
|
76
|
+
j = i + 2
|
|
77
|
+
while j < len(s) and len(hex_digits) < 4 and s[j] in "0123456789abcdefABCDEF":
|
|
78
|
+
hex_digits += s[j]
|
|
79
|
+
j += 1
|
|
80
|
+
if hex_digits:
|
|
81
|
+
try:
|
|
82
|
+
result.append(chr(int(hex_digits, 16)))
|
|
83
|
+
except (ValueError, OverflowError):
|
|
84
|
+
result.append("\\u" + hex_digits)
|
|
85
|
+
i = j
|
|
86
|
+
else:
|
|
87
|
+
result.append("\\u")
|
|
88
|
+
i += 2
|
|
89
|
+
elif next_char == "U":
|
|
90
|
+
# Unicode escape: \UHHHHHHHH (8 hex digits)
|
|
91
|
+
hex_digits = ""
|
|
92
|
+
j = i + 2
|
|
93
|
+
while j < len(s) and len(hex_digits) < 8 and s[j] in "0123456789abcdefABCDEF":
|
|
94
|
+
hex_digits += s[j]
|
|
95
|
+
j += 1
|
|
96
|
+
if hex_digits:
|
|
97
|
+
try:
|
|
98
|
+
result.append(chr(int(hex_digits, 16)))
|
|
99
|
+
except (ValueError, OverflowError):
|
|
100
|
+
result.append("\\U" + hex_digits)
|
|
101
|
+
i = j
|
|
102
|
+
else:
|
|
103
|
+
result.append("\\U")
|
|
104
|
+
i += 2
|
|
73
105
|
elif next_char == "c":
|
|
74
106
|
# \c stops output
|
|
75
107
|
return "".join(result)
|
just_bash/commands/grep/grep.py
CHANGED
|
@@ -152,7 +152,10 @@ class GrepCommand:
|
|
|
152
152
|
elif arg == "-e":
|
|
153
153
|
patterns.append(val)
|
|
154
154
|
else:
|
|
155
|
-
|
|
155
|
+
chars = arg[1:]
|
|
156
|
+
ci = 0
|
|
157
|
+
while ci < len(chars):
|
|
158
|
+
c = chars[ci]
|
|
156
159
|
if c == 'i':
|
|
157
160
|
ignore_case = True
|
|
158
161
|
elif c == 'v':
|
|
@@ -186,12 +189,38 @@ class GrepCommand:
|
|
|
186
189
|
word_regexp = True
|
|
187
190
|
elif c == 'x':
|
|
188
191
|
line_regexp = True
|
|
192
|
+
elif c in ('A', 'B', 'C', 'm', 'e'):
|
|
193
|
+
# These flags take a value: rest of string or next arg
|
|
194
|
+
rest = chars[ci + 1:]
|
|
195
|
+
if rest:
|
|
196
|
+
val = rest
|
|
197
|
+
elif i + 1 < len(args):
|
|
198
|
+
i += 1
|
|
199
|
+
val = args[i]
|
|
200
|
+
else:
|
|
201
|
+
return ExecResult(
|
|
202
|
+
stdout="",
|
|
203
|
+
stderr=f"grep: option requires an argument -- '{c}'\n",
|
|
204
|
+
exit_code=2,
|
|
205
|
+
)
|
|
206
|
+
if c == 'A':
|
|
207
|
+
after_context = int(val)
|
|
208
|
+
elif c == 'B':
|
|
209
|
+
before_context = int(val)
|
|
210
|
+
elif c == 'C':
|
|
211
|
+
before_context = after_context = int(val)
|
|
212
|
+
elif c == 'm':
|
|
213
|
+
max_count = int(val)
|
|
214
|
+
elif c == 'e':
|
|
215
|
+
patterns.append(val)
|
|
216
|
+
break # Rest of chars consumed as value
|
|
189
217
|
else:
|
|
190
218
|
return ExecResult(
|
|
191
219
|
stdout="",
|
|
192
220
|
stderr=f"grep: invalid option -- '{c}'\n",
|
|
193
221
|
exit_code=2,
|
|
194
222
|
)
|
|
223
|
+
ci += 1
|
|
195
224
|
elif pattern is None and not patterns:
|
|
196
225
|
pattern = arg
|
|
197
226
|
else:
|
just_bash/commands/od/od.py
CHANGED
|
@@ -13,6 +13,8 @@ class OdCommand:
|
|
|
13
13
|
format_type = "o" # octal (default)
|
|
14
14
|
address_format = "o" # octal addresses
|
|
15
15
|
suppress_address = False
|
|
16
|
+
skip_bytes = 0
|
|
17
|
+
read_count = -1 # -1 means read all
|
|
16
18
|
files: list[str] = []
|
|
17
19
|
|
|
18
20
|
i = 0
|
|
@@ -34,12 +36,97 @@ class OdCommand:
|
|
|
34
36
|
format_type = "d" # decimal
|
|
35
37
|
elif arg == "-An":
|
|
36
38
|
suppress_address = True
|
|
37
|
-
elif arg == "-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
elif arg == "-A":
|
|
40
|
+
# -A RADIX: address radix
|
|
41
|
+
if i + 1 < len(args):
|
|
42
|
+
i += 1
|
|
43
|
+
radix = args[i]
|
|
44
|
+
if radix == "n":
|
|
45
|
+
suppress_address = True
|
|
46
|
+
elif radix in ("d", "o", "x"):
|
|
47
|
+
address_format = radix
|
|
48
|
+
else:
|
|
49
|
+
return ExecResult(
|
|
50
|
+
stdout="",
|
|
51
|
+
stderr="od: option requires an argument -- 'A'\n",
|
|
52
|
+
exit_code=1,
|
|
53
|
+
)
|
|
54
|
+
elif arg.startswith("-A") and len(arg) == 3:
|
|
55
|
+
radix = arg[2]
|
|
56
|
+
if radix == "n":
|
|
57
|
+
suppress_address = True
|
|
58
|
+
elif radix in ("d", "o", "x"):
|
|
59
|
+
address_format = radix
|
|
60
|
+
elif arg == "-j":
|
|
61
|
+
# -j BYTES: skip bytes
|
|
62
|
+
if i + 1 < len(args):
|
|
63
|
+
i += 1
|
|
64
|
+
try:
|
|
65
|
+
skip_bytes = int(args[i])
|
|
66
|
+
except ValueError:
|
|
67
|
+
return ExecResult(
|
|
68
|
+
stdout="",
|
|
69
|
+
stderr=f"od: invalid argument '{args[i]}' for skip\n",
|
|
70
|
+
exit_code=1,
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
return ExecResult(
|
|
74
|
+
stdout="",
|
|
75
|
+
stderr="od: option requires an argument -- 'j'\n",
|
|
76
|
+
exit_code=1,
|
|
77
|
+
)
|
|
78
|
+
elif arg.startswith("-j") and len(arg) > 2:
|
|
79
|
+
try:
|
|
80
|
+
skip_bytes = int(arg[2:])
|
|
81
|
+
except ValueError:
|
|
82
|
+
return ExecResult(
|
|
83
|
+
stdout="",
|
|
84
|
+
stderr=f"od: invalid argument '{arg[2:]}' for skip\n",
|
|
85
|
+
exit_code=1,
|
|
86
|
+
)
|
|
87
|
+
elif arg == "-N":
|
|
88
|
+
# -N BYTES: read count
|
|
89
|
+
if i + 1 < len(args):
|
|
90
|
+
i += 1
|
|
91
|
+
try:
|
|
92
|
+
read_count = int(args[i])
|
|
93
|
+
except ValueError:
|
|
94
|
+
return ExecResult(
|
|
95
|
+
stdout="",
|
|
96
|
+
stderr=f"od: invalid argument '{args[i]}' for count\n",
|
|
97
|
+
exit_code=1,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
return ExecResult(
|
|
101
|
+
stdout="",
|
|
102
|
+
stderr="od: option requires an argument -- 'N'\n",
|
|
103
|
+
exit_code=1,
|
|
104
|
+
)
|
|
105
|
+
elif arg.startswith("-N") and len(arg) > 2:
|
|
106
|
+
try:
|
|
107
|
+
read_count = int(arg[2:])
|
|
108
|
+
except ValueError:
|
|
109
|
+
return ExecResult(
|
|
110
|
+
stdout="",
|
|
111
|
+
stderr=f"od: invalid argument '{arg[2:]}' for count\n",
|
|
112
|
+
exit_code=1,
|
|
113
|
+
)
|
|
114
|
+
elif arg == "-t":
|
|
115
|
+
# -t TYPE: type specifier follows
|
|
116
|
+
if i + 1 < len(args):
|
|
117
|
+
i += 1
|
|
118
|
+
type_spec = args[i]
|
|
119
|
+
format_type = self._parse_type_spec(type_spec)
|
|
120
|
+
else:
|
|
121
|
+
return ExecResult(
|
|
122
|
+
stdout="",
|
|
123
|
+
stderr="od: option requires an argument -- 't'\n",
|
|
124
|
+
exit_code=1,
|
|
125
|
+
)
|
|
126
|
+
elif arg.startswith("-t"):
|
|
127
|
+
# -tTYPE: type specifier attached
|
|
128
|
+
type_spec = arg[2:]
|
|
129
|
+
format_type = self._parse_type_spec(type_spec)
|
|
43
130
|
elif arg == "--":
|
|
44
131
|
files.extend(args[i + 1:])
|
|
45
132
|
break
|
|
@@ -55,7 +142,12 @@ class OdCommand:
|
|
|
55
142
|
|
|
56
143
|
# Read from stdin if no files
|
|
57
144
|
if not files:
|
|
58
|
-
content = ctx.stdin.encode("
|
|
145
|
+
content = ctx.stdin.encode("latin-1", errors="replace")
|
|
146
|
+
# Apply skip and count
|
|
147
|
+
if skip_bytes > 0:
|
|
148
|
+
content = content[skip_bytes:]
|
|
149
|
+
if read_count >= 0:
|
|
150
|
+
content = content[:read_count]
|
|
59
151
|
result = self._dump(content, format_type, address_format, suppress_address)
|
|
60
152
|
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
61
153
|
|
|
@@ -66,11 +158,17 @@ class OdCommand:
|
|
|
66
158
|
for file in files:
|
|
67
159
|
try:
|
|
68
160
|
if file == "-":
|
|
69
|
-
content = ctx.stdin.encode("
|
|
161
|
+
content = ctx.stdin.encode("latin-1", errors="replace")
|
|
70
162
|
else:
|
|
71
163
|
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
72
164
|
content = await ctx.fs.read_file_bytes(path)
|
|
73
165
|
|
|
166
|
+
# Apply skip and count
|
|
167
|
+
if skip_bytes > 0:
|
|
168
|
+
content = content[skip_bytes:]
|
|
169
|
+
if read_count >= 0:
|
|
170
|
+
content = content[:read_count]
|
|
171
|
+
|
|
74
172
|
result = self._dump(content, format_type, address_format, suppress_address)
|
|
75
173
|
stdout_parts.append(result)
|
|
76
174
|
|
|
@@ -80,6 +178,22 @@ class OdCommand:
|
|
|
80
178
|
|
|
81
179
|
return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
|
|
82
180
|
|
|
181
|
+
def _parse_type_spec(self, spec: str) -> str:
|
|
182
|
+
"""Parse a -t type specifier."""
|
|
183
|
+
if not spec:
|
|
184
|
+
return "o"
|
|
185
|
+
first_char = spec[0].lower()
|
|
186
|
+
if first_char == "c":
|
|
187
|
+
return "c"
|
|
188
|
+
elif first_char == "x":
|
|
189
|
+
return "x"
|
|
190
|
+
elif first_char == "o":
|
|
191
|
+
return "o"
|
|
192
|
+
elif first_char == "d" or first_char == "u":
|
|
193
|
+
return "d"
|
|
194
|
+
else:
|
|
195
|
+
return "o"
|
|
196
|
+
|
|
83
197
|
def _dump(
|
|
84
198
|
self, data: bytes, format_type: str, address_format: str, suppress_address: bool
|
|
85
199
|
) -> str:
|
|
@@ -101,44 +215,44 @@ class OdCommand:
|
|
|
101
215
|
else:
|
|
102
216
|
parts.append(f"{offset:07o}")
|
|
103
217
|
|
|
104
|
-
# Add data
|
|
218
|
+
# Add data with proper 4-char field formatting
|
|
105
219
|
if format_type == "c":
|
|
106
|
-
# Character format
|
|
220
|
+
# Character format: each char in 4-char field
|
|
107
221
|
chars = []
|
|
108
222
|
for byte in line_data:
|
|
109
223
|
if byte == 0:
|
|
110
|
-
chars.append("\\0")
|
|
224
|
+
chars.append(" \\0")
|
|
111
225
|
elif byte == 7:
|
|
112
|
-
chars.append("\\a")
|
|
226
|
+
chars.append(" \\a")
|
|
113
227
|
elif byte == 8:
|
|
114
|
-
chars.append("\\b")
|
|
228
|
+
chars.append(" \\b")
|
|
115
229
|
elif byte == 9:
|
|
116
|
-
chars.append("\\t")
|
|
230
|
+
chars.append(" \\t")
|
|
117
231
|
elif byte == 10:
|
|
118
|
-
chars.append("\\n")
|
|
232
|
+
chars.append(" \\n")
|
|
119
233
|
elif byte == 11:
|
|
120
|
-
chars.append("\\v")
|
|
234
|
+
chars.append(" \\v")
|
|
121
235
|
elif byte == 12:
|
|
122
|
-
chars.append("\\f")
|
|
236
|
+
chars.append(" \\f")
|
|
123
237
|
elif byte == 13:
|
|
124
|
-
chars.append("\\r")
|
|
238
|
+
chars.append(" \\r")
|
|
125
239
|
elif 32 <= byte <= 126:
|
|
126
|
-
chars.append(f"
|
|
240
|
+
chars.append(f" {chr(byte)}")
|
|
127
241
|
else:
|
|
128
|
-
chars.append(f"{byte:03o}")
|
|
129
|
-
parts.append("
|
|
242
|
+
chars.append(f" {byte:03o}")
|
|
243
|
+
parts.append("".join(chars))
|
|
130
244
|
elif format_type == "x":
|
|
131
|
-
# Hexadecimal format
|
|
132
|
-
hex_vals = [f"{byte:02x}" for byte in line_data]
|
|
133
|
-
parts.append("
|
|
245
|
+
# Hexadecimal format: 4-char fields
|
|
246
|
+
hex_vals = [f" {byte:02x}" for byte in line_data]
|
|
247
|
+
parts.append("".join(hex_vals))
|
|
134
248
|
elif format_type == "d":
|
|
135
|
-
# Decimal format
|
|
136
|
-
dec_vals = [f"{byte:3d}" for byte in line_data]
|
|
137
|
-
parts.append("
|
|
249
|
+
# Decimal format: 4-char fields
|
|
250
|
+
dec_vals = [f" {byte:3d}" for byte in line_data]
|
|
251
|
+
parts.append("".join(dec_vals))
|
|
138
252
|
else:
|
|
139
|
-
# Octal format (default)
|
|
140
|
-
oct_vals = [f"{byte:03o}" for byte in line_data]
|
|
141
|
-
parts.append("
|
|
253
|
+
# Octal format (default): 4-char fields
|
|
254
|
+
oct_vals = [f" {byte:03o}" for byte in line_data]
|
|
255
|
+
parts.append("".join(oct_vals))
|
|
142
256
|
|
|
143
257
|
result_lines.append(" ".join(parts))
|
|
144
258
|
offset += bytes_per_line
|