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.
Files changed (49) hide show
  1. just_bash/ast/factory.py +3 -1
  2. just_bash/bash.py +28 -6
  3. just_bash/commands/awk/awk.py +362 -17
  4. just_bash/commands/cat/cat.py +5 -1
  5. just_bash/commands/echo/echo.py +33 -1
  6. just_bash/commands/grep/grep.py +141 -3
  7. just_bash/commands/od/od.py +144 -30
  8. just_bash/commands/printf/printf.py +289 -87
  9. just_bash/commands/pwd/pwd.py +32 -2
  10. just_bash/commands/read/read.py +243 -64
  11. just_bash/commands/readlink/readlink.py +3 -9
  12. just_bash/commands/registry.py +32 -0
  13. just_bash/commands/rmdir/__init__.py +5 -0
  14. just_bash/commands/rmdir/rmdir.py +160 -0
  15. just_bash/commands/sed/sed.py +142 -31
  16. just_bash/commands/shuf/__init__.py +5 -0
  17. just_bash/commands/shuf/shuf.py +242 -0
  18. just_bash/commands/stat/stat.py +9 -0
  19. just_bash/commands/time/__init__.py +5 -0
  20. just_bash/commands/time/time.py +74 -0
  21. just_bash/commands/touch/touch.py +118 -8
  22. just_bash/commands/whoami/__init__.py +5 -0
  23. just_bash/commands/whoami/whoami.py +18 -0
  24. just_bash/fs/in_memory_fs.py +22 -0
  25. just_bash/fs/overlay_fs.py +22 -1
  26. just_bash/interpreter/__init__.py +1 -1
  27. just_bash/interpreter/builtins/__init__.py +2 -0
  28. just_bash/interpreter/builtins/control.py +4 -8
  29. just_bash/interpreter/builtins/declare.py +321 -24
  30. just_bash/interpreter/builtins/getopts.py +163 -0
  31. just_bash/interpreter/builtins/let.py +2 -2
  32. just_bash/interpreter/builtins/local.py +71 -5
  33. just_bash/interpreter/builtins/misc.py +22 -6
  34. just_bash/interpreter/builtins/readonly.py +38 -10
  35. just_bash/interpreter/builtins/set.py +58 -8
  36. just_bash/interpreter/builtins/test.py +136 -19
  37. just_bash/interpreter/builtins/unset.py +62 -10
  38. just_bash/interpreter/conditionals.py +29 -4
  39. just_bash/interpreter/control_flow.py +61 -17
  40. just_bash/interpreter/expansion.py +1647 -104
  41. just_bash/interpreter/interpreter.py +436 -69
  42. just_bash/interpreter/types.py +263 -2
  43. just_bash/parser/__init__.py +2 -0
  44. just_bash/parser/lexer.py +295 -26
  45. just_bash/parser/parser.py +523 -64
  46. just_bash/types.py +11 -0
  47. {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/METADATA +40 -1
  48. {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/RECORD +49 -40
  49. {just_bash-0.1.5.dist-info → just_bash-0.1.10.dist-info}/WHEEL +0 -0
@@ -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)
@@ -152,7 +152,10 @@ class GrepCommand:
152
152
  elif arg == "-e":
153
153
  patterns.append(val)
154
154
  else:
155
- for c in arg[1:]:
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:
@@ -203,8 +232,18 @@ class GrepCommand:
203
232
  if pattern:
204
233
  # If pattern was set, it's actually a file
205
234
  files.insert(0, pattern)
206
- pattern = "|".join(f"({p})" for p in patterns)
207
- elif pattern is None:
235
+ # Convert each pattern from BRE before combining (if not ERE mode)
236
+ if not extended_regexp and not fixed_strings:
237
+ converted = [self._bre_to_python_regex(p) for p in patterns]
238
+ pattern = "|".join(f"({p})" for p in converted)
239
+ else:
240
+ pattern = "|".join(f"({p})" for p in patterns)
241
+ # Mark as already converted
242
+ patterns_already_converted = True
243
+ else:
244
+ patterns_already_converted = False
245
+
246
+ if pattern is None and not patterns:
208
247
  return ExecResult(
209
248
  stdout="",
210
249
  stderr="grep: pattern not specified\n",
@@ -220,6 +259,10 @@ class GrepCommand:
220
259
  if fixed_strings:
221
260
  # Escape all regex metacharacters
222
261
  pattern = re.escape(pattern)
262
+ elif not extended_regexp and not patterns_already_converted:
263
+ # Convert BRE (Basic Regular Expression) to Python regex
264
+ pattern = self._bre_to_python_regex(pattern)
265
+
223
266
  if word_regexp:
224
267
  pattern = r'\b' + pattern + r'\b'
225
268
  if line_regexp:
@@ -382,6 +425,101 @@ class GrepCommand:
382
425
  exit_code = 0 if found_match else 1
383
426
  return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
384
427
 
428
+ def _bre_to_python_regex(self, pattern: str) -> str:
429
+ """Convert BRE (Basic Regular Expression) to Python regex.
430
+
431
+ In BRE:
432
+ - \\| is alternation, | is literal
433
+ - \\+ is one-or-more, + is literal
434
+ - \\? is zero-or-one, ? is literal
435
+ - \\( \\) is grouping, ( ) are literal
436
+ - \\{ \\} is repetition, { } are literal
437
+ - \\< \\> is word boundary
438
+
439
+ In Python regex (like ERE):
440
+ - | is alternation, \\| is literal
441
+ - + is one-or-more, \\+ is literal
442
+ - etc.
443
+ """
444
+ result = []
445
+ i = 0
446
+ while i < len(pattern):
447
+ if pattern[i] == '\\' and i + 1 < len(pattern):
448
+ next_char = pattern[i + 1]
449
+ if next_char == '|':
450
+ # BRE \| -> Python |
451
+ result.append('|')
452
+ i += 2
453
+ elif next_char == '+':
454
+ # BRE \+ -> Python +
455
+ result.append('+')
456
+ i += 2
457
+ elif next_char == '?':
458
+ # BRE \? -> Python ?
459
+ result.append('?')
460
+ i += 2
461
+ elif next_char == '(':
462
+ # BRE \( -> Python (
463
+ result.append('(')
464
+ i += 2
465
+ elif next_char == ')':
466
+ # BRE \) -> Python )
467
+ result.append(')')
468
+ i += 2
469
+ elif next_char == '{':
470
+ # BRE \{ -> Python {
471
+ result.append('{')
472
+ i += 2
473
+ elif next_char == '}':
474
+ # BRE \} -> Python }
475
+ result.append('}')
476
+ i += 2
477
+ elif next_char == '<':
478
+ # BRE \< (word start) -> Python \b
479
+ result.append(r'\b')
480
+ i += 2
481
+ elif next_char == '>':
482
+ # BRE \> (word end) -> Python \b
483
+ result.append(r'\b')
484
+ i += 2
485
+ else:
486
+ # Other escapes pass through as-is
487
+ result.append(pattern[i:i + 2])
488
+ i += 2
489
+ elif pattern[i] == '|':
490
+ # BRE literal | -> Python \|
491
+ result.append(r'\|')
492
+ i += 1
493
+ elif pattern[i] == '+':
494
+ # BRE literal + -> Python \+
495
+ result.append(r'\+')
496
+ i += 1
497
+ elif pattern[i] == '?':
498
+ # BRE literal ? -> Python \?
499
+ result.append(r'\?')
500
+ i += 1
501
+ elif pattern[i] == '(':
502
+ # BRE literal ( -> Python \(
503
+ result.append(r'\(')
504
+ i += 1
505
+ elif pattern[i] == ')':
506
+ # BRE literal ) -> Python \)
507
+ result.append(r'\)')
508
+ i += 1
509
+ elif pattern[i] == '{':
510
+ # BRE literal { -> Python \{
511
+ result.append(r'\{')
512
+ i += 1
513
+ elif pattern[i] == '}':
514
+ # BRE literal } -> Python \}
515
+ result.append(r'\}')
516
+ i += 1
517
+ else:
518
+ result.append(pattern[i])
519
+ i += 1
520
+
521
+ return ''.join(result)
522
+
385
523
  async def _get_files_recursive(self, ctx: CommandContext, path: str) -> list[str]:
386
524
  """Get all files in a directory recursively."""
387
525
  files = []
@@ -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 == "-Ad":
38
- address_format = "d"
39
- elif arg == "-Ao":
40
- address_format = "o"
41
- elif arg == "-Ax":
42
- address_format = "x"
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("utf-8", errors="replace")
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("utf-8", errors="replace")
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" {chr(byte)}")
240
+ chars.append(f" {chr(byte)}")
127
241
  else:
128
- chars.append(f"{byte:03o}")
129
- parts.append(" ".join(chars))
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(" ".join(hex_vals))
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(" ".join(dec_vals))
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(" ".join(oct_vals))
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