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/commands/sed/sed.py
CHANGED
|
@@ -31,6 +31,59 @@ from dataclasses import dataclass
|
|
|
31
31
|
from ...types import CommandContext, ExecResult
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _bre_to_python(pattern: str) -> str:
|
|
35
|
+
"""Convert a BRE (Basic Regular Expression) pattern to Python ERE.
|
|
36
|
+
|
|
37
|
+
In BRE: +, ?, (, ), {, }, | are literal; \\+, \\?, \\(, \\), \\{, \\}, \\| are special.
|
|
38
|
+
In Python ERE: +, ?, (, ), {, }, | are special; \\+, \\?, etc. are literal.
|
|
39
|
+
"""
|
|
40
|
+
result = []
|
|
41
|
+
i = 0
|
|
42
|
+
in_bracket = False
|
|
43
|
+
while i < len(pattern):
|
|
44
|
+
ch = pattern[i]
|
|
45
|
+
if in_bracket:
|
|
46
|
+
result.append(ch)
|
|
47
|
+
if ch == "]" and i > 0:
|
|
48
|
+
in_bracket = False
|
|
49
|
+
i += 1
|
|
50
|
+
continue
|
|
51
|
+
if ch == "[":
|
|
52
|
+
in_bracket = True
|
|
53
|
+
result.append(ch)
|
|
54
|
+
i += 1
|
|
55
|
+
# Handle ] as first char in bracket (literal)
|
|
56
|
+
if i < len(pattern) and pattern[i] == "^":
|
|
57
|
+
result.append(pattern[i])
|
|
58
|
+
i += 1
|
|
59
|
+
if i < len(pattern) and pattern[i] == "]":
|
|
60
|
+
result.append(pattern[i])
|
|
61
|
+
i += 1
|
|
62
|
+
continue
|
|
63
|
+
if ch == "\\" and i + 1 < len(pattern):
|
|
64
|
+
nxt = pattern[i + 1]
|
|
65
|
+
if nxt in "+?(){}|":
|
|
66
|
+
# BRE \+ \? \( \) \{ \} \| → ERE special
|
|
67
|
+
result.append(nxt)
|
|
68
|
+
i += 2
|
|
69
|
+
continue
|
|
70
|
+
else:
|
|
71
|
+
# Other escape sequences pass through
|
|
72
|
+
result.append(ch)
|
|
73
|
+
result.append(nxt)
|
|
74
|
+
i += 2
|
|
75
|
+
continue
|
|
76
|
+
if ch in "+?(){}|":
|
|
77
|
+
# Literal in BRE → escape for Python ERE
|
|
78
|
+
result.append("\\")
|
|
79
|
+
result.append(ch)
|
|
80
|
+
i += 1
|
|
81
|
+
continue
|
|
82
|
+
result.append(ch)
|
|
83
|
+
i += 1
|
|
84
|
+
return "".join(result)
|
|
85
|
+
|
|
86
|
+
|
|
34
87
|
@dataclass
|
|
35
88
|
class SedAddress:
|
|
36
89
|
"""Represents a sed address."""
|
|
@@ -48,6 +101,7 @@ class SedCommand_:
|
|
|
48
101
|
|
|
49
102
|
cmd: str # s, d, p, a, i, y, q, h, H, g, G, x, n, N, etc.
|
|
50
103
|
address: SedAddress | None = None
|
|
104
|
+
negate: bool = False # ! address negation
|
|
51
105
|
pattern: re.Pattern | None = None
|
|
52
106
|
replacement: str | None = None
|
|
53
107
|
flags: str = ""
|
|
@@ -258,18 +312,55 @@ class SedCommand:
|
|
|
258
312
|
commands: list[SedCommand_] = []
|
|
259
313
|
|
|
260
314
|
for script in scripts:
|
|
261
|
-
#
|
|
262
|
-
for cmd_str in
|
|
315
|
+
# Split respecting brace nesting
|
|
316
|
+
for cmd_str in self._split_script(script):
|
|
263
317
|
cmd_str = cmd_str.strip()
|
|
264
318
|
if not cmd_str:
|
|
265
319
|
continue
|
|
266
320
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
321
|
+
# Handle grouped commands: addr{cmd1;cmd2}
|
|
322
|
+
brace_pos = cmd_str.find("{")
|
|
323
|
+
if brace_pos != -1 and cmd_str.endswith("}"):
|
|
324
|
+
addr_prefix = cmd_str[:brace_pos]
|
|
325
|
+
inner = cmd_str[brace_pos + 1 : -1]
|
|
326
|
+
for inner_cmd in inner.split(";"):
|
|
327
|
+
inner_cmd = inner_cmd.strip()
|
|
328
|
+
if inner_cmd:
|
|
329
|
+
full_cmd = addr_prefix + inner_cmd
|
|
330
|
+
cmd = self._parse_command(full_cmd, extended_regex)
|
|
331
|
+
if cmd:
|
|
332
|
+
commands.append(cmd)
|
|
333
|
+
else:
|
|
334
|
+
cmd = self._parse_command(cmd_str, extended_regex)
|
|
335
|
+
if cmd:
|
|
336
|
+
commands.append(cmd)
|
|
270
337
|
|
|
271
338
|
return commands
|
|
272
339
|
|
|
340
|
+
def _split_script(self, script: str) -> list[str]:
|
|
341
|
+
"""Split a script on semicolons and newlines, respecting brace nesting."""
|
|
342
|
+
parts: list[str] = []
|
|
343
|
+
current: list[str] = []
|
|
344
|
+
depth = 0
|
|
345
|
+
|
|
346
|
+
for ch in script:
|
|
347
|
+
if ch == "{":
|
|
348
|
+
depth += 1
|
|
349
|
+
current.append(ch)
|
|
350
|
+
elif ch == "}":
|
|
351
|
+
depth -= 1
|
|
352
|
+
current.append(ch)
|
|
353
|
+
elif ch in ";\n" and depth == 0:
|
|
354
|
+
parts.append("".join(current))
|
|
355
|
+
current = []
|
|
356
|
+
else:
|
|
357
|
+
current.append(ch)
|
|
358
|
+
|
|
359
|
+
if current:
|
|
360
|
+
parts.append("".join(current))
|
|
361
|
+
|
|
362
|
+
return parts
|
|
363
|
+
|
|
273
364
|
def _parse_command(self, cmd_str: str, extended_regex: bool) -> SedCommand_ | None:
|
|
274
365
|
"""Parse a single sed command."""
|
|
275
366
|
pos = 0
|
|
@@ -299,7 +390,8 @@ class SedCommand:
|
|
|
299
390
|
pattern = cmd_str[1:end]
|
|
300
391
|
flags = re.IGNORECASE if extended_regex else 0
|
|
301
392
|
try:
|
|
302
|
-
|
|
393
|
+
py_pat = pattern if extended_regex else _bre_to_python(pattern)
|
|
394
|
+
address = SedAddress(type="regex", regex=re.compile(py_pat, flags))
|
|
303
395
|
except re.error as e:
|
|
304
396
|
raise ValueError(f"invalid regex: {e}")
|
|
305
397
|
pos = end + 1
|
|
@@ -320,11 +412,12 @@ class SedCommand:
|
|
|
320
412
|
end2 = self._find_delimiter(cmd_str, pos + 1, "/")
|
|
321
413
|
if end2 != -1:
|
|
322
414
|
pattern2 = cmd_str[pos + 1:end2]
|
|
415
|
+
py_pat2 = pattern2 if extended_regex else _bre_to_python(pattern2)
|
|
323
416
|
try:
|
|
324
417
|
address = SedAddress(
|
|
325
418
|
type="range",
|
|
326
419
|
regex=address.regex,
|
|
327
|
-
end_regex=re.compile(
|
|
420
|
+
end_regex=re.compile(py_pat2, flags),
|
|
328
421
|
)
|
|
329
422
|
except re.error as e:
|
|
330
423
|
raise ValueError(f"invalid regex: {e}")
|
|
@@ -334,11 +427,21 @@ class SedCommand:
|
|
|
334
427
|
while pos < len(cmd_str) and cmd_str[pos] in " \t":
|
|
335
428
|
pos += 1
|
|
336
429
|
|
|
430
|
+
# Check for negation
|
|
431
|
+
negate = False
|
|
432
|
+
if pos < len(cmd_str) and cmd_str[pos] == "!":
|
|
433
|
+
negate = True
|
|
434
|
+
pos += 1
|
|
435
|
+
# Skip whitespace after !
|
|
436
|
+
while pos < len(cmd_str) and cmd_str[pos] in " \t":
|
|
437
|
+
pos += 1
|
|
438
|
+
|
|
337
439
|
if pos >= len(cmd_str):
|
|
338
440
|
return None
|
|
339
441
|
|
|
340
442
|
cmd_char = cmd_str[pos]
|
|
341
443
|
pos += 1
|
|
444
|
+
result = None
|
|
342
445
|
|
|
343
446
|
if cmd_char == "s":
|
|
344
447
|
# Substitution
|
|
@@ -370,11 +473,12 @@ class SedCommand:
|
|
|
370
473
|
regex_flags |= re.IGNORECASE
|
|
371
474
|
|
|
372
475
|
try:
|
|
373
|
-
|
|
476
|
+
py_pat = pattern if extended_regex else _bre_to_python(pattern)
|
|
477
|
+
compiled = re.compile(py_pat, regex_flags)
|
|
374
478
|
except re.error as e:
|
|
375
479
|
raise ValueError(f"invalid regex: {e}")
|
|
376
480
|
|
|
377
|
-
|
|
481
|
+
result = SedCommand_(
|
|
378
482
|
cmd="s",
|
|
379
483
|
address=address,
|
|
380
484
|
pattern=compiled,
|
|
@@ -404,7 +508,7 @@ class SedCommand:
|
|
|
404
508
|
if len(source) != len(dest):
|
|
405
509
|
raise ValueError("y command requires equal length strings")
|
|
406
510
|
|
|
407
|
-
|
|
511
|
+
result = SedCommand_(
|
|
408
512
|
cmd="y",
|
|
409
513
|
address=address,
|
|
410
514
|
source=source,
|
|
@@ -412,91 +516,95 @@ class SedCommand:
|
|
|
412
516
|
)
|
|
413
517
|
|
|
414
518
|
elif cmd_char == "d":
|
|
415
|
-
|
|
519
|
+
result = SedCommand_(cmd="d", address=address)
|
|
416
520
|
|
|
417
521
|
elif cmd_char == "=":
|
|
418
522
|
# Print line number
|
|
419
|
-
|
|
523
|
+
result = SedCommand_(cmd="=", address=address)
|
|
420
524
|
|
|
421
525
|
elif cmd_char == "p":
|
|
422
|
-
|
|
526
|
+
result = SedCommand_(cmd="p", address=address)
|
|
423
527
|
|
|
424
528
|
elif cmd_char == "l":
|
|
425
529
|
# List pattern space with escapes
|
|
426
|
-
|
|
530
|
+
result = SedCommand_(cmd="l", address=address)
|
|
427
531
|
|
|
428
532
|
elif cmd_char == "F":
|
|
429
533
|
# Print filename
|
|
430
|
-
|
|
534
|
+
result = SedCommand_(cmd="F", address=address)
|
|
431
535
|
|
|
432
536
|
elif cmd_char == "q":
|
|
433
|
-
|
|
537
|
+
result = SedCommand_(cmd="q", address=address)
|
|
434
538
|
|
|
435
539
|
elif cmd_char in ("a", "i", "c"):
|
|
436
540
|
# Append, insert, or change
|
|
437
541
|
text = cmd_str[pos:].lstrip()
|
|
438
542
|
if text.startswith("\\"):
|
|
439
543
|
text = text[1:].lstrip()
|
|
440
|
-
|
|
544
|
+
result = SedCommand_(cmd=cmd_char, address=address, text=text)
|
|
441
545
|
|
|
442
546
|
elif cmd_char in ("h", "H", "g", "G", "x"):
|
|
443
547
|
# Hold space commands
|
|
444
|
-
|
|
548
|
+
result = SedCommand_(cmd=cmd_char, address=address)
|
|
445
549
|
|
|
446
550
|
elif cmd_char in ("n", "N"):
|
|
447
551
|
# Next line commands
|
|
448
|
-
|
|
552
|
+
result = SedCommand_(cmd=cmd_char, address=address)
|
|
449
553
|
|
|
450
554
|
elif cmd_char in ("P", "D"):
|
|
451
555
|
# Print/delete first line of pattern space
|
|
452
|
-
|
|
556
|
+
result = SedCommand_(cmd=cmd_char, address=address)
|
|
453
557
|
|
|
454
558
|
elif cmd_char == "b":
|
|
455
559
|
# Branch to label
|
|
456
560
|
label = cmd_str[pos:].strip()
|
|
457
|
-
|
|
561
|
+
result = SedCommand_(cmd="b", address=address, label=label)
|
|
458
562
|
|
|
459
563
|
elif cmd_char == "t":
|
|
460
564
|
# Branch on successful substitute
|
|
461
565
|
label = cmd_str[pos:].strip()
|
|
462
|
-
|
|
566
|
+
result = SedCommand_(cmd="t", address=address, label=label)
|
|
463
567
|
|
|
464
568
|
elif cmd_char == "T":
|
|
465
569
|
# Branch on failed substitute
|
|
466
570
|
label = cmd_str[pos:].strip()
|
|
467
|
-
|
|
571
|
+
result = SedCommand_(cmd="T", address=address, label=label)
|
|
468
572
|
|
|
469
573
|
elif cmd_char == ":":
|
|
470
574
|
# Label definition
|
|
471
575
|
label = cmd_str[pos:].strip()
|
|
472
|
-
|
|
576
|
+
result = SedCommand_(cmd=":", label=label)
|
|
473
577
|
|
|
474
578
|
elif cmd_char == "r":
|
|
475
579
|
# Read file
|
|
476
580
|
filename = cmd_str[pos:].strip()
|
|
477
|
-
|
|
581
|
+
result = SedCommand_(cmd="r", address=address, filename=filename)
|
|
478
582
|
|
|
479
583
|
elif cmd_char == "w":
|
|
480
584
|
# Write to file
|
|
481
585
|
filename = cmd_str[pos:].strip()
|
|
482
|
-
|
|
586
|
+
result = SedCommand_(cmd="w", address=address, filename=filename)
|
|
483
587
|
|
|
484
588
|
elif cmd_char == "R":
|
|
485
589
|
# Read single line from file
|
|
486
590
|
filename = cmd_str[pos:].strip()
|
|
487
|
-
|
|
591
|
+
result = SedCommand_(cmd="R", address=address, filename=filename)
|
|
488
592
|
|
|
489
593
|
elif cmd_char == "{":
|
|
490
594
|
# Start of block (handled in parsing)
|
|
491
|
-
|
|
595
|
+
pass
|
|
492
596
|
|
|
493
597
|
elif cmd_char == "}":
|
|
494
598
|
# End of block (handled in parsing)
|
|
495
|
-
|
|
599
|
+
pass
|
|
496
600
|
|
|
497
601
|
else:
|
|
498
602
|
raise ValueError(f"unknown command: {cmd_char}")
|
|
499
603
|
|
|
604
|
+
if result is not None and negate:
|
|
605
|
+
result.negate = True
|
|
606
|
+
return result
|
|
607
|
+
|
|
500
608
|
def _find_delimiter(self, s: str, start: int, delim: str) -> int:
|
|
501
609
|
"""Find the next unescaped delimiter."""
|
|
502
610
|
i = start
|
|
@@ -570,8 +678,11 @@ class SedCommand:
|
|
|
570
678
|
cmd_idx += 1
|
|
571
679
|
continue
|
|
572
680
|
|
|
573
|
-
# Check if address matches
|
|
574
|
-
|
|
681
|
+
# Check if address matches (with negation support)
|
|
682
|
+
matches = self._address_matches(cmd.address, line_num, total_lines, pattern_space, in_range, cmd_idx)
|
|
683
|
+
if cmd.negate:
|
|
684
|
+
matches = not matches
|
|
685
|
+
if not matches:
|
|
575
686
|
cmd_idx += 1
|
|
576
687
|
continue
|
|
577
688
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Shuf command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: shuf [OPTION]... [FILE]
|
|
4
|
+
or: shuf -e [OPTION]... [ARG]...
|
|
5
|
+
or: shuf -i LO-HI [OPTION]...
|
|
6
|
+
|
|
7
|
+
Write a random permutation of the input lines to standard output.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
-e, --echo treat each ARG as an input line
|
|
11
|
+
-i, --input-range=LO-HI treat each number LO through HI as an input line
|
|
12
|
+
-n, --head-count=COUNT output at most COUNT lines
|
|
13
|
+
-o, --output=FILE write result to FILE instead of standard output
|
|
14
|
+
-r, --repeat output lines can be repeated (requires -n)
|
|
15
|
+
--random-source=FILE get random bytes from FILE
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import random
|
|
19
|
+
from ...types import CommandContext, ExecResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ShufCommand:
|
|
23
|
+
"""The shuf command."""
|
|
24
|
+
|
|
25
|
+
name = "shuf"
|
|
26
|
+
|
|
27
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
28
|
+
"""Execute the shuf command."""
|
|
29
|
+
# Parse arguments
|
|
30
|
+
echo_mode = False
|
|
31
|
+
input_range = None
|
|
32
|
+
head_count = None
|
|
33
|
+
output_file = None
|
|
34
|
+
repeat = False
|
|
35
|
+
random_source = None
|
|
36
|
+
input_args: list[str] = []
|
|
37
|
+
input_file = None
|
|
38
|
+
|
|
39
|
+
i = 0
|
|
40
|
+
while i < len(args):
|
|
41
|
+
arg = args[i]
|
|
42
|
+
|
|
43
|
+
if arg in ("-e", "--echo"):
|
|
44
|
+
echo_mode = True
|
|
45
|
+
elif arg in ("-r", "--repeat"):
|
|
46
|
+
repeat = True
|
|
47
|
+
elif arg == "-n":
|
|
48
|
+
if i + 1 >= len(args):
|
|
49
|
+
return ExecResult(
|
|
50
|
+
stdout="",
|
|
51
|
+
stderr="shuf: option requires an argument -- 'n'\n",
|
|
52
|
+
exit_code=1,
|
|
53
|
+
)
|
|
54
|
+
i += 1
|
|
55
|
+
try:
|
|
56
|
+
head_count = int(args[i])
|
|
57
|
+
if head_count < 0:
|
|
58
|
+
raise ValueError()
|
|
59
|
+
except ValueError:
|
|
60
|
+
return ExecResult(
|
|
61
|
+
stdout="",
|
|
62
|
+
stderr=f"shuf: invalid line count: '{args[i]}'\n",
|
|
63
|
+
exit_code=1,
|
|
64
|
+
)
|
|
65
|
+
elif arg.startswith("-n"):
|
|
66
|
+
try:
|
|
67
|
+
head_count = int(arg[2:])
|
|
68
|
+
if head_count < 0:
|
|
69
|
+
raise ValueError()
|
|
70
|
+
except ValueError:
|
|
71
|
+
return ExecResult(
|
|
72
|
+
stdout="",
|
|
73
|
+
stderr=f"shuf: invalid line count: '{arg[2:]}'\n",
|
|
74
|
+
exit_code=1,
|
|
75
|
+
)
|
|
76
|
+
elif arg.startswith("--head-count="):
|
|
77
|
+
try:
|
|
78
|
+
head_count = int(arg[13:])
|
|
79
|
+
if head_count < 0:
|
|
80
|
+
raise ValueError()
|
|
81
|
+
except ValueError:
|
|
82
|
+
return ExecResult(
|
|
83
|
+
stdout="",
|
|
84
|
+
stderr=f"shuf: invalid line count: '{arg[13:]}'\n",
|
|
85
|
+
exit_code=1,
|
|
86
|
+
)
|
|
87
|
+
elif arg == "-i":
|
|
88
|
+
if i + 1 >= len(args):
|
|
89
|
+
return ExecResult(
|
|
90
|
+
stdout="",
|
|
91
|
+
stderr="shuf: option requires an argument -- 'i'\n",
|
|
92
|
+
exit_code=1,
|
|
93
|
+
)
|
|
94
|
+
i += 1
|
|
95
|
+
input_range = args[i]
|
|
96
|
+
elif arg.startswith("-i"):
|
|
97
|
+
input_range = arg[2:]
|
|
98
|
+
elif arg.startswith("--input-range="):
|
|
99
|
+
input_range = arg[14:]
|
|
100
|
+
elif arg == "-o":
|
|
101
|
+
if i + 1 >= len(args):
|
|
102
|
+
return ExecResult(
|
|
103
|
+
stdout="",
|
|
104
|
+
stderr="shuf: option requires an argument -- 'o'\n",
|
|
105
|
+
exit_code=1,
|
|
106
|
+
)
|
|
107
|
+
i += 1
|
|
108
|
+
output_file = args[i]
|
|
109
|
+
elif arg.startswith("-o"):
|
|
110
|
+
output_file = arg[2:]
|
|
111
|
+
elif arg.startswith("--output="):
|
|
112
|
+
output_file = arg[9:]
|
|
113
|
+
elif arg.startswith("--random-source="):
|
|
114
|
+
random_source = arg[16:]
|
|
115
|
+
elif arg == "--random-source":
|
|
116
|
+
if i + 1 >= len(args):
|
|
117
|
+
return ExecResult(
|
|
118
|
+
stdout="",
|
|
119
|
+
stderr="shuf: option requires an argument -- 'random-source'\n",
|
|
120
|
+
exit_code=1,
|
|
121
|
+
)
|
|
122
|
+
i += 1
|
|
123
|
+
random_source = args[i]
|
|
124
|
+
elif arg.startswith("-") and len(arg) > 1 and arg != "-":
|
|
125
|
+
return ExecResult(
|
|
126
|
+
stdout="",
|
|
127
|
+
stderr=f"shuf: invalid option -- '{arg[1]}'\n",
|
|
128
|
+
exit_code=1,
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
if echo_mode:
|
|
132
|
+
input_args.append(arg)
|
|
133
|
+
elif input_file is None:
|
|
134
|
+
input_file = arg
|
|
135
|
+
else:
|
|
136
|
+
input_args.append(arg)
|
|
137
|
+
|
|
138
|
+
i += 1
|
|
139
|
+
|
|
140
|
+
# Set up random generator
|
|
141
|
+
rng = random.Random()
|
|
142
|
+
if random_source:
|
|
143
|
+
try:
|
|
144
|
+
path = ctx.fs.resolve_path(ctx.cwd, random_source)
|
|
145
|
+
seed_data = await ctx.fs.read_file(path)
|
|
146
|
+
# Use hash of file content as seed
|
|
147
|
+
rng.seed(hash(seed_data))
|
|
148
|
+
except FileNotFoundError:
|
|
149
|
+
return ExecResult(
|
|
150
|
+
stdout="",
|
|
151
|
+
stderr=f"shuf: {random_source}: No such file or directory\n",
|
|
152
|
+
exit_code=1,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Get input lines
|
|
156
|
+
lines: list[str] = []
|
|
157
|
+
|
|
158
|
+
if input_range:
|
|
159
|
+
# Parse range LO-HI
|
|
160
|
+
if "-" not in input_range:
|
|
161
|
+
return ExecResult(
|
|
162
|
+
stdout="",
|
|
163
|
+
stderr=f"shuf: invalid input range: '{input_range}'\n",
|
|
164
|
+
exit_code=1,
|
|
165
|
+
)
|
|
166
|
+
parts = input_range.split("-", 1)
|
|
167
|
+
try:
|
|
168
|
+
lo = int(parts[0])
|
|
169
|
+
hi = int(parts[1])
|
|
170
|
+
except ValueError:
|
|
171
|
+
return ExecResult(
|
|
172
|
+
stdout="",
|
|
173
|
+
stderr=f"shuf: invalid input range: '{input_range}'\n",
|
|
174
|
+
exit_code=1,
|
|
175
|
+
)
|
|
176
|
+
if lo > hi:
|
|
177
|
+
return ExecResult(
|
|
178
|
+
stdout="",
|
|
179
|
+
stderr=f"shuf: invalid input range: '{input_range}'\n",
|
|
180
|
+
exit_code=1,
|
|
181
|
+
)
|
|
182
|
+
lines = [str(n) for n in range(lo, hi + 1)]
|
|
183
|
+
elif echo_mode:
|
|
184
|
+
lines = input_args
|
|
185
|
+
else:
|
|
186
|
+
# Read from file or stdin
|
|
187
|
+
if input_file:
|
|
188
|
+
try:
|
|
189
|
+
path = ctx.fs.resolve_path(ctx.cwd, input_file)
|
|
190
|
+
content = await ctx.fs.read_file(path)
|
|
191
|
+
except FileNotFoundError:
|
|
192
|
+
return ExecResult(
|
|
193
|
+
stdout="",
|
|
194
|
+
stderr=f"shuf: {input_file}: No such file or directory\n",
|
|
195
|
+
exit_code=1,
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
content = ctx.stdin
|
|
199
|
+
|
|
200
|
+
if content:
|
|
201
|
+
# Split into lines, preserving empty lines but removing final newline
|
|
202
|
+
if content.endswith("\n"):
|
|
203
|
+
content = content[:-1]
|
|
204
|
+
if content:
|
|
205
|
+
lines = content.split("\n")
|
|
206
|
+
|
|
207
|
+
# Handle empty input
|
|
208
|
+
if not lines:
|
|
209
|
+
if output_file:
|
|
210
|
+
path = ctx.fs.resolve_path(ctx.cwd, output_file)
|
|
211
|
+
await ctx.fs.write_file(path, "")
|
|
212
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
213
|
+
|
|
214
|
+
# Generate output
|
|
215
|
+
output_lines: list[str] = []
|
|
216
|
+
|
|
217
|
+
if repeat:
|
|
218
|
+
# With repeat, we can output more lines than input
|
|
219
|
+
count = head_count if head_count is not None else len(lines)
|
|
220
|
+
for _ in range(count):
|
|
221
|
+
output_lines.append(rng.choice(lines))
|
|
222
|
+
else:
|
|
223
|
+
# Shuffle and optionally limit
|
|
224
|
+
shuffled = lines.copy()
|
|
225
|
+
rng.shuffle(shuffled)
|
|
226
|
+
if head_count is not None:
|
|
227
|
+
output_lines = shuffled[:head_count]
|
|
228
|
+
else:
|
|
229
|
+
output_lines = shuffled
|
|
230
|
+
|
|
231
|
+
# Build output
|
|
232
|
+
output = "\n".join(output_lines)
|
|
233
|
+
if output:
|
|
234
|
+
output += "\n"
|
|
235
|
+
|
|
236
|
+
# Write to file or stdout
|
|
237
|
+
if output_file:
|
|
238
|
+
path = ctx.fs.resolve_path(ctx.cwd, output_file)
|
|
239
|
+
await ctx.fs.write_file(path, output)
|
|
240
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
241
|
+
|
|
242
|
+
return ExecResult(stdout=output, stderr="", exit_code=0)
|
just_bash/commands/stat/stat.py
CHANGED
|
@@ -113,6 +113,15 @@ class StatCommand:
|
|
|
113
113
|
# %G - group name (hardcoded for virtual FS)
|
|
114
114
|
result = result.replace("%G", "group")
|
|
115
115
|
|
|
116
|
+
# %Y - modification time as seconds since Epoch
|
|
117
|
+
result = result.replace("%Y", str(int(stat.mtime)))
|
|
118
|
+
|
|
119
|
+
# %X - access time as seconds since Epoch (same as mtime for our FS)
|
|
120
|
+
result = result.replace("%X", str(int(stat.mtime)))
|
|
121
|
+
|
|
122
|
+
# %Z - change time as seconds since Epoch (same as mtime for our FS)
|
|
123
|
+
result = result.replace("%Z", str(int(stat.mtime)))
|
|
124
|
+
|
|
116
125
|
return result + "\n"
|
|
117
126
|
|
|
118
127
|
def _default_format(self, path: str, stat) -> str:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Time command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: time [-p] [COMMAND [ARGS]...]
|
|
4
|
+
|
|
5
|
+
Run COMMAND and print timing statistics.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-p Use POSIX output format
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import time as time_module
|
|
12
|
+
from ...types import CommandContext, ExecResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TimeCommand:
|
|
16
|
+
"""The time command."""
|
|
17
|
+
|
|
18
|
+
name = "time"
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
|
+
"""Execute the time command."""
|
|
22
|
+
posix_format = False
|
|
23
|
+
command_args: list[str] = []
|
|
24
|
+
|
|
25
|
+
# Parse arguments
|
|
26
|
+
i = 0
|
|
27
|
+
while i < len(args):
|
|
28
|
+
arg = args[i]
|
|
29
|
+
if arg == "-p" and not command_args:
|
|
30
|
+
posix_format = True
|
|
31
|
+
elif arg == "--":
|
|
32
|
+
command_args.extend(args[i + 1:])
|
|
33
|
+
break
|
|
34
|
+
else:
|
|
35
|
+
command_args.extend(args[i:])
|
|
36
|
+
break
|
|
37
|
+
i += 1
|
|
38
|
+
|
|
39
|
+
# If no command, just show timing for empty command
|
|
40
|
+
if not command_args:
|
|
41
|
+
if posix_format:
|
|
42
|
+
timing = "real 0.00\nuser 0.00\nsys 0.00\n"
|
|
43
|
+
else:
|
|
44
|
+
timing = "\nreal\t0m0.000s\nuser\t0m0.000s\nsys\t0m0.000s\n"
|
|
45
|
+
return ExecResult(stdout="", stderr=timing, exit_code=0)
|
|
46
|
+
|
|
47
|
+
# Execute the command and measure time
|
|
48
|
+
if ctx.exec is None:
|
|
49
|
+
return ExecResult(
|
|
50
|
+
stdout="",
|
|
51
|
+
stderr="time: cannot execute subcommand\n",
|
|
52
|
+
exit_code=1,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Build command string
|
|
56
|
+
command_str = " ".join(command_args)
|
|
57
|
+
|
|
58
|
+
start_time = time_module.time()
|
|
59
|
+
result = await ctx.exec(command_str, {"cwd": ctx.cwd})
|
|
60
|
+
elapsed = time_module.time() - start_time
|
|
61
|
+
|
|
62
|
+
# Format timing output
|
|
63
|
+
if posix_format:
|
|
64
|
+
timing = f"real {elapsed:.2f}\nuser 0.00\nsys 0.00\n"
|
|
65
|
+
else:
|
|
66
|
+
minutes = int(elapsed // 60)
|
|
67
|
+
seconds = elapsed % 60
|
|
68
|
+
timing = f"\nreal\t{minutes}m{seconds:.3f}s\nuser\t0m0.000s\nsys\t0m0.000s\n"
|
|
69
|
+
|
|
70
|
+
return ExecResult(
|
|
71
|
+
stdout=result.stdout,
|
|
72
|
+
stderr=result.stderr + timing,
|
|
73
|
+
exit_code=result.exit_code,
|
|
74
|
+
)
|