just-bash 0.1.5__py3-none-any.whl → 0.1.8__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/commands/awk/awk.py +252 -12
- just_bash/commands/grep/grep.py +111 -2
- just_bash/commands/registry.py +8 -0
- just_bash/commands/shuf/__init__.py +5 -0
- just_bash/commands/shuf/shuf.py +242 -0
- just_bash/fs/overlay_fs.py +8 -1
- just_bash/interpreter/interpreter.py +14 -1
- {just_bash-0.1.5.dist-info → just_bash-0.1.8.dist-info}/METADATA +1 -1
- {just_bash-0.1.5.dist-info → just_bash-0.1.8.dist-info}/RECORD +10 -8
- {just_bash-0.1.5.dist-info → just_bash-0.1.8.dist-info}/WHEEL +0 -0
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
|
|
@@ -60,6 +84,7 @@ class AwkState:
|
|
|
60
84
|
output: str = ""
|
|
61
85
|
next_record: bool = False
|
|
62
86
|
exit_program: bool = False
|
|
87
|
+
rng: random.Random = field(default_factory=random.Random)
|
|
63
88
|
|
|
64
89
|
|
|
65
90
|
class AwkCommand:
|
|
@@ -596,6 +621,17 @@ class AwkCommand:
|
|
|
596
621
|
pass
|
|
597
622
|
return
|
|
598
623
|
|
|
624
|
+
# Handle match() as a statement (for side effects on RSTART/RLENGTH)
|
|
625
|
+
if stmt.startswith("match("):
|
|
626
|
+
# Just evaluate it - _eval_expr will set RSTART/RLENGTH
|
|
627
|
+
self._eval_expr(stmt, state, line, fields)
|
|
628
|
+
return
|
|
629
|
+
|
|
630
|
+
# Handle srand() as a statement
|
|
631
|
+
if stmt.startswith("srand(") or stmt == "srand()":
|
|
632
|
+
self._eval_expr(stmt, state, line, fields)
|
|
633
|
+
return
|
|
634
|
+
|
|
599
635
|
# Handle assignment
|
|
600
636
|
if "=" in stmt and not stmt.startswith("if") and "==" not in stmt and "!=" not in stmt:
|
|
601
637
|
# Handle += -= *= /=
|
|
@@ -773,7 +809,6 @@ class AwkCommand:
|
|
|
773
809
|
if expr.startswith("sqrt("):
|
|
774
810
|
match = re.match(r"sqrt\((.+)\)", expr)
|
|
775
811
|
if match:
|
|
776
|
-
import math
|
|
777
812
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
778
813
|
try:
|
|
779
814
|
return math.sqrt(float(arg))
|
|
@@ -784,7 +819,6 @@ class AwkCommand:
|
|
|
784
819
|
if expr.startswith("sin("):
|
|
785
820
|
match = re.match(r"sin\((.+)\)", expr)
|
|
786
821
|
if match:
|
|
787
|
-
import math
|
|
788
822
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
789
823
|
try:
|
|
790
824
|
return math.sin(float(arg))
|
|
@@ -795,7 +829,6 @@ class AwkCommand:
|
|
|
795
829
|
if expr.startswith("cos("):
|
|
796
830
|
match = re.match(r"cos\((.+)\)", expr)
|
|
797
831
|
if match:
|
|
798
|
-
import math
|
|
799
832
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
800
833
|
try:
|
|
801
834
|
return math.cos(float(arg))
|
|
@@ -806,7 +839,6 @@ class AwkCommand:
|
|
|
806
839
|
if expr.startswith("log("):
|
|
807
840
|
match = re.match(r"log\((.+)\)", expr)
|
|
808
841
|
if match:
|
|
809
|
-
import math
|
|
810
842
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
811
843
|
try:
|
|
812
844
|
return math.log(float(arg))
|
|
@@ -817,7 +849,6 @@ class AwkCommand:
|
|
|
817
849
|
if expr.startswith("exp("):
|
|
818
850
|
match = re.match(r"exp\((.+)\)", expr)
|
|
819
851
|
if match:
|
|
820
|
-
import math
|
|
821
852
|
arg = self._eval_expr(match.group(1), state, line, fields)
|
|
822
853
|
try:
|
|
823
854
|
return math.exp(float(arg))
|
|
@@ -845,6 +876,104 @@ class AwkCommand:
|
|
|
845
876
|
return len(parts)
|
|
846
877
|
return 0
|
|
847
878
|
|
|
879
|
+
# rand() - return random number 0 <= n < 1
|
|
880
|
+
if expr == "rand()":
|
|
881
|
+
return state.rng.random()
|
|
882
|
+
|
|
883
|
+
# srand([seed]) - seed the random number generator
|
|
884
|
+
if expr.startswith("srand(") or expr == "srand()":
|
|
885
|
+
match = re.match(r"srand\(([^)]*)\)", expr)
|
|
886
|
+
if match:
|
|
887
|
+
seed_str = match.group(1).strip()
|
|
888
|
+
if seed_str:
|
|
889
|
+
seed = self._eval_expr(seed_str, state, line, fields)
|
|
890
|
+
try:
|
|
891
|
+
state.rng.seed(int(float(seed)))
|
|
892
|
+
except (ValueError, TypeError):
|
|
893
|
+
state.rng.seed(int(time.time()))
|
|
894
|
+
else:
|
|
895
|
+
state.rng.seed(int(time.time()))
|
|
896
|
+
return 0 # srand returns previous seed, but we just return 0
|
|
897
|
+
|
|
898
|
+
# sprintf(fmt, args...) - return formatted string
|
|
899
|
+
if expr.startswith("sprintf("):
|
|
900
|
+
match = re.match(r"sprintf\((.+)\)", expr)
|
|
901
|
+
if match:
|
|
902
|
+
args = self._split_args(match.group(1))
|
|
903
|
+
if args:
|
|
904
|
+
fmt = str(self._eval_expr(args[0], state, line, fields))
|
|
905
|
+
values = [self._eval_expr(a, state, line, fields) for a in args[1:]]
|
|
906
|
+
return self._format_string(fmt, values)
|
|
907
|
+
return ""
|
|
908
|
+
|
|
909
|
+
# match(s, r) - return position of regex match, set RSTART and RLENGTH
|
|
910
|
+
if expr.startswith("match("):
|
|
911
|
+
match_call = re.match(r"match\((.+)\)", expr)
|
|
912
|
+
if match_call:
|
|
913
|
+
args = self._split_args(match_call.group(1))
|
|
914
|
+
if len(args) >= 2:
|
|
915
|
+
s = str(self._eval_expr(args[0], state, line, fields))
|
|
916
|
+
pattern = args[1].strip()
|
|
917
|
+
# Handle /regex/ syntax
|
|
918
|
+
if pattern.startswith("/") and pattern.endswith("/"):
|
|
919
|
+
pattern = pattern[1:-1]
|
|
920
|
+
try:
|
|
921
|
+
regex_match = re.search(pattern, s)
|
|
922
|
+
if regex_match:
|
|
923
|
+
pos = regex_match.start() + 1 # 1-based
|
|
924
|
+
length = regex_match.end() - regex_match.start()
|
|
925
|
+
state.variables["RSTART"] = pos
|
|
926
|
+
state.variables["RLENGTH"] = length
|
|
927
|
+
return pos
|
|
928
|
+
else:
|
|
929
|
+
state.variables["RSTART"] = 0
|
|
930
|
+
state.variables["RLENGTH"] = -1
|
|
931
|
+
return 0
|
|
932
|
+
except re.error:
|
|
933
|
+
state.variables["RSTART"] = 0
|
|
934
|
+
state.variables["RLENGTH"] = -1
|
|
935
|
+
return 0
|
|
936
|
+
return 0
|
|
937
|
+
|
|
938
|
+
# atan2(y, x) - arctangent of y/x
|
|
939
|
+
if expr.startswith("atan2("):
|
|
940
|
+
match = re.match(r"atan2\((.+)\)", expr)
|
|
941
|
+
if match:
|
|
942
|
+
args = self._split_args(match.group(1))
|
|
943
|
+
if len(args) >= 2:
|
|
944
|
+
y = self._eval_expr(args[0], state, line, fields)
|
|
945
|
+
x = self._eval_expr(args[1], state, line, fields)
|
|
946
|
+
try:
|
|
947
|
+
return math.atan2(float(y), float(x))
|
|
948
|
+
except (ValueError, TypeError):
|
|
949
|
+
return 0
|
|
950
|
+
return 0
|
|
951
|
+
|
|
952
|
+
# systime() - return current epoch timestamp
|
|
953
|
+
if expr == "systime()":
|
|
954
|
+
return int(time.time())
|
|
955
|
+
|
|
956
|
+
# strftime(fmt, timestamp) - format timestamp as string
|
|
957
|
+
if expr.startswith("strftime("):
|
|
958
|
+
match = re.match(r"strftime\((.+)\)", expr)
|
|
959
|
+
if match:
|
|
960
|
+
args = self._split_args(match.group(1))
|
|
961
|
+
if args:
|
|
962
|
+
fmt = str(self._eval_expr(args[0], state, line, fields))
|
|
963
|
+
if len(args) >= 2:
|
|
964
|
+
timestamp = self._eval_expr(args[1], state, line, fields)
|
|
965
|
+
try:
|
|
966
|
+
timestamp = int(float(timestamp))
|
|
967
|
+
except (ValueError, TypeError):
|
|
968
|
+
timestamp = int(time.time())
|
|
969
|
+
else:
|
|
970
|
+
timestamp = int(time.time())
|
|
971
|
+
try:
|
|
972
|
+
return time.strftime(fmt, time.localtime(timestamp))
|
|
973
|
+
except (ValueError, OSError):
|
|
974
|
+
return ""
|
|
975
|
+
return ""
|
|
976
|
+
|
|
848
977
|
# Arithmetic - check for operators (including with spaces like "2 + 3")
|
|
849
978
|
for op in ["+", "-", "*", "/", "%"]:
|
|
850
979
|
if op in expr:
|
|
@@ -1010,17 +1139,70 @@ class AwkCommand:
|
|
|
1010
1139
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
1011
1140
|
) -> None:
|
|
1012
1141
|
"""Execute an if statement."""
|
|
1013
|
-
#
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1142
|
+
# Find the condition by matching balanced parentheses
|
|
1143
|
+
if not stmt.startswith("if"):
|
|
1144
|
+
return
|
|
1145
|
+
|
|
1146
|
+
# Find opening paren
|
|
1147
|
+
paren_start = stmt.find("(")
|
|
1148
|
+
if paren_start == -1:
|
|
1149
|
+
return
|
|
1150
|
+
|
|
1151
|
+
# Find matching closing paren
|
|
1152
|
+
depth = 1
|
|
1153
|
+
pos = paren_start + 1
|
|
1154
|
+
while pos < len(stmt) and depth > 0:
|
|
1155
|
+
if stmt[pos] == "(":
|
|
1156
|
+
depth += 1
|
|
1157
|
+
elif stmt[pos] == ")":
|
|
1158
|
+
depth -= 1
|
|
1159
|
+
pos += 1
|
|
1160
|
+
|
|
1161
|
+
if depth != 0:
|
|
1162
|
+
return
|
|
1163
|
+
|
|
1164
|
+
condition = stmt[paren_start + 1:pos - 1]
|
|
1165
|
+
rest = stmt[pos:].strip()
|
|
1166
|
+
|
|
1167
|
+
# Check for braced then-action
|
|
1168
|
+
if rest.startswith("{"):
|
|
1169
|
+
# Find matching closing brace
|
|
1170
|
+
brace_depth = 1
|
|
1171
|
+
brace_pos = 1
|
|
1172
|
+
while brace_pos < len(rest) and brace_depth > 0:
|
|
1173
|
+
if rest[brace_pos] == "{":
|
|
1174
|
+
brace_depth += 1
|
|
1175
|
+
elif rest[brace_pos] == "}":
|
|
1176
|
+
brace_depth -= 1
|
|
1177
|
+
brace_pos += 1
|
|
1178
|
+
|
|
1179
|
+
then_action = rest[1:brace_pos - 1]
|
|
1180
|
+
after_then = rest[brace_pos:].strip()
|
|
1181
|
+
|
|
1182
|
+
# Check for else
|
|
1183
|
+
else_action = None
|
|
1184
|
+
if after_then.startswith("else"):
|
|
1185
|
+
else_rest = after_then[4:].strip()
|
|
1186
|
+
if else_rest.startswith("{"):
|
|
1187
|
+
# Find matching brace for else
|
|
1188
|
+
brace_depth = 1
|
|
1189
|
+
brace_pos = 1
|
|
1190
|
+
while brace_pos < len(else_rest) and brace_depth > 0:
|
|
1191
|
+
if else_rest[brace_pos] == "{":
|
|
1192
|
+
brace_depth += 1
|
|
1193
|
+
elif else_rest[brace_pos] == "}":
|
|
1194
|
+
brace_depth -= 1
|
|
1195
|
+
brace_pos += 1
|
|
1196
|
+
else_action = else_rest[1:brace_pos - 1]
|
|
1019
1197
|
|
|
1020
1198
|
if self._eval_condition(condition, state, fields, line):
|
|
1021
1199
|
self._execute_action(then_action, state, fields, line)
|
|
1022
1200
|
elif else_action:
|
|
1023
1201
|
self._execute_action(else_action, state, fields, line)
|
|
1202
|
+
else:
|
|
1203
|
+
# No braces - rest is the statement
|
|
1204
|
+
if self._eval_condition(condition, state, fields, line):
|
|
1205
|
+
self._execute_statement(rest, state, fields, line)
|
|
1024
1206
|
|
|
1025
1207
|
def _execute_for(
|
|
1026
1208
|
self, stmt: str, state: AwkState, fields: list[str], line: str
|
|
@@ -1166,3 +1348,61 @@ class AwkCommand:
|
|
|
1166
1348
|
return str(int(n))
|
|
1167
1349
|
return str(n)
|
|
1168
1350
|
return str(n)
|
|
1351
|
+
|
|
1352
|
+
def _format_string(self, fmt: str, values: list[Any]) -> str:
|
|
1353
|
+
"""Format a string using printf-style format specifiers."""
|
|
1354
|
+
result = ""
|
|
1355
|
+
i = 0
|
|
1356
|
+
val_idx = 0
|
|
1357
|
+
|
|
1358
|
+
while i < len(fmt):
|
|
1359
|
+
if fmt[i] == "\\" and i + 1 < len(fmt):
|
|
1360
|
+
c = fmt[i + 1]
|
|
1361
|
+
if c == "n":
|
|
1362
|
+
result += "\n"
|
|
1363
|
+
elif c == "t":
|
|
1364
|
+
result += "\t"
|
|
1365
|
+
elif c == "\\":
|
|
1366
|
+
result += "\\"
|
|
1367
|
+
else:
|
|
1368
|
+
result += c
|
|
1369
|
+
i += 2
|
|
1370
|
+
elif fmt[i] == "%" and i + 1 < len(fmt):
|
|
1371
|
+
# Parse format spec
|
|
1372
|
+
j = i + 1
|
|
1373
|
+
while j < len(fmt) and fmt[j] in "-+0 #":
|
|
1374
|
+
j += 1
|
|
1375
|
+
while j < len(fmt) and fmt[j].isdigit():
|
|
1376
|
+
j += 1
|
|
1377
|
+
if j < len(fmt) and fmt[j] == ".":
|
|
1378
|
+
j += 1
|
|
1379
|
+
while j < len(fmt) and fmt[j].isdigit():
|
|
1380
|
+
j += 1
|
|
1381
|
+
if j < len(fmt):
|
|
1382
|
+
spec = fmt[i:j + 1]
|
|
1383
|
+
conv = fmt[j]
|
|
1384
|
+
if conv == "%":
|
|
1385
|
+
result += "%"
|
|
1386
|
+
elif val_idx < len(values):
|
|
1387
|
+
val = values[val_idx]
|
|
1388
|
+
val_idx += 1
|
|
1389
|
+
try:
|
|
1390
|
+
if conv in "diouxX":
|
|
1391
|
+
result += spec % int(float(val))
|
|
1392
|
+
elif conv in "eEfFgG":
|
|
1393
|
+
result += spec % float(val)
|
|
1394
|
+
elif conv == "s":
|
|
1395
|
+
result += spec % str(val)
|
|
1396
|
+
else:
|
|
1397
|
+
result += spec % val
|
|
1398
|
+
except (ValueError, TypeError):
|
|
1399
|
+
result += str(val)
|
|
1400
|
+
i = j + 1
|
|
1401
|
+
else:
|
|
1402
|
+
result += fmt[i]
|
|
1403
|
+
i += 1
|
|
1404
|
+
else:
|
|
1405
|
+
result += fmt[i]
|
|
1406
|
+
i += 1
|
|
1407
|
+
|
|
1408
|
+
return result
|
just_bash/commands/grep/grep.py
CHANGED
|
@@ -203,8 +203,18 @@ class GrepCommand:
|
|
|
203
203
|
if pattern:
|
|
204
204
|
# If pattern was set, it's actually a file
|
|
205
205
|
files.insert(0, pattern)
|
|
206
|
-
pattern
|
|
207
|
-
|
|
206
|
+
# Convert each pattern from BRE before combining (if not ERE mode)
|
|
207
|
+
if not extended_regexp and not fixed_strings:
|
|
208
|
+
converted = [self._bre_to_python_regex(p) for p in patterns]
|
|
209
|
+
pattern = "|".join(f"({p})" for p in converted)
|
|
210
|
+
else:
|
|
211
|
+
pattern = "|".join(f"({p})" for p in patterns)
|
|
212
|
+
# Mark as already converted
|
|
213
|
+
patterns_already_converted = True
|
|
214
|
+
else:
|
|
215
|
+
patterns_already_converted = False
|
|
216
|
+
|
|
217
|
+
if pattern is None and not patterns:
|
|
208
218
|
return ExecResult(
|
|
209
219
|
stdout="",
|
|
210
220
|
stderr="grep: pattern not specified\n",
|
|
@@ -220,6 +230,10 @@ class GrepCommand:
|
|
|
220
230
|
if fixed_strings:
|
|
221
231
|
# Escape all regex metacharacters
|
|
222
232
|
pattern = re.escape(pattern)
|
|
233
|
+
elif not extended_regexp and not patterns_already_converted:
|
|
234
|
+
# Convert BRE (Basic Regular Expression) to Python regex
|
|
235
|
+
pattern = self._bre_to_python_regex(pattern)
|
|
236
|
+
|
|
223
237
|
if word_regexp:
|
|
224
238
|
pattern = r'\b' + pattern + r'\b'
|
|
225
239
|
if line_regexp:
|
|
@@ -382,6 +396,101 @@ class GrepCommand:
|
|
|
382
396
|
exit_code = 0 if found_match else 1
|
|
383
397
|
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
384
398
|
|
|
399
|
+
def _bre_to_python_regex(self, pattern: str) -> str:
|
|
400
|
+
"""Convert BRE (Basic Regular Expression) to Python regex.
|
|
401
|
+
|
|
402
|
+
In BRE:
|
|
403
|
+
- \\| is alternation, | is literal
|
|
404
|
+
- \\+ is one-or-more, + is literal
|
|
405
|
+
- \\? is zero-or-one, ? is literal
|
|
406
|
+
- \\( \\) is grouping, ( ) are literal
|
|
407
|
+
- \\{ \\} is repetition, { } are literal
|
|
408
|
+
- \\< \\> is word boundary
|
|
409
|
+
|
|
410
|
+
In Python regex (like ERE):
|
|
411
|
+
- | is alternation, \\| is literal
|
|
412
|
+
- + is one-or-more, \\+ is literal
|
|
413
|
+
- etc.
|
|
414
|
+
"""
|
|
415
|
+
result = []
|
|
416
|
+
i = 0
|
|
417
|
+
while i < len(pattern):
|
|
418
|
+
if pattern[i] == '\\' and i + 1 < len(pattern):
|
|
419
|
+
next_char = pattern[i + 1]
|
|
420
|
+
if next_char == '|':
|
|
421
|
+
# BRE \| -> Python |
|
|
422
|
+
result.append('|')
|
|
423
|
+
i += 2
|
|
424
|
+
elif next_char == '+':
|
|
425
|
+
# BRE \+ -> Python +
|
|
426
|
+
result.append('+')
|
|
427
|
+
i += 2
|
|
428
|
+
elif next_char == '?':
|
|
429
|
+
# BRE \? -> Python ?
|
|
430
|
+
result.append('?')
|
|
431
|
+
i += 2
|
|
432
|
+
elif next_char == '(':
|
|
433
|
+
# BRE \( -> Python (
|
|
434
|
+
result.append('(')
|
|
435
|
+
i += 2
|
|
436
|
+
elif next_char == ')':
|
|
437
|
+
# BRE \) -> Python )
|
|
438
|
+
result.append(')')
|
|
439
|
+
i += 2
|
|
440
|
+
elif next_char == '{':
|
|
441
|
+
# BRE \{ -> Python {
|
|
442
|
+
result.append('{')
|
|
443
|
+
i += 2
|
|
444
|
+
elif next_char == '}':
|
|
445
|
+
# BRE \} -> Python }
|
|
446
|
+
result.append('}')
|
|
447
|
+
i += 2
|
|
448
|
+
elif next_char == '<':
|
|
449
|
+
# BRE \< (word start) -> Python \b
|
|
450
|
+
result.append(r'\b')
|
|
451
|
+
i += 2
|
|
452
|
+
elif next_char == '>':
|
|
453
|
+
# BRE \> (word end) -> Python \b
|
|
454
|
+
result.append(r'\b')
|
|
455
|
+
i += 2
|
|
456
|
+
else:
|
|
457
|
+
# Other escapes pass through as-is
|
|
458
|
+
result.append(pattern[i:i + 2])
|
|
459
|
+
i += 2
|
|
460
|
+
elif pattern[i] == '|':
|
|
461
|
+
# BRE literal | -> Python \|
|
|
462
|
+
result.append(r'\|')
|
|
463
|
+
i += 1
|
|
464
|
+
elif pattern[i] == '+':
|
|
465
|
+
# BRE literal + -> Python \+
|
|
466
|
+
result.append(r'\+')
|
|
467
|
+
i += 1
|
|
468
|
+
elif pattern[i] == '?':
|
|
469
|
+
# BRE literal ? -> Python \?
|
|
470
|
+
result.append(r'\?')
|
|
471
|
+
i += 1
|
|
472
|
+
elif pattern[i] == '(':
|
|
473
|
+
# BRE literal ( -> Python \(
|
|
474
|
+
result.append(r'\(')
|
|
475
|
+
i += 1
|
|
476
|
+
elif pattern[i] == ')':
|
|
477
|
+
# BRE literal ) -> Python \)
|
|
478
|
+
result.append(r'\)')
|
|
479
|
+
i += 1
|
|
480
|
+
elif pattern[i] == '{':
|
|
481
|
+
# BRE literal { -> Python \{
|
|
482
|
+
result.append(r'\{')
|
|
483
|
+
i += 1
|
|
484
|
+
elif pattern[i] == '}':
|
|
485
|
+
# BRE literal } -> Python \}
|
|
486
|
+
result.append(r'\}')
|
|
487
|
+
i += 1
|
|
488
|
+
else:
|
|
489
|
+
result.append(pattern[i])
|
|
490
|
+
i += 1
|
|
491
|
+
|
|
492
|
+
return ''.join(result)
|
|
493
|
+
|
|
385
494
|
async def _get_files_recursive(self, ctx: CommandContext, path: str) -> list[str]:
|
|
386
495
|
"""Get all files in a directory recursively."""
|
|
387
496
|
files = []
|
just_bash/commands/registry.py
CHANGED
|
@@ -113,6 +113,7 @@ COMMAND_NAMES = [
|
|
|
113
113
|
"timeout",
|
|
114
114
|
"seq",
|
|
115
115
|
"expr",
|
|
116
|
+
"shuf",
|
|
116
117
|
# Checksums
|
|
117
118
|
"md5sum",
|
|
118
119
|
"sha1sum",
|
|
@@ -186,6 +187,7 @@ _command_loaders: list[LazyCommandDef] = [
|
|
|
186
187
|
LazyCommandDef(name="date", load=lambda: _load_date()),
|
|
187
188
|
LazyCommandDef(name="seq", load=lambda: _load_seq()),
|
|
188
189
|
LazyCommandDef(name="expr", load=lambda: _load_expr()),
|
|
190
|
+
LazyCommandDef(name="shuf", load=lambda: _load_shuf()),
|
|
189
191
|
# JSON processing
|
|
190
192
|
LazyCommandDef(name="jq", load=lambda: _load_jq()),
|
|
191
193
|
# High priority commands
|
|
@@ -389,6 +391,12 @@ async def _load_expr() -> Command:
|
|
|
389
391
|
return ExprCommand()
|
|
390
392
|
|
|
391
393
|
|
|
394
|
+
async def _load_shuf() -> Command:
|
|
395
|
+
"""Load the shuf command."""
|
|
396
|
+
from .shuf.shuf import ShufCommand
|
|
397
|
+
return ShufCommand()
|
|
398
|
+
|
|
399
|
+
|
|
392
400
|
async def _load_uniq() -> Command:
|
|
393
401
|
"""Load the uniq command."""
|
|
394
402
|
from .uniq.uniq import UniqCommand
|
|
@@ -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/fs/overlay_fs.py
CHANGED
|
@@ -155,6 +155,9 @@ class OverlayFs:
|
|
|
155
155
|
|
|
156
156
|
def _is_under_mount(self, path: str) -> bool:
|
|
157
157
|
"""Check if a normalized path is under the mount point."""
|
|
158
|
+
# Special case: root mount point means all paths are under it
|
|
159
|
+
if self._mount_point == "/":
|
|
160
|
+
return True
|
|
158
161
|
return path == self._mount_point or path.startswith(self._mount_point + "/")
|
|
159
162
|
|
|
160
163
|
def _to_real_path(self, virtual_path: str) -> Path | None:
|
|
@@ -172,7 +175,11 @@ class OverlayFs:
|
|
|
172
175
|
return self._root
|
|
173
176
|
|
|
174
177
|
# Strip mount point prefix
|
|
175
|
-
|
|
178
|
+
# Special case: when mount_point is "/", just strip the leading "/"
|
|
179
|
+
if self._mount_point == "/":
|
|
180
|
+
relative = normalized[1:] # Just strip the leading /
|
|
181
|
+
else:
|
|
182
|
+
relative = normalized[len(self._mount_point) + 1:] # +1 for the /
|
|
176
183
|
return self._root / relative
|
|
177
184
|
|
|
178
185
|
def _is_deleted(self, path: str) -> bool:
|
|
@@ -543,7 +543,9 @@ class Interpreter:
|
|
|
543
543
|
if node.name is None:
|
|
544
544
|
return _ok()
|
|
545
545
|
|
|
546
|
-
# Process redirections for heredocs
|
|
546
|
+
# Process redirections for heredocs and input redirections
|
|
547
|
+
from ..ast.types import WordNode
|
|
548
|
+
|
|
547
549
|
for redir in node.redirections:
|
|
548
550
|
if redir.operator in ("<<", "<<-"):
|
|
549
551
|
# Here-document: the target should be a HereDocNode
|
|
@@ -557,6 +559,17 @@ class Interpreter:
|
|
|
557
559
|
lines = heredoc_content.split("\n")
|
|
558
560
|
heredoc_content = "\n".join(line.lstrip("\t") for line in lines)
|
|
559
561
|
stdin = heredoc_content
|
|
562
|
+
elif redir.operator == "<":
|
|
563
|
+
# Input redirection: read file content into stdin
|
|
564
|
+
if redir.target is not None and isinstance(redir.target, WordNode):
|
|
565
|
+
target_path = await expand_word_async(self._ctx, redir.target)
|
|
566
|
+
target_path = self._fs.resolve_path(self._state.cwd, target_path)
|
|
567
|
+
try:
|
|
568
|
+
stdin = await self._fs.read_file(target_path)
|
|
569
|
+
except FileNotFoundError:
|
|
570
|
+
return _failure(f"bash: {target_path}: No such file or directory\n")
|
|
571
|
+
except IsADirectoryError:
|
|
572
|
+
return _failure(f"bash: {target_path}: Is a directory\n")
|
|
560
573
|
|
|
561
574
|
try:
|
|
562
575
|
# Expand command name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: just-bash
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A pure Python bash interpreter with in-memory virtual filesystem
|
|
5
5
|
Project-URL: Homepage, https://github.com/dbreunig/just-bash-py
|
|
6
6
|
Project-URL: Repository, https://github.com/dbreunig/just-bash-py
|
|
@@ -6,11 +6,11 @@ just_bash/ast/__init__.py,sha256=yItXk_RIPHj3Op6JKBKjxdyZaxW_1lTg5d-L-KHgDLc,457
|
|
|
6
6
|
just_bash/ast/factory.py,sha256=TPlE5uT2XAqViKiHtJILw5vpeqrAKRUfzLQAwIb19y4,8268
|
|
7
7
|
just_bash/ast/types.py,sha256=o8MdwIirjAcA1HgvQaOmrK5iwSWpYOlvvnRdkbvTBck,26574
|
|
8
8
|
just_bash/commands/__init__.py,sha256=sKNb1FjmMY56svg4BJuVgm0ljhbpVl_tMKcx3jFNdE8,523
|
|
9
|
-
just_bash/commands/registry.py,sha256=
|
|
9
|
+
just_bash/commands/registry.py,sha256=luqpSlvVD4TxhoG1sqEVKzCdt0sez8qYH6-Cb5KreqU,22168
|
|
10
10
|
just_bash/commands/argv/__init__.py,sha256=QrGig0hlCcxkOYGzqWsw_bWVOFwxxLLeukU7wAiGDp4,85
|
|
11
11
|
just_bash/commands/argv/argv.py,sha256=Qbn7gIjplMA3HSB7scnLd0wOerjcwuTfg_m5HmyTYIE,560
|
|
12
12
|
just_bash/commands/awk/__init__.py,sha256=ZpgMK6A0QsApd3GyCIvpkmL3iPf7FaUPav13bdUsZhk,89
|
|
13
|
-
just_bash/commands/awk/awk.py,sha256=
|
|
13
|
+
just_bash/commands/awk/awk.py,sha256=_xA7b2eKvAaxF6VXdkc1gni4nMLWwGWSJ-ScFdKUO10,50164
|
|
14
14
|
just_bash/commands/base64/__init__.py,sha256=0hKkKgCi7m_TDz83hd3KaFJgjmBDjIqfLx7bctx98xA,101
|
|
15
15
|
just_bash/commands/base64/base64.py,sha256=UtzE0e-3MAYm_61anfI3Vxm36UlOB4X9fobl9ySV8s0,4927
|
|
16
16
|
just_bash/commands/basename/__init__.py,sha256=mEapj7eo70RrVGjwhVj9S9idWrBxptOPu6uV6z_cteo,94
|
|
@@ -58,7 +58,7 @@ just_bash/commands/find/find.py,sha256=I2Qmn3cSUvyiVfdh2XIYJqACpe2ggiPdxnNhPlN2d
|
|
|
58
58
|
just_bash/commands/fold/__init__.py,sha256=R0PCq0oBJ5PWEXIeRz-M1vuNkoRdvWLaSSiXyEx9nKQ,78
|
|
59
59
|
just_bash/commands/fold/fold.py,sha256=QWGehi7vdVhcAGxaAZ4lWYHAuWquduhFb515zpKL09M,5417
|
|
60
60
|
just_bash/commands/grep/__init__.py,sha256=Ho400PkJRLXBf5Rt7tYF4uiS8Dbv4VhWcfkQNzqgxB4,153
|
|
61
|
-
just_bash/commands/grep/grep.py,sha256=
|
|
61
|
+
just_bash/commands/grep/grep.py,sha256=GKmcSSKAjNnwwuAaNEBYPQsjTp_Rj1rB3g6mffQS5Uc,20599
|
|
62
62
|
just_bash/commands/head/__init__.py,sha256=UTgOtL29ZxbqIKjEXC0EjaxmlnWD8dYFFbW4YruJCDI,93
|
|
63
63
|
just_bash/commands/head/head.py,sha256=41Ib8Z5uIBiiISWB-My9CAK7nDfpQJnnS7vkRwkA2yo,6083
|
|
64
64
|
just_bash/commands/help/__init__.py,sha256=F-NY6nt_1vpSm4qPDd93-muE6Pz_K68wy3-_PpMNvlE,78
|
|
@@ -108,6 +108,8 @@ just_bash/commands/seq/__init__.py,sha256=WoS9bcCPBCi4kEAwqmUSyxo-1ekumaJ-QFJYLN
|
|
|
108
108
|
just_bash/commands/seq/seq.py,sha256=cBfVJqbZMmXXXUA2FcXDJk8H2yZpTXqKLgy6-f5vciE,6485
|
|
109
109
|
just_bash/commands/shell/__init__.py,sha256=7FUmyHPZv-ujikzHzks8SRTPYScfs9vr_9KJ8Ny72XM,189
|
|
110
110
|
just_bash/commands/shell/shell.py,sha256=NEuSVfXz-L-0L2ev6M5ir4S9z6TvEN8-mIJgSlV1wjg,6753
|
|
111
|
+
just_bash/commands/shuf/__init__.py,sha256=0xLdf5wRMK4vfOoCwDWdOisHy09PGuJnkIp0oRnBhjM,78
|
|
112
|
+
just_bash/commands/shuf/shuf.py,sha256=BDGkvKqYvAq1I6ZGetBfvOzsaSjznwfIc2vPZ4yhoSs,8523
|
|
111
113
|
just_bash/commands/sleep/__init__.py,sha256=c2BuhU78G38aj6zOZwplWhtqmbf0ydfv8y_cIdc9N3I,82
|
|
112
114
|
just_bash/commands/sleep/sleep.py,sha256=tKXLGndy43nDgT-gO5jLnclkmMY_k2mU7cZh3Hxcb2M,1710
|
|
113
115
|
just_bash/commands/sort/__init__.py,sha256=97Zp6z7YGf6HLvhVYsN9atRXUDfCTxr7GrfsQmY0fBY,93
|
|
@@ -153,14 +155,14 @@ just_bash/commands/yq/yq.py,sha256=Av6QQSHOvV5pp11lgvK22bJR0SEzQNuNkVvXbznc5a0,2
|
|
|
153
155
|
just_bash/fs/__init__.py,sha256=hEDypfPi7T7tHOIyf97JOcoryKjd9fF_XAShDUyGj58,636
|
|
154
156
|
just_bash/fs/in_memory_fs.py,sha256=I7pnAYyrZVTEBr05XV3cbetENAUgnqW7D-rt93RFmQQ,21347
|
|
155
157
|
just_bash/fs/mountable_fs.py,sha256=xhYLRllN1mCScL4tG8q0JXyanEntyZLxMYK6hvc3Pg4,17774
|
|
156
|
-
just_bash/fs/overlay_fs.py,sha256=
|
|
158
|
+
just_bash/fs/overlay_fs.py,sha256=9ZKCw_xtH8EFXvrs1kz2sDtyf73HoVJYLgSdKGyxEIQ,30733
|
|
157
159
|
just_bash/fs/read_write_fs.py,sha256=7auXvArIuKlmbQGxPNSpV8tR48tJp4mfqUhrBNEpbXc,14813
|
|
158
160
|
just_bash/interpreter/__init__.py,sha256=ud2zeA0Ly0rMjhzY9zEOnLzukUi7FnJYJ0ZCMnTEuhA,790
|
|
159
161
|
just_bash/interpreter/conditionals.py,sha256=wRMRiEMevhJom6KJbYC8Rxb7mfQdOmUG3cOJk8iQXDk,11169
|
|
160
162
|
just_bash/interpreter/control_flow.py,sha256=k-ZDCXslQ4EJc9N1GbvDmfzPNoavpL36qZFgifLJ6A4,11997
|
|
161
163
|
just_bash/interpreter/errors.py,sha256=Nz_dbvGxCybfnxFSTxKD5QIEQBz0Y6SrRA4KDhq2n20,3585
|
|
162
164
|
just_bash/interpreter/expansion.py,sha256=Yc-yKt16MjsjFrdYNJ-e1JdN_B1jS5G85JvRmv6TuIM,41584
|
|
163
|
-
just_bash/interpreter/interpreter.py,sha256=
|
|
165
|
+
just_bash/interpreter/interpreter.py,sha256=LnQEuSHJoCY1B5hdp8WzQkh_vfjsVw04xyfNwwaObHE,30719
|
|
164
166
|
just_bash/interpreter/types.py,sha256=t8v7sRSVf0_sVmXWKxAV3x1pi2vzun3yi97qtq8wanY,3992
|
|
165
167
|
just_bash/interpreter/builtins/__init__.py,sha256=uGh2b8ij3DQ4oxYodRyfMhOWTFdRHPa3wuqMfOBw_ms,2461
|
|
166
168
|
just_bash/interpreter/builtins/alias.py,sha256=b575DQKcNbT2uNK-7YBsPEyNZ54-YjjduUaoQ6-z150,4017
|
|
@@ -188,6 +190,6 @@ just_bash/query_engine/parser.py,sha256=pHw-il3AoaIbuvL9MBuxgv4BVsPCJjTl3OHEZmqe
|
|
|
188
190
|
just_bash/query_engine/tokenizer.py,sha256=PfaHJ8Y8N_KkFCiti7iGP4Le4A2JjAhg3OtAMJuNIBo,10161
|
|
189
191
|
just_bash/query_engine/types.py,sha256=zhUj2FvClAANpWvcvXSyvWBxT-g2jAczCiE7kNQzHrM,7272
|
|
190
192
|
just_bash/query_engine/builtins/__init__.py,sha256=Rs7TjJ0rjFmL9GyQXBbU4H0k9WLbgDlT31_mhpd-1aw,40315
|
|
191
|
-
just_bash-0.1.
|
|
192
|
-
just_bash-0.1.
|
|
193
|
-
just_bash-0.1.
|
|
193
|
+
just_bash-0.1.8.dist-info/METADATA,sha256=wItRiamHJQtxPWo9XJqw5AnxLaba052xs-ORi-YuKQM,12809
|
|
194
|
+
just_bash-0.1.8.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
195
|
+
just_bash-0.1.8.dist-info/RECORD,,
|
|
File without changes
|