just-bash 0.1.8__py3-none-any.whl → 0.1.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- just_bash/ast/factory.py +3 -1
- just_bash/bash.py +28 -6
- just_bash/commands/awk/awk.py +112 -7
- just_bash/commands/cat/cat.py +5 -1
- just_bash/commands/echo/echo.py +33 -1
- just_bash/commands/grep/grep.py +30 -1
- just_bash/commands/od/od.py +144 -30
- just_bash/commands/printf/printf.py +289 -87
- just_bash/commands/pwd/pwd.py +32 -2
- just_bash/commands/read/read.py +243 -64
- just_bash/commands/readlink/readlink.py +3 -9
- just_bash/commands/registry.py +24 -0
- just_bash/commands/rmdir/__init__.py +5 -0
- just_bash/commands/rmdir/rmdir.py +160 -0
- just_bash/commands/sed/sed.py +142 -31
- just_bash/commands/stat/stat.py +9 -0
- just_bash/commands/time/__init__.py +5 -0
- just_bash/commands/time/time.py +74 -0
- just_bash/commands/touch/touch.py +118 -8
- just_bash/commands/whoami/__init__.py +5 -0
- just_bash/commands/whoami/whoami.py +18 -0
- just_bash/fs/in_memory_fs.py +22 -0
- just_bash/fs/overlay_fs.py +14 -0
- just_bash/interpreter/__init__.py +1 -1
- just_bash/interpreter/builtins/__init__.py +2 -0
- just_bash/interpreter/builtins/control.py +4 -8
- just_bash/interpreter/builtins/declare.py +321 -24
- just_bash/interpreter/builtins/getopts.py +163 -0
- just_bash/interpreter/builtins/let.py +2 -2
- just_bash/interpreter/builtins/local.py +71 -5
- just_bash/interpreter/builtins/misc.py +22 -6
- just_bash/interpreter/builtins/readonly.py +38 -10
- just_bash/interpreter/builtins/set.py +58 -8
- just_bash/interpreter/builtins/test.py +136 -19
- just_bash/interpreter/builtins/unset.py +62 -10
- just_bash/interpreter/conditionals.py +29 -4
- just_bash/interpreter/control_flow.py +61 -17
- just_bash/interpreter/expansion.py +1647 -104
- just_bash/interpreter/interpreter.py +424 -70
- just_bash/interpreter/types.py +263 -2
- just_bash/parser/__init__.py +2 -0
- just_bash/parser/lexer.py +295 -26
- just_bash/parser/parser.py +523 -64
- just_bash/types.py +11 -0
- {just_bash-0.1.8.dist-info → just_bash-0.1.10.dist-info}/METADATA +40 -1
- {just_bash-0.1.8.dist-info → just_bash-0.1.10.dist-info}/RECORD +47 -40
- {just_bash-0.1.8.dist-info → just_bash-0.1.10.dist-info}/WHEEL +0 -0
just_bash/commands/read/read.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"""Read command implementation.
|
|
2
2
|
|
|
3
|
-
Usage: read [-r] [-d delim] [-n nchars] [-p prompt] [-t timeout] [name ...]
|
|
3
|
+
Usage: read [-r] [-d delim] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [name ...]
|
|
4
4
|
|
|
5
5
|
Read a line from stdin and split it into fields.
|
|
6
6
|
|
|
7
7
|
Options:
|
|
8
8
|
-r Do not treat backslash as escape character
|
|
9
9
|
-d delim Use delim as line delimiter instead of newline
|
|
10
|
-
-n nchars Read
|
|
10
|
+
-n nchars Read at most nchars characters
|
|
11
|
+
-N nchars Read exactly nchars characters (no IFS splitting, ignores delimiters)
|
|
11
12
|
-p prompt Output the string prompt before reading
|
|
12
13
|
-t timeout Time out after timeout seconds
|
|
13
14
|
|
|
@@ -28,7 +29,9 @@ class ReadCommand:
|
|
|
28
29
|
raw_mode = False
|
|
29
30
|
delimiter = "\n"
|
|
30
31
|
nchars = None
|
|
32
|
+
no_split = False # -N mode: no IFS splitting
|
|
31
33
|
array_name = None # -a option
|
|
34
|
+
fd_num = None # -u option
|
|
32
35
|
var_names = []
|
|
33
36
|
|
|
34
37
|
i = 0
|
|
@@ -41,7 +44,9 @@ class ReadCommand:
|
|
|
41
44
|
array_name = args[i]
|
|
42
45
|
elif arg == "-d" and i + 1 < len(args):
|
|
43
46
|
i += 1
|
|
44
|
-
delimiter = args[i]
|
|
47
|
+
delimiter = args[i] if args[i] else "\0"
|
|
48
|
+
elif arg.startswith("-d") and len(arg) > 2:
|
|
49
|
+
delimiter = arg[2:]
|
|
45
50
|
elif arg == "-n" and i + 1 < len(args):
|
|
46
51
|
i += 1
|
|
47
52
|
try:
|
|
@@ -52,33 +57,99 @@ class ReadCommand:
|
|
|
52
57
|
stderr=f"bash: read: {args[i]}: invalid number\n",
|
|
53
58
|
exit_code=1,
|
|
54
59
|
)
|
|
60
|
+
elif arg.startswith("-n") and len(arg) > 2:
|
|
61
|
+
try:
|
|
62
|
+
nchars = int(arg[2:])
|
|
63
|
+
except ValueError:
|
|
64
|
+
return ExecResult(
|
|
65
|
+
stdout="",
|
|
66
|
+
stderr=f"bash: read: {arg[2:]}: invalid number\n",
|
|
67
|
+
exit_code=1,
|
|
68
|
+
)
|
|
69
|
+
elif arg == "-N" and i + 1 < len(args):
|
|
70
|
+
i += 1
|
|
71
|
+
try:
|
|
72
|
+
nchars = int(args[i])
|
|
73
|
+
except ValueError:
|
|
74
|
+
return ExecResult(
|
|
75
|
+
stdout="",
|
|
76
|
+
stderr=f"bash: read: {args[i]}: invalid number\n",
|
|
77
|
+
exit_code=1,
|
|
78
|
+
)
|
|
79
|
+
no_split = True
|
|
80
|
+
delimiter = "" # -N ignores delimiters
|
|
81
|
+
elif arg.startswith("-N") and len(arg) > 2:
|
|
82
|
+
try:
|
|
83
|
+
nchars = int(arg[2:])
|
|
84
|
+
except ValueError:
|
|
85
|
+
return ExecResult(
|
|
86
|
+
stdout="",
|
|
87
|
+
stderr=f"bash: read: {arg[2:]}: invalid number\n",
|
|
88
|
+
exit_code=1,
|
|
89
|
+
)
|
|
90
|
+
no_split = True
|
|
91
|
+
delimiter = "" # -N ignores delimiters
|
|
92
|
+
elif arg == "-u" and i + 1 < len(args):
|
|
93
|
+
i += 1
|
|
94
|
+
try:
|
|
95
|
+
fd_num = int(args[i])
|
|
96
|
+
except ValueError:
|
|
97
|
+
return ExecResult(
|
|
98
|
+
stdout="",
|
|
99
|
+
stderr=f"bash: read: {args[i]}: invalid file descriptor\n",
|
|
100
|
+
exit_code=1,
|
|
101
|
+
)
|
|
102
|
+
elif arg.startswith("-u") and len(arg) > 2:
|
|
103
|
+
try:
|
|
104
|
+
fd_num = int(arg[2:])
|
|
105
|
+
except ValueError:
|
|
106
|
+
return ExecResult(
|
|
107
|
+
stdout="",
|
|
108
|
+
stderr=f"bash: read: {arg[2:]}: invalid file descriptor\n",
|
|
109
|
+
exit_code=1,
|
|
110
|
+
)
|
|
55
111
|
elif arg == "-p" and i + 1 < len(args):
|
|
56
|
-
# Prompt option - we ignore it since we can't prompt
|
|
57
112
|
i += 1
|
|
58
113
|
elif arg == "-t" and i + 1 < len(args):
|
|
59
|
-
# Timeout option - we ignore it
|
|
60
114
|
i += 1
|
|
61
115
|
elif arg.startswith("-"):
|
|
62
|
-
# Unknown option - ignore for compatibility
|
|
63
116
|
pass
|
|
64
117
|
else:
|
|
65
118
|
var_names.append(arg)
|
|
66
119
|
i += 1
|
|
67
120
|
|
|
68
|
-
#
|
|
121
|
+
# Track whether we're using default REPLY
|
|
122
|
+
use_reply = not var_names and not array_name
|
|
69
123
|
if not var_names:
|
|
70
124
|
var_names = ["REPLY"]
|
|
71
125
|
|
|
72
|
-
# Get input from stdin
|
|
73
|
-
|
|
126
|
+
# Get input from stdin or custom FD
|
|
127
|
+
if fd_num is not None and fd_num >= 3:
|
|
128
|
+
fd_contents = getattr(ctx, 'fd_contents', {})
|
|
129
|
+
stdin = fd_contents.get(fd_num, "")
|
|
130
|
+
else:
|
|
131
|
+
stdin = ctx.stdin or ""
|
|
132
|
+
|
|
133
|
+
# Determine if input was properly terminated by delimiter
|
|
134
|
+
# (affects exit code: 1 if no terminating delimiter found)
|
|
135
|
+
eof_reached = False
|
|
136
|
+
if not stdin:
|
|
137
|
+
eof_reached = True
|
|
138
|
+
elif no_split:
|
|
139
|
+
# -N mode: EOF if fewer chars available than requested
|
|
140
|
+
if nchars is not None and len(stdin) < nchars:
|
|
141
|
+
eof_reached = True
|
|
142
|
+
elif delimiter not in stdin:
|
|
143
|
+
eof_reached = True
|
|
74
144
|
|
|
75
145
|
# Find the line to read
|
|
76
|
-
if delimiter == "
|
|
77
|
-
#
|
|
146
|
+
if delimiter == "":
|
|
147
|
+
# -N mode: read raw bytes, no delimiter processing
|
|
148
|
+
line = stdin
|
|
149
|
+
elif delimiter == "\n":
|
|
78
150
|
lines = stdin.split("\n")
|
|
79
151
|
line = lines[0] if lines else ""
|
|
80
152
|
else:
|
|
81
|
-
# Custom delimiter
|
|
82
153
|
parts = stdin.split(delimiter)
|
|
83
154
|
line = parts[0] if parts else ""
|
|
84
155
|
|
|
@@ -86,99 +157,207 @@ class ReadCommand:
|
|
|
86
157
|
if nchars is not None:
|
|
87
158
|
line = line[:nchars]
|
|
88
159
|
|
|
89
|
-
# Process backslash escapes if not in raw mode
|
|
90
|
-
if not raw_mode:
|
|
91
|
-
# Handle backslash-newline continuation (remove them)
|
|
160
|
+
# Process backslash escapes if not in raw mode and not -N mode
|
|
161
|
+
if not raw_mode and not no_split:
|
|
92
162
|
line = line.replace("\\\n", "")
|
|
93
|
-
# Handle other escapes
|
|
94
163
|
result = []
|
|
95
|
-
|
|
96
|
-
while
|
|
97
|
-
if line[
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
i += 2
|
|
164
|
+
ci = 0
|
|
165
|
+
while ci < len(line):
|
|
166
|
+
if line[ci] == "\\" and ci + 1 < len(line):
|
|
167
|
+
result.append(line[ci + 1])
|
|
168
|
+
ci += 2
|
|
101
169
|
else:
|
|
102
|
-
result.append(line[
|
|
103
|
-
|
|
170
|
+
result.append(line[ci])
|
|
171
|
+
ci += 1
|
|
104
172
|
line = "".join(result)
|
|
105
173
|
|
|
106
|
-
#
|
|
174
|
+
# Get IFS
|
|
107
175
|
ifs = ctx.env.get("IFS", " \t\n")
|
|
108
|
-
if ifs:
|
|
109
|
-
# Split on IFS characters
|
|
110
|
-
words = self._split_on_ifs(line, ifs)
|
|
111
|
-
else:
|
|
112
|
-
# Empty IFS - no splitting
|
|
113
|
-
words = [line] if line else []
|
|
114
176
|
|
|
115
177
|
# Handle -a option (read into array)
|
|
116
178
|
if array_name:
|
|
179
|
+
# Split on IFS for array assignment
|
|
180
|
+
if no_split:
|
|
181
|
+
words = [line] if line else []
|
|
182
|
+
elif ifs:
|
|
183
|
+
words = self._split_on_ifs(line, ifs)
|
|
184
|
+
else:
|
|
185
|
+
words = [line] if line else []
|
|
186
|
+
|
|
117
187
|
# Clear existing array elements
|
|
118
188
|
prefix = f"{array_name}_"
|
|
119
189
|
to_remove = [k for k in ctx.env if k.startswith(prefix) and not k.startswith(f"{array_name}__")]
|
|
120
190
|
for k in to_remove:
|
|
121
191
|
del ctx.env[k]
|
|
122
192
|
|
|
123
|
-
# Mark as array
|
|
124
193
|
ctx.env[f"{array_name}__is_array"] = "indexed"
|
|
125
194
|
|
|
126
|
-
# Store each word as array element
|
|
127
195
|
for idx, word in enumerate(words):
|
|
128
196
|
ctx.env[f"{array_name}_{idx}"] = word
|
|
129
197
|
|
|
130
|
-
|
|
131
|
-
return ExecResult(stdout="", stderr="", exit_code=exit_code)
|
|
198
|
+
return ExecResult(stdout="", stderr="", exit_code=1 if eof_reached else 0)
|
|
132
199
|
|
|
133
200
|
# Assign to variables
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
ctx.env[var] = " ".join(words[i:])
|
|
139
|
-
else:
|
|
140
|
-
ctx.env[var] = words[i]
|
|
201
|
+
if no_split:
|
|
202
|
+
# -N mode: no IFS splitting at all
|
|
203
|
+
if len(var_names) == 1:
|
|
204
|
+
ctx.env[var_names[0]] = line
|
|
141
205
|
else:
|
|
142
|
-
|
|
206
|
+
# Even with multiple vars, -N doesn't split
|
|
207
|
+
ctx.env[var_names[0]] = line
|
|
208
|
+
for v in var_names[1:]:
|
|
209
|
+
ctx.env[v] = ""
|
|
210
|
+
elif use_reply or len(var_names) == 1:
|
|
211
|
+
# Single variable or REPLY: no IFS splitting
|
|
212
|
+
# But strip leading/trailing IFS whitespace
|
|
213
|
+
stripped = self._strip_ifs_whitespace(line, ifs)
|
|
214
|
+
ctx.env[var_names[0]] = stripped
|
|
215
|
+
elif not ifs:
|
|
216
|
+
# Empty IFS: no splitting
|
|
217
|
+
ctx.env[var_names[0]] = line
|
|
218
|
+
for v in var_names[1:]:
|
|
219
|
+
ctx.env[v] = ""
|
|
220
|
+
else:
|
|
221
|
+
# Multiple variables: split on IFS
|
|
222
|
+
self._assign_split_vars(line, var_names, ifs, ctx)
|
|
223
|
+
|
|
224
|
+
return ExecResult(stdout="", stderr="", exit_code=1 if eof_reached else 0)
|
|
225
|
+
|
|
226
|
+
def _strip_ifs_whitespace(self, value: str, ifs: str) -> str:
|
|
227
|
+
"""Strip leading and trailing IFS whitespace characters."""
|
|
228
|
+
if not ifs:
|
|
229
|
+
return value
|
|
230
|
+
ifs_ws = set(c for c in ifs if c in " \t\n")
|
|
231
|
+
if not ifs_ws:
|
|
232
|
+
return value
|
|
233
|
+
# Strip leading
|
|
234
|
+
start = 0
|
|
235
|
+
while start < len(value) and value[start] in ifs_ws:
|
|
236
|
+
start += 1
|
|
237
|
+
# Strip trailing
|
|
238
|
+
end = len(value)
|
|
239
|
+
while end > start and value[end - 1] in ifs_ws:
|
|
240
|
+
end -= 1
|
|
241
|
+
return value[start:end]
|
|
242
|
+
|
|
243
|
+
def _assign_split_vars(self, line: str, var_names: list[str], ifs: str, ctx: CommandContext) -> None:
|
|
244
|
+
"""Split line on IFS and assign to multiple variables.
|
|
245
|
+
|
|
246
|
+
The last variable gets the remainder of the line (preserving
|
|
247
|
+
original separators from the input).
|
|
248
|
+
"""
|
|
249
|
+
ifs_ws = set(c for c in ifs if c in " \t\n")
|
|
250
|
+
ifs_nonws = set(c for c in ifs if c not in " \t\n")
|
|
251
|
+
|
|
252
|
+
# We need to track positions in the original line so the last
|
|
253
|
+
# variable gets the remainder from the original string
|
|
254
|
+
num_vars = len(var_names)
|
|
255
|
+
words = []
|
|
256
|
+
pos = 0
|
|
257
|
+
|
|
258
|
+
# Skip leading IFS whitespace
|
|
259
|
+
while pos < len(line) and line[pos] in ifs_ws:
|
|
260
|
+
pos += 1
|
|
143
261
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
262
|
+
for var_idx in range(num_vars):
|
|
263
|
+
if pos >= len(line):
|
|
264
|
+
# No more input - set remaining vars to empty
|
|
265
|
+
for vi in range(var_idx, num_vars):
|
|
266
|
+
ctx.env[var_names[vi]] = ""
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
if var_idx == num_vars - 1:
|
|
270
|
+
# Last variable: gets the rest of the line, with trailing
|
|
271
|
+
# IFS whitespace stripped
|
|
272
|
+
remainder = line[pos:]
|
|
273
|
+
# Strip trailing IFS whitespace
|
|
274
|
+
end = len(remainder)
|
|
275
|
+
while end > 0 and remainder[end - 1] in ifs_ws:
|
|
276
|
+
end -= 1
|
|
277
|
+
ctx.env[var_names[var_idx]] = remainder[:end]
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
# Collect next word
|
|
281
|
+
word_start = pos
|
|
282
|
+
while pos < len(line) and line[pos] not in ifs_ws and line[pos] not in ifs_nonws:
|
|
283
|
+
pos += 1
|
|
284
|
+
|
|
285
|
+
word = line[word_start:pos]
|
|
286
|
+
ctx.env[var_names[var_idx]] = word
|
|
287
|
+
|
|
288
|
+
# Skip IFS delimiters between words
|
|
289
|
+
# Whitespace IFS chars: skip all consecutive
|
|
290
|
+
# Non-whitespace IFS chars: each one is a delimiter
|
|
291
|
+
# Whitespace around non-whitespace is part of the delimiter
|
|
292
|
+
if pos < len(line):
|
|
293
|
+
# Skip leading whitespace
|
|
294
|
+
while pos < len(line) and line[pos] in ifs_ws:
|
|
295
|
+
pos += 1
|
|
296
|
+
# If we hit a non-whitespace delimiter, consume it
|
|
297
|
+
if pos < len(line) and line[pos] in ifs_nonws:
|
|
298
|
+
pos += 1
|
|
299
|
+
# Skip trailing whitespace after non-ws delimiter
|
|
300
|
+
while pos < len(line) and line[pos] in ifs_ws:
|
|
301
|
+
pos += 1
|
|
302
|
+
|
|
303
|
+
# Shouldn't reach here, but just in case
|
|
304
|
+
for vi in range(len(words), num_vars):
|
|
305
|
+
if var_names[vi] not in ctx.env:
|
|
306
|
+
ctx.env[var_names[vi]] = ""
|
|
147
307
|
|
|
148
308
|
def _split_on_ifs(self, value: str, ifs: str) -> list[str]:
|
|
149
|
-
"""Split a string on IFS characters.
|
|
309
|
+
"""Split a string on IFS characters.
|
|
310
|
+
|
|
311
|
+
Follows bash IFS splitting rules:
|
|
312
|
+
- IFS whitespace (space, tab, newline): leading/trailing stripped,
|
|
313
|
+
consecutive act as single separator
|
|
314
|
+
- IFS non-whitespace: each occurrence is a separator, consecutive
|
|
315
|
+
produce empty fields
|
|
316
|
+
- Mixed: whitespace adjacent to non-whitespace is part of the delimiter
|
|
317
|
+
"""
|
|
150
318
|
if not value:
|
|
151
319
|
return []
|
|
152
320
|
|
|
153
|
-
|
|
154
|
-
|
|
321
|
+
ifs_ws = set(c for c in ifs if c in " \t\n")
|
|
322
|
+
ifs_nonws = set(c for c in ifs if c not in " \t\n")
|
|
155
323
|
|
|
156
|
-
#
|
|
157
|
-
if
|
|
324
|
+
# Whitespace-only IFS: simple split (strips leading/trailing, merges consecutive)
|
|
325
|
+
if not ifs_nonws:
|
|
158
326
|
return value.split()
|
|
159
327
|
|
|
160
|
-
# Complex case with non-whitespace delimiters
|
|
161
328
|
result = []
|
|
162
329
|
current = []
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
330
|
+
pos = 0
|
|
331
|
+
|
|
332
|
+
# Skip leading IFS whitespace
|
|
333
|
+
while pos < len(value) and value[pos] in ifs_ws:
|
|
334
|
+
pos += 1
|
|
335
|
+
|
|
336
|
+
while pos < len(value):
|
|
337
|
+
c = value[pos]
|
|
338
|
+
if c in ifs_nonws:
|
|
339
|
+
# Non-whitespace delimiter: always produces field boundary
|
|
340
|
+
result.append("".join(current))
|
|
341
|
+
current = []
|
|
342
|
+
pos += 1
|
|
343
|
+
# Skip trailing IFS whitespace after non-ws delimiter
|
|
344
|
+
while pos < len(value) and value[pos] in ifs_ws:
|
|
345
|
+
pos += 1
|
|
346
|
+
elif c in ifs_ws:
|
|
347
|
+
# IFS whitespace
|
|
167
348
|
if current:
|
|
168
349
|
result.append("".join(current))
|
|
169
350
|
current = []
|
|
170
351
|
# Skip consecutive whitespace
|
|
171
|
-
while
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
#
|
|
175
|
-
result.append("".join(current))
|
|
176
|
-
current = []
|
|
177
|
-
i += 1
|
|
352
|
+
while pos < len(value) and value[pos] in ifs_ws:
|
|
353
|
+
pos += 1
|
|
354
|
+
# If next char is non-ws delimiter, it's part of this delimiter run
|
|
355
|
+
# (don't start a new field yet - the non-ws handler will do it)
|
|
178
356
|
else:
|
|
179
357
|
current.append(c)
|
|
180
|
-
|
|
358
|
+
pos += 1
|
|
181
359
|
|
|
360
|
+
# Add last field if non-empty
|
|
182
361
|
if current:
|
|
183
362
|
result.append("".join(current))
|
|
184
363
|
|
|
@@ -55,15 +55,9 @@ class ReadlinkCommand:
|
|
|
55
55
|
stat = await ctx.fs.lstat(resolved)
|
|
56
56
|
|
|
57
57
|
if canonicalize:
|
|
58
|
-
# Return the canonical path
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if not target.startswith("/"):
|
|
62
|
-
import os
|
|
63
|
-
target = ctx.fs.resolve_path(os.path.dirname(resolved), target)
|
|
64
|
-
stdout_parts.append(target)
|
|
65
|
-
else:
|
|
66
|
-
stdout_parts.append(resolved)
|
|
58
|
+
# Return the canonical path (fully resolved)
|
|
59
|
+
canonical = await ctx.fs.realpath(resolved)
|
|
60
|
+
stdout_parts.append(canonical)
|
|
67
61
|
else:
|
|
68
62
|
# Only works on symlinks
|
|
69
63
|
if stat.is_symbolic_link:
|
just_bash/commands/registry.py
CHANGED
|
@@ -127,6 +127,9 @@ COMMAND_NAMES = [
|
|
|
127
127
|
"tac",
|
|
128
128
|
"hostname",
|
|
129
129
|
"od",
|
|
130
|
+
"rmdir",
|
|
131
|
+
"time",
|
|
132
|
+
"whoami",
|
|
130
133
|
# Testing utilities
|
|
131
134
|
"argv.py",
|
|
132
135
|
# Builtins
|
|
@@ -243,6 +246,9 @@ _command_loaders: list[LazyCommandDef] = [
|
|
|
243
246
|
LazyCommandDef(name="comm", load=lambda: _load_comm()),
|
|
244
247
|
LazyCommandDef(name="strings", load=lambda: _load_strings()),
|
|
245
248
|
LazyCommandDef(name="od", load=lambda: _load_od()),
|
|
249
|
+
LazyCommandDef(name="rmdir", load=lambda: _load_rmdir()),
|
|
250
|
+
LazyCommandDef(name="time", load=lambda: _load_time()),
|
|
251
|
+
LazyCommandDef(name="whoami", load=lambda: _load_whoami()),
|
|
246
252
|
# Testing utilities
|
|
247
253
|
LazyCommandDef(name="argv.py", load=lambda: _load_argv()),
|
|
248
254
|
# Builtins
|
|
@@ -697,6 +703,24 @@ async def _load_od() -> Command:
|
|
|
697
703
|
return OdCommand()
|
|
698
704
|
|
|
699
705
|
|
|
706
|
+
async def _load_rmdir() -> Command:
|
|
707
|
+
"""Load the rmdir command."""
|
|
708
|
+
from .rmdir.rmdir import RmdirCommand
|
|
709
|
+
return RmdirCommand()
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
async def _load_time() -> Command:
|
|
713
|
+
"""Load the time command."""
|
|
714
|
+
from .time.time import TimeCommand
|
|
715
|
+
return TimeCommand()
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
async def _load_whoami() -> Command:
|
|
719
|
+
"""Load the whoami command."""
|
|
720
|
+
from .whoami.whoami import WhoamiCommand
|
|
721
|
+
return WhoamiCommand()
|
|
722
|
+
|
|
723
|
+
|
|
700
724
|
async def _load_argv() -> Command:
|
|
701
725
|
"""Load the argv.py command."""
|
|
702
726
|
from .argv.argv import ArgvCommand
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Rmdir command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: rmdir [OPTION]... DIRECTORY...
|
|
4
|
+
|
|
5
|
+
Remove empty directories.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-p, --parents remove DIRECTORY and its ancestors
|
|
9
|
+
-v, --verbose output a diagnostic for every directory processed
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ...types import CommandContext, ExecResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RmdirCommand:
|
|
16
|
+
"""The rmdir command."""
|
|
17
|
+
|
|
18
|
+
name = "rmdir"
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
|
+
"""Execute the rmdir command."""
|
|
22
|
+
parents = False
|
|
23
|
+
verbose = False
|
|
24
|
+
directories: list[str] = []
|
|
25
|
+
|
|
26
|
+
# Parse arguments
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(args):
|
|
29
|
+
arg = args[i]
|
|
30
|
+
if arg == "--":
|
|
31
|
+
directories.extend(args[i + 1:])
|
|
32
|
+
break
|
|
33
|
+
elif arg.startswith("--"):
|
|
34
|
+
if arg == "--parents":
|
|
35
|
+
parents = True
|
|
36
|
+
elif arg == "--verbose":
|
|
37
|
+
verbose = True
|
|
38
|
+
else:
|
|
39
|
+
return ExecResult(
|
|
40
|
+
stdout="",
|
|
41
|
+
stderr=f"rmdir: unrecognized option '{arg}'\n",
|
|
42
|
+
exit_code=1,
|
|
43
|
+
)
|
|
44
|
+
elif arg.startswith("-") and arg != "-":
|
|
45
|
+
for c in arg[1:]:
|
|
46
|
+
if c == "p":
|
|
47
|
+
parents = True
|
|
48
|
+
elif c == "v":
|
|
49
|
+
verbose = True
|
|
50
|
+
else:
|
|
51
|
+
return ExecResult(
|
|
52
|
+
stdout="",
|
|
53
|
+
stderr=f"rmdir: invalid option -- '{c}'\n",
|
|
54
|
+
exit_code=1,
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
directories.append(arg)
|
|
58
|
+
i += 1
|
|
59
|
+
|
|
60
|
+
if not directories:
|
|
61
|
+
return ExecResult(
|
|
62
|
+
stdout="",
|
|
63
|
+
stderr="rmdir: missing operand\n",
|
|
64
|
+
exit_code=1,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
stdout = ""
|
|
68
|
+
stderr = ""
|
|
69
|
+
exit_code = 0
|
|
70
|
+
|
|
71
|
+
for directory in directories:
|
|
72
|
+
result = await self._rmdir_one(
|
|
73
|
+
ctx, directory, parents, verbose
|
|
74
|
+
)
|
|
75
|
+
stdout += result.stdout
|
|
76
|
+
stderr += result.stderr
|
|
77
|
+
if result.exit_code != 0:
|
|
78
|
+
exit_code = result.exit_code
|
|
79
|
+
|
|
80
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
81
|
+
|
|
82
|
+
async def _rmdir_one(
|
|
83
|
+
self,
|
|
84
|
+
ctx: CommandContext,
|
|
85
|
+
directory: str,
|
|
86
|
+
parents: bool,
|
|
87
|
+
verbose: bool,
|
|
88
|
+
) -> ExecResult:
|
|
89
|
+
"""Remove a single directory (and optionally its parents)."""
|
|
90
|
+
path = ctx.fs.resolve_path(ctx.cwd, directory)
|
|
91
|
+
stdout = ""
|
|
92
|
+
stderr = ""
|
|
93
|
+
|
|
94
|
+
# Get list of directories to remove
|
|
95
|
+
dirs_to_remove = [path]
|
|
96
|
+
if parents:
|
|
97
|
+
# Add parent directories up to root
|
|
98
|
+
current = path
|
|
99
|
+
while True:
|
|
100
|
+
parent = self._dirname(current)
|
|
101
|
+
if parent == current or parent == "/":
|
|
102
|
+
break
|
|
103
|
+
dirs_to_remove.append(parent)
|
|
104
|
+
current = parent
|
|
105
|
+
|
|
106
|
+
# Try to remove directories
|
|
107
|
+
for dir_path in dirs_to_remove:
|
|
108
|
+
try:
|
|
109
|
+
# Check if path exists
|
|
110
|
+
try:
|
|
111
|
+
stat = await ctx.fs.stat(dir_path)
|
|
112
|
+
except FileNotFoundError:
|
|
113
|
+
return ExecResult(
|
|
114
|
+
stdout=stdout,
|
|
115
|
+
stderr=stderr + f"rmdir: failed to remove '{dir_path}': No such file or directory\n",
|
|
116
|
+
exit_code=1,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if not stat.is_directory:
|
|
120
|
+
return ExecResult(
|
|
121
|
+
stdout=stdout,
|
|
122
|
+
stderr=stderr + f"rmdir: failed to remove '{dir_path}': Not a directory\n",
|
|
123
|
+
exit_code=1,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Check if directory is empty
|
|
127
|
+
contents = await ctx.fs.readdir(dir_path)
|
|
128
|
+
if contents:
|
|
129
|
+
return ExecResult(
|
|
130
|
+
stdout=stdout,
|
|
131
|
+
stderr=stderr + f"rmdir: failed to remove '{dir_path}': Directory not empty\n",
|
|
132
|
+
exit_code=1,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Remove the directory
|
|
136
|
+
await ctx.fs.rm(dir_path)
|
|
137
|
+
|
|
138
|
+
if verbose:
|
|
139
|
+
stdout += f"rmdir: removing directory, '{dir_path}'\n"
|
|
140
|
+
|
|
141
|
+
except OSError as e:
|
|
142
|
+
return ExecResult(
|
|
143
|
+
stdout=stdout,
|
|
144
|
+
stderr=stderr + f"rmdir: failed to remove '{dir_path}': {e}\n",
|
|
145
|
+
exit_code=1,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=0)
|
|
149
|
+
|
|
150
|
+
def _dirname(self, path: str) -> str:
|
|
151
|
+
"""Get the directory name of a path."""
|
|
152
|
+
if path == "/":
|
|
153
|
+
return "/"
|
|
154
|
+
path = path.rstrip("/")
|
|
155
|
+
last_slash = path.rfind("/")
|
|
156
|
+
if last_slash == -1:
|
|
157
|
+
return "."
|
|
158
|
+
if last_slash == 0:
|
|
159
|
+
return "/"
|
|
160
|
+
return path[:last_slash]
|