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
|
@@ -14,76 +14,73 @@ class PrintfCommand:
|
|
|
14
14
|
if not args:
|
|
15
15
|
return ExecResult(
|
|
16
16
|
stdout="",
|
|
17
|
-
stderr="printf: usage: printf format [arguments]\n",
|
|
17
|
+
stderr="printf: usage: printf [-v var] format [arguments]\n",
|
|
18
18
|
exit_code=2,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
# Parse -v option and -- end-of-options
|
|
22
|
+
var_name = None
|
|
23
|
+
format_start = 0
|
|
24
|
+
if len(args) >= 2 and args[0] == "-v":
|
|
25
|
+
var_name = args[1]
|
|
26
|
+
format_start = 2
|
|
27
|
+
if format_start < len(args) and args[format_start] == "--":
|
|
28
|
+
format_start += 1
|
|
29
|
+
|
|
30
|
+
if len(args) <= format_start:
|
|
31
|
+
return ExecResult(
|
|
32
|
+
stdout="",
|
|
33
|
+
stderr="printf: usage: printf [-v var] format [arguments]\n",
|
|
34
|
+
exit_code=2,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
format_str = args[format_start]
|
|
38
|
+
arguments = args[format_start + 1:]
|
|
23
39
|
|
|
24
40
|
try:
|
|
25
41
|
output = self._format(format_str, arguments)
|
|
42
|
+
|
|
43
|
+
if var_name is not None:
|
|
44
|
+
# Assign to variable instead of printing
|
|
45
|
+
ctx.env[var_name] = output
|
|
46
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
47
|
+
|
|
26
48
|
return ExecResult(stdout=output, stderr="", exit_code=0)
|
|
27
49
|
except ValueError as e:
|
|
28
50
|
return ExecResult(stdout="", stderr=f"printf: {e}\n", exit_code=1)
|
|
29
51
|
|
|
30
52
|
def _format(self, format_str: str, arguments: list[str]) -> str:
|
|
31
|
-
"""Format the string with arguments.
|
|
53
|
+
"""Format the string with arguments.
|
|
54
|
+
|
|
55
|
+
Supports format reuse: if there are more arguments than format specifiers,
|
|
56
|
+
the format string is reused for the remaining arguments.
|
|
57
|
+
"""
|
|
32
58
|
result = []
|
|
33
59
|
arg_index = 0
|
|
60
|
+
|
|
61
|
+
# Continue formatting until all arguments are consumed
|
|
62
|
+
while True:
|
|
63
|
+
start_arg_index = arg_index
|
|
64
|
+
formatted, arg_index = self._format_once(format_str, arguments, arg_index)
|
|
65
|
+
result.append(formatted)
|
|
66
|
+
|
|
67
|
+
# If no arguments were consumed or all arguments are consumed, stop
|
|
68
|
+
if arg_index == start_arg_index or arg_index >= len(arguments):
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
return "".join(result)
|
|
72
|
+
|
|
73
|
+
def _format_once(self, format_str: str, arguments: list[str], arg_index: int) -> tuple[str, int]:
|
|
74
|
+
"""Format the string once, returning formatted string and new arg index."""
|
|
75
|
+
result = []
|
|
34
76
|
i = 0
|
|
35
77
|
|
|
36
78
|
while i < len(format_str):
|
|
37
79
|
if format_str[i] == "\\" and i + 1 < len(format_str):
|
|
38
80
|
# Handle escape sequences
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
elif escape_char == "t":
|
|
43
|
-
result.append("\t")
|
|
44
|
-
elif escape_char == "r":
|
|
45
|
-
result.append("\r")
|
|
46
|
-
elif escape_char == "\\":
|
|
47
|
-
result.append("\\")
|
|
48
|
-
elif escape_char == "a":
|
|
49
|
-
result.append("\a")
|
|
50
|
-
elif escape_char == "b":
|
|
51
|
-
result.append("\b")
|
|
52
|
-
elif escape_char == "f":
|
|
53
|
-
result.append("\f")
|
|
54
|
-
elif escape_char == "v":
|
|
55
|
-
result.append("\v")
|
|
56
|
-
elif escape_char == "e" or escape_char == "E":
|
|
57
|
-
result.append("\x1b")
|
|
58
|
-
elif escape_char == "0":
|
|
59
|
-
# Octal escape
|
|
60
|
-
octal = ""
|
|
61
|
-
j = i + 2
|
|
62
|
-
while j < len(format_str) and len(octal) < 3 and format_str[j] in "01234567":
|
|
63
|
-
octal += format_str[j]
|
|
64
|
-
j += 1
|
|
65
|
-
if octal:
|
|
66
|
-
result.append(chr(int(octal, 8)))
|
|
67
|
-
i = j
|
|
68
|
-
continue
|
|
69
|
-
else:
|
|
70
|
-
result.append("\0")
|
|
71
|
-
elif escape_char == "x":
|
|
72
|
-
# Hex escape
|
|
73
|
-
hex_digits = ""
|
|
74
|
-
j = i + 2
|
|
75
|
-
while j < len(format_str) and len(hex_digits) < 2 and format_str[j] in "0123456789abcdefABCDEF":
|
|
76
|
-
hex_digits += format_str[j]
|
|
77
|
-
j += 1
|
|
78
|
-
if hex_digits:
|
|
79
|
-
result.append(chr(int(hex_digits, 16)))
|
|
80
|
-
i = j
|
|
81
|
-
continue
|
|
82
|
-
else:
|
|
83
|
-
result.append(escape_char)
|
|
84
|
-
else:
|
|
85
|
-
result.append(escape_char)
|
|
86
|
-
i += 2
|
|
81
|
+
escape_result, consumed = self._process_escape(format_str, i)
|
|
82
|
+
result.append(escape_result)
|
|
83
|
+
i += consumed
|
|
87
84
|
elif format_str[i] == "%" and i + 1 < len(format_str):
|
|
88
85
|
# Handle format specifiers
|
|
89
86
|
if format_str[i + 1] == "%":
|
|
@@ -91,12 +88,47 @@ class PrintfCommand:
|
|
|
91
88
|
i += 2
|
|
92
89
|
continue
|
|
93
90
|
|
|
94
|
-
# Parse format specifier
|
|
95
|
-
|
|
91
|
+
# Parse format specifier with full support
|
|
92
|
+
# Pattern: %[flags][width][.precision]specifier
|
|
93
|
+
# Flags: -, +, space, #, 0
|
|
94
|
+
# Width: number or *
|
|
95
|
+
# Precision: .number or .*
|
|
96
|
+
spec_pattern = r"([-+# 0]*)(\*|\d+)?(?:\.(\*|\d*))?([diouxXeEfFgGsbcq])"
|
|
97
|
+
spec_match = re.match(spec_pattern, format_str[i + 1:])
|
|
98
|
+
|
|
96
99
|
if spec_match:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
flags = spec_match.group(1) or ""
|
|
101
|
+
width_spec = spec_match.group(2)
|
|
102
|
+
precision_spec = spec_match.group(3)
|
|
103
|
+
spec_type = spec_match.group(4)
|
|
104
|
+
|
|
105
|
+
# Handle * for width
|
|
106
|
+
width = None
|
|
107
|
+
if width_spec == "*":
|
|
108
|
+
if arg_index < len(arguments):
|
|
109
|
+
try:
|
|
110
|
+
width = int(arguments[arg_index])
|
|
111
|
+
except ValueError:
|
|
112
|
+
width = 0
|
|
113
|
+
arg_index += 1
|
|
114
|
+
else:
|
|
115
|
+
width = 0
|
|
116
|
+
elif width_spec:
|
|
117
|
+
width = int(width_spec)
|
|
118
|
+
|
|
119
|
+
# Handle * for precision
|
|
120
|
+
precision = None
|
|
121
|
+
if precision_spec == "*":
|
|
122
|
+
if arg_index < len(arguments):
|
|
123
|
+
try:
|
|
124
|
+
precision = int(arguments[arg_index])
|
|
125
|
+
except ValueError:
|
|
126
|
+
precision = 0
|
|
127
|
+
arg_index += 1
|
|
128
|
+
else:
|
|
129
|
+
precision = 0
|
|
130
|
+
elif precision_spec is not None:
|
|
131
|
+
precision = int(precision_spec) if precision_spec else 0
|
|
100
132
|
|
|
101
133
|
# Get argument
|
|
102
134
|
if arg_index < len(arguments):
|
|
@@ -106,24 +138,10 @@ class PrintfCommand:
|
|
|
106
138
|
arg = ""
|
|
107
139
|
|
|
108
140
|
# Format based on type
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
elif spec_type in "eEfFgG":
|
|
114
|
-
val = float(arg) if arg else 0.0
|
|
115
|
-
result.append(full_spec % val)
|
|
116
|
-
elif spec_type == "s":
|
|
117
|
-
result.append(full_spec % arg)
|
|
118
|
-
elif spec_type == "c":
|
|
119
|
-
result.append(arg[0] if arg else "")
|
|
120
|
-
elif spec_type == "b":
|
|
121
|
-
# %b is like %s but interprets escapes
|
|
122
|
-
result.append(self._process_escapes(arg))
|
|
123
|
-
except (ValueError, TypeError):
|
|
124
|
-
result.append(full_spec % 0 if spec_type in "diouxXeEfFgG" else "")
|
|
125
|
-
|
|
126
|
-
i += 1 + len(spec)
|
|
141
|
+
formatted = self._format_specifier(spec_type, arg, flags, width, precision)
|
|
142
|
+
result.append(formatted)
|
|
143
|
+
|
|
144
|
+
i += 1 + len(spec_match.group(0))
|
|
127
145
|
else:
|
|
128
146
|
result.append(format_str[i])
|
|
129
147
|
i += 1
|
|
@@ -131,26 +149,210 @@ class PrintfCommand:
|
|
|
131
149
|
result.append(format_str[i])
|
|
132
150
|
i += 1
|
|
133
151
|
|
|
152
|
+
return "".join(result), arg_index
|
|
153
|
+
|
|
154
|
+
def _format_specifier(self, spec_type: str, arg: str, flags: str, width: int | None, precision: int | None) -> str:
|
|
155
|
+
"""Format a single specifier."""
|
|
156
|
+
try:
|
|
157
|
+
if spec_type == "q":
|
|
158
|
+
# Shell quoting
|
|
159
|
+
return self._shell_quote(arg)
|
|
160
|
+
elif spec_type in "diouxX":
|
|
161
|
+
val = self._parse_numeric_arg(arg)
|
|
162
|
+
fmt = self._build_format_string(spec_type, flags, width, precision)
|
|
163
|
+
return fmt % val
|
|
164
|
+
elif spec_type in "eEfFgG":
|
|
165
|
+
val = float(arg) if arg else 0.0
|
|
166
|
+
fmt = self._build_format_string(spec_type, flags, width, precision)
|
|
167
|
+
return fmt % val
|
|
168
|
+
elif spec_type == "s":
|
|
169
|
+
fmt = self._build_format_string(spec_type, flags, width, precision)
|
|
170
|
+
return fmt % arg
|
|
171
|
+
elif spec_type == "c":
|
|
172
|
+
return arg[0] if arg else ""
|
|
173
|
+
elif spec_type == "b":
|
|
174
|
+
# %b is like %s but interprets escapes
|
|
175
|
+
processed = self._process_escapes(arg)
|
|
176
|
+
if width is not None:
|
|
177
|
+
if "-" in flags:
|
|
178
|
+
return processed.ljust(width)
|
|
179
|
+
else:
|
|
180
|
+
return processed.rjust(width)
|
|
181
|
+
return processed
|
|
182
|
+
else:
|
|
183
|
+
return ""
|
|
184
|
+
except (ValueError, TypeError):
|
|
185
|
+
if spec_type in "diouxXeEfFgG":
|
|
186
|
+
return "0"
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
def _parse_numeric_arg(self, arg: str) -> int:
|
|
190
|
+
"""Parse a numeric argument, handling hex, octal, and character notation."""
|
|
191
|
+
if not arg:
|
|
192
|
+
return 0
|
|
193
|
+
# Character notation: 'c or "c
|
|
194
|
+
if len(arg) >= 2 and arg[0] in ("'", '"'):
|
|
195
|
+
return ord(arg[1])
|
|
196
|
+
# Handle sign
|
|
197
|
+
s = arg.strip()
|
|
198
|
+
sign = 1
|
|
199
|
+
if s.startswith("-"):
|
|
200
|
+
sign = -1
|
|
201
|
+
s = s[1:]
|
|
202
|
+
elif s.startswith("+"):
|
|
203
|
+
s = s[1:]
|
|
204
|
+
try:
|
|
205
|
+
# Hex: 0x or 0X
|
|
206
|
+
if s.startswith("0x") or s.startswith("0X"):
|
|
207
|
+
return sign * int(s, 16)
|
|
208
|
+
# Octal: leading 0 followed by digits
|
|
209
|
+
if len(s) > 1 and s[0] == "0" and all(c in "01234567" for c in s[1:]):
|
|
210
|
+
return sign * int(s, 8)
|
|
211
|
+
return sign * int(s)
|
|
212
|
+
except ValueError:
|
|
213
|
+
return 0
|
|
214
|
+
|
|
215
|
+
def _build_format_string(self, spec_type: str, flags: str, width: int | None, precision: int | None) -> str:
|
|
216
|
+
"""Build a Python format string from components."""
|
|
217
|
+
fmt = "%"
|
|
218
|
+
fmt += flags
|
|
219
|
+
if width is not None:
|
|
220
|
+
fmt += str(abs(width))
|
|
221
|
+
if width < 0:
|
|
222
|
+
# Negative width means left-justify
|
|
223
|
+
fmt = "%-" + fmt[1:]
|
|
224
|
+
if precision is not None:
|
|
225
|
+
fmt += f".{precision}"
|
|
226
|
+
fmt += spec_type
|
|
227
|
+
return fmt
|
|
228
|
+
|
|
229
|
+
def _shell_quote(self, s: str) -> str:
|
|
230
|
+
"""Quote a string for shell use."""
|
|
231
|
+
if not s:
|
|
232
|
+
return "''"
|
|
233
|
+
|
|
234
|
+
# Check if quoting is needed
|
|
235
|
+
safe_chars = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_@%+=:,./-")
|
|
236
|
+
if all(c in safe_chars for c in s):
|
|
237
|
+
return s
|
|
238
|
+
|
|
239
|
+
# Use $'...' format for strings with special chars
|
|
240
|
+
result = ["$'"]
|
|
241
|
+
for c in s:
|
|
242
|
+
if c == "'":
|
|
243
|
+
result.append("\\'")
|
|
244
|
+
elif c == "\\":
|
|
245
|
+
result.append("\\\\")
|
|
246
|
+
elif c == "\n":
|
|
247
|
+
result.append("\\n")
|
|
248
|
+
elif c == "\t":
|
|
249
|
+
result.append("\\t")
|
|
250
|
+
elif c == "\r":
|
|
251
|
+
result.append("\\r")
|
|
252
|
+
elif ord(c) < 32 or ord(c) > 126:
|
|
253
|
+
result.append(f"\\x{ord(c):02x}")
|
|
254
|
+
else:
|
|
255
|
+
result.append(c)
|
|
256
|
+
result.append("'")
|
|
134
257
|
return "".join(result)
|
|
135
258
|
|
|
259
|
+
def _process_escape(self, s: str, i: int) -> tuple[str, int]:
|
|
260
|
+
"""Process an escape sequence starting at position i.
|
|
261
|
+
|
|
262
|
+
Returns (result_string, characters_consumed).
|
|
263
|
+
"""
|
|
264
|
+
if i + 1 >= len(s):
|
|
265
|
+
return ("\\", 1)
|
|
266
|
+
|
|
267
|
+
escape_char = s[i + 1]
|
|
268
|
+
escape_map = {
|
|
269
|
+
"n": "\n",
|
|
270
|
+
"t": "\t",
|
|
271
|
+
"r": "\r",
|
|
272
|
+
"\\": "\\",
|
|
273
|
+
"a": "\a",
|
|
274
|
+
"b": "\b",
|
|
275
|
+
"f": "\f",
|
|
276
|
+
"v": "\v",
|
|
277
|
+
"e": "\x1b",
|
|
278
|
+
"E": "\x1b",
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if escape_char in escape_map:
|
|
282
|
+
return (escape_map[escape_char], 2)
|
|
283
|
+
elif escape_char in "01234567":
|
|
284
|
+
# Octal escape: \NNN - first digit plus up to 2 more (3 total)
|
|
285
|
+
octal = escape_char
|
|
286
|
+
j = i + 2
|
|
287
|
+
while j < len(s) and len(octal) < 3 and s[j] in "01234567":
|
|
288
|
+
octal += s[j]
|
|
289
|
+
j += 1
|
|
290
|
+
return (chr(int(octal, 8) & 0xFF), j - i)
|
|
291
|
+
elif escape_char == "x":
|
|
292
|
+
# Hex escape - collect consecutive \xHH sequences and try UTF-8 decoding
|
|
293
|
+
hex_bytes = []
|
|
294
|
+
j = i
|
|
295
|
+
while j < len(s) and s[j:j+2] == "\\x":
|
|
296
|
+
hex_digits = ""
|
|
297
|
+
k = j + 2
|
|
298
|
+
while k < len(s) and len(hex_digits) < 2 and s[k] in "0123456789abcdefABCDEF":
|
|
299
|
+
hex_digits += s[k]
|
|
300
|
+
k += 1
|
|
301
|
+
if hex_digits:
|
|
302
|
+
hex_bytes.append(int(hex_digits, 16))
|
|
303
|
+
j = k
|
|
304
|
+
else:
|
|
305
|
+
break
|
|
306
|
+
|
|
307
|
+
if hex_bytes:
|
|
308
|
+
# Try UTF-8 decoding first
|
|
309
|
+
byte_data = bytes(hex_bytes)
|
|
310
|
+
try:
|
|
311
|
+
decoded = byte_data.decode("utf-8")
|
|
312
|
+
return (decoded, j - i)
|
|
313
|
+
except UnicodeDecodeError:
|
|
314
|
+
# Fall back to Latin-1 (1:1 byte to codepoint)
|
|
315
|
+
return (byte_data.decode("latin-1"), j - i)
|
|
316
|
+
else:
|
|
317
|
+
return (escape_char, 2)
|
|
318
|
+
elif escape_char == "u":
|
|
319
|
+
# Unicode escape \uHHHH
|
|
320
|
+
hex_digits = ""
|
|
321
|
+
j = i + 2
|
|
322
|
+
while j < len(s) and len(hex_digits) < 4 and s[j] in "0123456789abcdefABCDEF":
|
|
323
|
+
hex_digits += s[j]
|
|
324
|
+
j += 1
|
|
325
|
+
if hex_digits:
|
|
326
|
+
try:
|
|
327
|
+
return (chr(int(hex_digits, 16)), j - i)
|
|
328
|
+
except ValueError:
|
|
329
|
+
return (escape_char, 2)
|
|
330
|
+
return (escape_char, 2)
|
|
331
|
+
elif escape_char == "U":
|
|
332
|
+
# Unicode escape \UHHHHHHHH
|
|
333
|
+
hex_digits = ""
|
|
334
|
+
j = i + 2
|
|
335
|
+
while j < len(s) and len(hex_digits) < 8 and s[j] in "0123456789abcdefABCDEF":
|
|
336
|
+
hex_digits += s[j]
|
|
337
|
+
j += 1
|
|
338
|
+
if hex_digits:
|
|
339
|
+
try:
|
|
340
|
+
return (chr(int(hex_digits, 16)), j - i)
|
|
341
|
+
except ValueError:
|
|
342
|
+
return (escape_char, 2)
|
|
343
|
+
return (escape_char, 2)
|
|
344
|
+
else:
|
|
345
|
+
return (escape_char, 2)
|
|
346
|
+
|
|
136
347
|
def _process_escapes(self, s: str) -> str:
|
|
137
348
|
"""Process escape sequences in a string."""
|
|
138
349
|
result = []
|
|
139
350
|
i = 0
|
|
140
351
|
while i < len(s):
|
|
141
352
|
if s[i] == "\\" and i + 1 < len(s):
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
elif c == "t":
|
|
146
|
-
result.append("\t")
|
|
147
|
-
elif c == "r":
|
|
148
|
-
result.append("\r")
|
|
149
|
-
elif c == "\\":
|
|
150
|
-
result.append("\\")
|
|
151
|
-
else:
|
|
152
|
-
result.append(c)
|
|
153
|
-
i += 2
|
|
353
|
+
escaped, consumed = self._process_escape(s, i)
|
|
354
|
+
result.append(escaped)
|
|
355
|
+
i += consumed
|
|
154
356
|
else:
|
|
155
357
|
result.append(s[i])
|
|
156
358
|
i += 1
|
just_bash/commands/pwd/pwd.py
CHANGED
|
@@ -19,5 +19,35 @@ class PwdCommand:
|
|
|
19
19
|
|
|
20
20
|
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
21
|
"""Execute the pwd command."""
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
physical = False
|
|
23
|
+
|
|
24
|
+
# Parse arguments
|
|
25
|
+
for arg in args:
|
|
26
|
+
if arg == "-P":
|
|
27
|
+
physical = True
|
|
28
|
+
elif arg == "-L":
|
|
29
|
+
physical = False
|
|
30
|
+
elif arg.startswith("-"):
|
|
31
|
+
# Check for combined flags like -LP or -PL
|
|
32
|
+
for c in arg[1:]:
|
|
33
|
+
if c == "P":
|
|
34
|
+
physical = True
|
|
35
|
+
elif c == "L":
|
|
36
|
+
physical = False
|
|
37
|
+
else:
|
|
38
|
+
return ExecResult(
|
|
39
|
+
stdout="",
|
|
40
|
+
stderr=f"pwd: invalid option -- '{c}'\n",
|
|
41
|
+
exit_code=1,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if physical:
|
|
45
|
+
# Resolve symlinks in cwd
|
|
46
|
+
try:
|
|
47
|
+
resolved = await ctx.fs.realpath(ctx.cwd)
|
|
48
|
+
return ExecResult(stdout=f"{resolved}\n", stderr="", exit_code=0)
|
|
49
|
+
except (FileNotFoundError, OSError):
|
|
50
|
+
return ExecResult(stdout=f"{ctx.cwd}\n", stderr="", exit_code=0)
|
|
51
|
+
else:
|
|
52
|
+
# Return logical path (cwd as-is)
|
|
53
|
+
return ExecResult(stdout=f"{ctx.cwd}\n", stderr="", exit_code=0)
|