just-bash 0.1.5__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/__init__.py +55 -0
- just_bash/ast/__init__.py +213 -0
- just_bash/ast/factory.py +320 -0
- just_bash/ast/types.py +953 -0
- just_bash/bash.py +220 -0
- just_bash/commands/__init__.py +23 -0
- just_bash/commands/argv/__init__.py +5 -0
- just_bash/commands/argv/argv.py +21 -0
- just_bash/commands/awk/__init__.py +5 -0
- just_bash/commands/awk/awk.py +1168 -0
- just_bash/commands/base64/__init__.py +5 -0
- just_bash/commands/base64/base64.py +138 -0
- just_bash/commands/basename/__init__.py +5 -0
- just_bash/commands/basename/basename.py +72 -0
- just_bash/commands/bash/__init__.py +5 -0
- just_bash/commands/bash/bash.py +188 -0
- just_bash/commands/cat/__init__.py +5 -0
- just_bash/commands/cat/cat.py +173 -0
- just_bash/commands/checksum/__init__.py +5 -0
- just_bash/commands/checksum/checksum.py +179 -0
- just_bash/commands/chmod/__init__.py +5 -0
- just_bash/commands/chmod/chmod.py +216 -0
- just_bash/commands/column/__init__.py +5 -0
- just_bash/commands/column/column.py +180 -0
- just_bash/commands/comm/__init__.py +5 -0
- just_bash/commands/comm/comm.py +150 -0
- just_bash/commands/compression/__init__.py +5 -0
- just_bash/commands/compression/compression.py +298 -0
- just_bash/commands/cp/__init__.py +5 -0
- just_bash/commands/cp/cp.py +149 -0
- just_bash/commands/curl/__init__.py +5 -0
- just_bash/commands/curl/curl.py +801 -0
- just_bash/commands/cut/__init__.py +5 -0
- just_bash/commands/cut/cut.py +327 -0
- just_bash/commands/date/__init__.py +5 -0
- just_bash/commands/date/date.py +258 -0
- just_bash/commands/diff/__init__.py +5 -0
- just_bash/commands/diff/diff.py +118 -0
- just_bash/commands/dirname/__init__.py +5 -0
- just_bash/commands/dirname/dirname.py +56 -0
- just_bash/commands/du/__init__.py +5 -0
- just_bash/commands/du/du.py +150 -0
- just_bash/commands/echo/__init__.py +5 -0
- just_bash/commands/echo/echo.py +125 -0
- just_bash/commands/env/__init__.py +5 -0
- just_bash/commands/env/env.py +163 -0
- just_bash/commands/expand/__init__.py +5 -0
- just_bash/commands/expand/expand.py +299 -0
- just_bash/commands/expr/__init__.py +5 -0
- just_bash/commands/expr/expr.py +273 -0
- just_bash/commands/file/__init__.py +5 -0
- just_bash/commands/file/file.py +274 -0
- just_bash/commands/find/__init__.py +5 -0
- just_bash/commands/find/find.py +623 -0
- just_bash/commands/fold/__init__.py +5 -0
- just_bash/commands/fold/fold.py +160 -0
- just_bash/commands/grep/__init__.py +5 -0
- just_bash/commands/grep/grep.py +418 -0
- just_bash/commands/head/__init__.py +5 -0
- just_bash/commands/head/head.py +167 -0
- just_bash/commands/help/__init__.py +5 -0
- just_bash/commands/help/help.py +67 -0
- just_bash/commands/hostname/__init__.py +5 -0
- just_bash/commands/hostname/hostname.py +21 -0
- just_bash/commands/html_to_markdown/__init__.py +5 -0
- just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
- just_bash/commands/join/__init__.py +5 -0
- just_bash/commands/join/join.py +252 -0
- just_bash/commands/jq/__init__.py +5 -0
- just_bash/commands/jq/jq.py +280 -0
- just_bash/commands/ln/__init__.py +5 -0
- just_bash/commands/ln/ln.py +127 -0
- just_bash/commands/ls/__init__.py +5 -0
- just_bash/commands/ls/ls.py +280 -0
- just_bash/commands/mkdir/__init__.py +5 -0
- just_bash/commands/mkdir/mkdir.py +92 -0
- just_bash/commands/mv/__init__.py +5 -0
- just_bash/commands/mv/mv.py +142 -0
- just_bash/commands/nl/__init__.py +5 -0
- just_bash/commands/nl/nl.py +180 -0
- just_bash/commands/od/__init__.py +5 -0
- just_bash/commands/od/od.py +157 -0
- just_bash/commands/paste/__init__.py +5 -0
- just_bash/commands/paste/paste.py +100 -0
- just_bash/commands/printf/__init__.py +5 -0
- just_bash/commands/printf/printf.py +157 -0
- just_bash/commands/pwd/__init__.py +5 -0
- just_bash/commands/pwd/pwd.py +23 -0
- just_bash/commands/read/__init__.py +5 -0
- just_bash/commands/read/read.py +185 -0
- just_bash/commands/readlink/__init__.py +5 -0
- just_bash/commands/readlink/readlink.py +86 -0
- just_bash/commands/registry.py +844 -0
- just_bash/commands/rev/__init__.py +5 -0
- just_bash/commands/rev/rev.py +74 -0
- just_bash/commands/rg/__init__.py +5 -0
- just_bash/commands/rg/rg.py +1048 -0
- just_bash/commands/rm/__init__.py +5 -0
- just_bash/commands/rm/rm.py +106 -0
- just_bash/commands/search_engine/__init__.py +13 -0
- just_bash/commands/search_engine/matcher.py +170 -0
- just_bash/commands/search_engine/regex.py +159 -0
- just_bash/commands/sed/__init__.py +5 -0
- just_bash/commands/sed/sed.py +863 -0
- just_bash/commands/seq/__init__.py +5 -0
- just_bash/commands/seq/seq.py +190 -0
- just_bash/commands/shell/__init__.py +5 -0
- just_bash/commands/shell/shell.py +206 -0
- just_bash/commands/sleep/__init__.py +5 -0
- just_bash/commands/sleep/sleep.py +62 -0
- just_bash/commands/sort/__init__.py +5 -0
- just_bash/commands/sort/sort.py +411 -0
- just_bash/commands/split/__init__.py +5 -0
- just_bash/commands/split/split.py +237 -0
- just_bash/commands/sqlite3/__init__.py +5 -0
- just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
- just_bash/commands/stat/__init__.py +5 -0
- just_bash/commands/stat/stat.py +150 -0
- just_bash/commands/strings/__init__.py +5 -0
- just_bash/commands/strings/strings.py +150 -0
- just_bash/commands/tac/__init__.py +5 -0
- just_bash/commands/tac/tac.py +158 -0
- just_bash/commands/tail/__init__.py +5 -0
- just_bash/commands/tail/tail.py +180 -0
- just_bash/commands/tar/__init__.py +5 -0
- just_bash/commands/tar/tar.py +1067 -0
- just_bash/commands/tee/__init__.py +5 -0
- just_bash/commands/tee/tee.py +63 -0
- just_bash/commands/timeout/__init__.py +5 -0
- just_bash/commands/timeout/timeout.py +188 -0
- just_bash/commands/touch/__init__.py +5 -0
- just_bash/commands/touch/touch.py +91 -0
- just_bash/commands/tr/__init__.py +5 -0
- just_bash/commands/tr/tr.py +297 -0
- just_bash/commands/tree/__init__.py +5 -0
- just_bash/commands/tree/tree.py +139 -0
- just_bash/commands/true/__init__.py +5 -0
- just_bash/commands/true/true.py +32 -0
- just_bash/commands/uniq/__init__.py +5 -0
- just_bash/commands/uniq/uniq.py +323 -0
- just_bash/commands/wc/__init__.py +5 -0
- just_bash/commands/wc/wc.py +169 -0
- just_bash/commands/which/__init__.py +5 -0
- just_bash/commands/which/which.py +52 -0
- just_bash/commands/xan/__init__.py +5 -0
- just_bash/commands/xan/xan.py +1663 -0
- just_bash/commands/xargs/__init__.py +5 -0
- just_bash/commands/xargs/xargs.py +136 -0
- just_bash/commands/yq/__init__.py +5 -0
- just_bash/commands/yq/yq.py +848 -0
- just_bash/fs/__init__.py +29 -0
- just_bash/fs/in_memory_fs.py +621 -0
- just_bash/fs/mountable_fs.py +504 -0
- just_bash/fs/overlay_fs.py +894 -0
- just_bash/fs/read_write_fs.py +455 -0
- just_bash/interpreter/__init__.py +37 -0
- just_bash/interpreter/builtins/__init__.py +92 -0
- just_bash/interpreter/builtins/alias.py +154 -0
- just_bash/interpreter/builtins/cd.py +76 -0
- just_bash/interpreter/builtins/control.py +127 -0
- just_bash/interpreter/builtins/declare.py +336 -0
- just_bash/interpreter/builtins/export.py +56 -0
- just_bash/interpreter/builtins/let.py +44 -0
- just_bash/interpreter/builtins/local.py +57 -0
- just_bash/interpreter/builtins/mapfile.py +152 -0
- just_bash/interpreter/builtins/misc.py +378 -0
- just_bash/interpreter/builtins/readonly.py +80 -0
- just_bash/interpreter/builtins/set.py +234 -0
- just_bash/interpreter/builtins/shopt.py +201 -0
- just_bash/interpreter/builtins/source.py +136 -0
- just_bash/interpreter/builtins/test.py +290 -0
- just_bash/interpreter/builtins/unset.py +53 -0
- just_bash/interpreter/conditionals.py +387 -0
- just_bash/interpreter/control_flow.py +381 -0
- just_bash/interpreter/errors.py +116 -0
- just_bash/interpreter/expansion.py +1156 -0
- just_bash/interpreter/interpreter.py +813 -0
- just_bash/interpreter/types.py +134 -0
- just_bash/network/__init__.py +1 -0
- just_bash/parser/__init__.py +39 -0
- just_bash/parser/lexer.py +948 -0
- just_bash/parser/parser.py +2162 -0
- just_bash/py.typed +0 -0
- just_bash/query_engine/__init__.py +83 -0
- just_bash/query_engine/builtins/__init__.py +1283 -0
- just_bash/query_engine/evaluator.py +578 -0
- just_bash/query_engine/parser.py +525 -0
- just_bash/query_engine/tokenizer.py +329 -0
- just_bash/query_engine/types.py +373 -0
- just_bash/types.py +180 -0
- just_bash-0.1.5.dist-info/METADATA +410 -0
- just_bash-0.1.5.dist-info/RECORD +193 -0
- just_bash-0.1.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Ls command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: ls [OPTION]... [FILE]...
|
|
4
|
+
|
|
5
|
+
List information about the FILEs (the current directory by default).
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-a, --all do not ignore entries starting with .
|
|
9
|
+
-A, --almost-all do not list implied . and ..
|
|
10
|
+
-l use a long listing format
|
|
11
|
+
-1 list one file per line
|
|
12
|
+
-R, --recursive list subdirectories recursively
|
|
13
|
+
-h, --human-readable with -l, print sizes in human readable format
|
|
14
|
+
-d, --directory list directories themselves, not their contents
|
|
15
|
+
-F, --classify append indicator (one of */=>@|) to entries
|
|
16
|
+
-S sort by file size, largest first
|
|
17
|
+
-t sort by modification time, newest first
|
|
18
|
+
-r, --reverse reverse order while sorting
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import stat
|
|
22
|
+
from ...types import CommandContext, ExecResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def format_size(size: int, human_readable: bool = False) -> str:
|
|
26
|
+
"""Format file size."""
|
|
27
|
+
if not human_readable:
|
|
28
|
+
return str(size)
|
|
29
|
+
|
|
30
|
+
for unit in ['B', 'K', 'M', 'G', 'T']:
|
|
31
|
+
if size < 1024:
|
|
32
|
+
if unit == 'B':
|
|
33
|
+
return str(size)
|
|
34
|
+
return f"{size:.1f}{unit}"
|
|
35
|
+
size //= 1024
|
|
36
|
+
return f"{size:.1f}P"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def format_mode(mode: int, is_dir: bool, is_link: bool) -> str:
|
|
40
|
+
"""Format file mode as rwxrwxrwx string."""
|
|
41
|
+
if is_dir:
|
|
42
|
+
result = 'd'
|
|
43
|
+
elif is_link:
|
|
44
|
+
result = 'l'
|
|
45
|
+
else:
|
|
46
|
+
result = '-'
|
|
47
|
+
|
|
48
|
+
# Owner
|
|
49
|
+
result += 'r' if mode & stat.S_IRUSR else '-'
|
|
50
|
+
result += 'w' if mode & stat.S_IWUSR else '-'
|
|
51
|
+
result += 'x' if mode & stat.S_IXUSR else '-'
|
|
52
|
+
# Group
|
|
53
|
+
result += 'r' if mode & stat.S_IRGRP else '-'
|
|
54
|
+
result += 'w' if mode & stat.S_IWGRP else '-'
|
|
55
|
+
result += 'x' if mode & stat.S_IXGRP else '-'
|
|
56
|
+
# Other
|
|
57
|
+
result += 'r' if mode & stat.S_IROTH else '-'
|
|
58
|
+
result += 'w' if mode & stat.S_IWOTH else '-'
|
|
59
|
+
result += 'x' if mode & stat.S_IXOTH else '-'
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class LsCommand:
|
|
65
|
+
"""The ls command."""
|
|
66
|
+
|
|
67
|
+
name = "ls"
|
|
68
|
+
|
|
69
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
70
|
+
"""Execute the ls command."""
|
|
71
|
+
# Options
|
|
72
|
+
show_all = False
|
|
73
|
+
almost_all = False
|
|
74
|
+
long_format = False
|
|
75
|
+
one_per_line = False
|
|
76
|
+
recursive = False
|
|
77
|
+
human_readable = False
|
|
78
|
+
dir_only = False
|
|
79
|
+
classify = False
|
|
80
|
+
reverse = False
|
|
81
|
+
sort_by_size = False
|
|
82
|
+
sort_by_time = False
|
|
83
|
+
|
|
84
|
+
paths: list[str] = []
|
|
85
|
+
|
|
86
|
+
# Parse arguments
|
|
87
|
+
i = 0
|
|
88
|
+
while i < len(args):
|
|
89
|
+
arg = args[i]
|
|
90
|
+
if arg == "--":
|
|
91
|
+
paths.extend(args[i + 1:])
|
|
92
|
+
break
|
|
93
|
+
elif arg.startswith("--"):
|
|
94
|
+
if arg == "--all":
|
|
95
|
+
show_all = True
|
|
96
|
+
elif arg == "--almost-all":
|
|
97
|
+
almost_all = True
|
|
98
|
+
elif arg == "--recursive":
|
|
99
|
+
recursive = True
|
|
100
|
+
elif arg == "--human-readable":
|
|
101
|
+
human_readable = True
|
|
102
|
+
elif arg == "--directory":
|
|
103
|
+
dir_only = True
|
|
104
|
+
elif arg == "--classify":
|
|
105
|
+
classify = True
|
|
106
|
+
elif arg == "--reverse":
|
|
107
|
+
reverse = True
|
|
108
|
+
else:
|
|
109
|
+
return ExecResult(
|
|
110
|
+
stdout="",
|
|
111
|
+
stderr=f"ls: unrecognized option '{arg}'\n",
|
|
112
|
+
exit_code=2,
|
|
113
|
+
)
|
|
114
|
+
elif arg.startswith("-") and arg != "-":
|
|
115
|
+
for c in arg[1:]:
|
|
116
|
+
if c == 'a':
|
|
117
|
+
show_all = True
|
|
118
|
+
elif c == 'A':
|
|
119
|
+
almost_all = True
|
|
120
|
+
elif c == 'l':
|
|
121
|
+
long_format = True
|
|
122
|
+
elif c == '1':
|
|
123
|
+
one_per_line = True
|
|
124
|
+
elif c == 'R':
|
|
125
|
+
recursive = True
|
|
126
|
+
elif c == 'h':
|
|
127
|
+
human_readable = True
|
|
128
|
+
elif c == 'd':
|
|
129
|
+
dir_only = True
|
|
130
|
+
elif c == 'F':
|
|
131
|
+
classify = True
|
|
132
|
+
elif c == 'r':
|
|
133
|
+
reverse = True
|
|
134
|
+
elif c == 'S':
|
|
135
|
+
sort_by_size = True
|
|
136
|
+
elif c == 't':
|
|
137
|
+
sort_by_time = True
|
|
138
|
+
else:
|
|
139
|
+
return ExecResult(
|
|
140
|
+
stdout="",
|
|
141
|
+
stderr=f"ls: invalid option -- '{c}'\n",
|
|
142
|
+
exit_code=2,
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
paths.append(arg)
|
|
146
|
+
i += 1
|
|
147
|
+
|
|
148
|
+
# Default to current directory
|
|
149
|
+
if not paths:
|
|
150
|
+
paths = ["."]
|
|
151
|
+
|
|
152
|
+
stdout = ""
|
|
153
|
+
stderr = ""
|
|
154
|
+
exit_code = 0
|
|
155
|
+
|
|
156
|
+
for path_idx, path in enumerate(paths):
|
|
157
|
+
# Resolve path
|
|
158
|
+
full_path = ctx.fs.resolve_path(ctx.cwd, path)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
st = await ctx.fs.stat(full_path)
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
stderr += f"ls: cannot access '{path}': No such file or directory\n"
|
|
164
|
+
exit_code = 2
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if st.is_directory and not dir_only:
|
|
168
|
+
# List directory contents
|
|
169
|
+
if len(paths) > 1:
|
|
170
|
+
if path_idx > 0:
|
|
171
|
+
stdout += "\n"
|
|
172
|
+
stdout += f"{path}:\n"
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
entries = await ctx.fs.readdir(full_path)
|
|
176
|
+
except PermissionError:
|
|
177
|
+
stderr += f"ls: cannot open directory '{path}': Permission denied\n"
|
|
178
|
+
exit_code = 2
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Filter hidden files
|
|
182
|
+
if not show_all and not almost_all:
|
|
183
|
+
entries = [e for e in entries if not e.startswith('.')]
|
|
184
|
+
elif almost_all:
|
|
185
|
+
# -A shows hidden files but not . and ..
|
|
186
|
+
entries = [e for e in entries if e not in (".", "..")]
|
|
187
|
+
# else: show_all shows everything including . and ..
|
|
188
|
+
|
|
189
|
+
# Sort entries
|
|
190
|
+
if sort_by_size or sort_by_time:
|
|
191
|
+
# Get stat for each entry
|
|
192
|
+
entries_with_stats: list[tuple[str, int, float]] = []
|
|
193
|
+
for e in entries:
|
|
194
|
+
entry_path = f"{full_path}/{e}" if full_path != "/" else f"/{e}"
|
|
195
|
+
try:
|
|
196
|
+
entry_stat = await ctx.fs.stat(entry_path)
|
|
197
|
+
entries_with_stats.append((e, entry_stat.size, entry_stat.mtime or 0))
|
|
198
|
+
except Exception:
|
|
199
|
+
entries_with_stats.append((e, 0, 0))
|
|
200
|
+
|
|
201
|
+
if sort_by_time:
|
|
202
|
+
# Sort by mtime, newest first (descending)
|
|
203
|
+
entries_with_stats.sort(key=lambda x: (x[2], x[0]), reverse=True)
|
|
204
|
+
elif sort_by_size:
|
|
205
|
+
# Sort by size, largest first (descending)
|
|
206
|
+
entries_with_stats.sort(key=lambda x: (x[1], x[0]), reverse=True)
|
|
207
|
+
|
|
208
|
+
entries = [e for e, _, _ in entries_with_stats]
|
|
209
|
+
if reverse:
|
|
210
|
+
entries.reverse()
|
|
211
|
+
else:
|
|
212
|
+
entries.sort(reverse=reverse)
|
|
213
|
+
|
|
214
|
+
if long_format:
|
|
215
|
+
for entry in entries:
|
|
216
|
+
entry_path = f"{full_path}/{entry}" if full_path != "/" else f"/{entry}"
|
|
217
|
+
try:
|
|
218
|
+
entry_stat = await ctx.fs.stat(entry_path)
|
|
219
|
+
mode_str = format_mode(entry_stat.mode, entry_stat.is_directory, entry_stat.is_symbolic_link)
|
|
220
|
+
size_str = format_size(entry_stat.size, human_readable)
|
|
221
|
+
name = entry
|
|
222
|
+
if classify:
|
|
223
|
+
if entry_stat.is_directory:
|
|
224
|
+
name += "/"
|
|
225
|
+
elif entry_stat.is_symbolic_link:
|
|
226
|
+
name += "@"
|
|
227
|
+
elif entry_stat.mode & stat.S_IXUSR:
|
|
228
|
+
name += "*"
|
|
229
|
+
stdout += f"{mode_str} {entry_stat.nlink:2d} user user {size_str:>8s} Jan 1 00:00 {name}\n"
|
|
230
|
+
except FileNotFoundError:
|
|
231
|
+
stdout += f"????????? ? ? ? ? ? {entry}\n"
|
|
232
|
+
elif one_per_line:
|
|
233
|
+
for entry in entries:
|
|
234
|
+
name = entry
|
|
235
|
+
if classify:
|
|
236
|
+
entry_path = f"{full_path}/{entry}" if full_path != "/" else f"/{entry}"
|
|
237
|
+
try:
|
|
238
|
+
entry_stat = await ctx.fs.stat(entry_path)
|
|
239
|
+
if entry_stat.is_directory:
|
|
240
|
+
name += "/"
|
|
241
|
+
elif entry_stat.is_symbolic_link:
|
|
242
|
+
name += "@"
|
|
243
|
+
except FileNotFoundError:
|
|
244
|
+
pass
|
|
245
|
+
stdout += f"{name}\n"
|
|
246
|
+
else:
|
|
247
|
+
# Simple format
|
|
248
|
+
output_entries = []
|
|
249
|
+
for entry in entries:
|
|
250
|
+
name = entry
|
|
251
|
+
if classify:
|
|
252
|
+
entry_path = f"{full_path}/{entry}" if full_path != "/" else f"/{entry}"
|
|
253
|
+
try:
|
|
254
|
+
entry_stat = await ctx.fs.stat(entry_path)
|
|
255
|
+
if entry_stat.is_directory:
|
|
256
|
+
name += "/"
|
|
257
|
+
elif entry_stat.is_symbolic_link:
|
|
258
|
+
name += "@"
|
|
259
|
+
except FileNotFoundError:
|
|
260
|
+
pass
|
|
261
|
+
output_entries.append(name)
|
|
262
|
+
stdout += " ".join(output_entries) + "\n"
|
|
263
|
+
else:
|
|
264
|
+
# Single file/directory
|
|
265
|
+
name = path
|
|
266
|
+
if classify and st.is_directory:
|
|
267
|
+
name += "/"
|
|
268
|
+
elif classify and st.is_symbolic_link:
|
|
269
|
+
name += "@"
|
|
270
|
+
elif classify and st.mode & stat.S_IXUSR:
|
|
271
|
+
name += "*"
|
|
272
|
+
|
|
273
|
+
if long_format:
|
|
274
|
+
mode_str = format_mode(st.mode, st.is_directory, st.is_symbolic_link)
|
|
275
|
+
size_str = format_size(st.size, human_readable)
|
|
276
|
+
stdout += f"{mode_str} {st.nlink:2d} user user {size_str:>8s} Jan 1 00:00 {name}\n"
|
|
277
|
+
else:
|
|
278
|
+
stdout += f"{name}\n"
|
|
279
|
+
|
|
280
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Mkdir command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: mkdir [OPTION]... DIRECTORY...
|
|
4
|
+
|
|
5
|
+
Create the DIRECTORY(ies), if they do not already exist.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-p, --parents make parent directories as needed
|
|
9
|
+
-v, --verbose print a message for each created directory
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ...types import CommandContext, ExecResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MkdirCommand:
|
|
16
|
+
"""The mkdir command."""
|
|
17
|
+
|
|
18
|
+
name = "mkdir"
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
|
+
"""Execute the mkdir command."""
|
|
22
|
+
recursive = False
|
|
23
|
+
verbose = False
|
|
24
|
+
dirs: list[str] = []
|
|
25
|
+
|
|
26
|
+
# Parse arguments
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(args):
|
|
29
|
+
arg = args[i]
|
|
30
|
+
if arg == "--":
|
|
31
|
+
dirs.extend(args[i + 1:])
|
|
32
|
+
break
|
|
33
|
+
elif arg.startswith("--"):
|
|
34
|
+
if arg == "--parents":
|
|
35
|
+
recursive = True
|
|
36
|
+
elif arg == "--verbose":
|
|
37
|
+
verbose = True
|
|
38
|
+
else:
|
|
39
|
+
return ExecResult(
|
|
40
|
+
stdout="",
|
|
41
|
+
stderr=f"mkdir: 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
|
+
recursive = True
|
|
48
|
+
elif c == "v":
|
|
49
|
+
verbose = True
|
|
50
|
+
else:
|
|
51
|
+
return ExecResult(
|
|
52
|
+
stdout="",
|
|
53
|
+
stderr=f"mkdir: invalid option -- '{c}'\n",
|
|
54
|
+
exit_code=1,
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
dirs.append(arg)
|
|
58
|
+
i += 1
|
|
59
|
+
|
|
60
|
+
if not dirs:
|
|
61
|
+
return ExecResult(
|
|
62
|
+
stdout="",
|
|
63
|
+
stderr="mkdir: missing operand\n",
|
|
64
|
+
exit_code=1,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
stdout = ""
|
|
68
|
+
stderr = ""
|
|
69
|
+
exit_code = 0
|
|
70
|
+
|
|
71
|
+
for d in dirs:
|
|
72
|
+
try:
|
|
73
|
+
path = ctx.fs.resolve_path(ctx.cwd, d)
|
|
74
|
+
await ctx.fs.mkdir(path, recursive=recursive)
|
|
75
|
+
if verbose:
|
|
76
|
+
stdout += f"mkdir: created directory '{d}'\n"
|
|
77
|
+
except (FileExistsError, OSError) as e:
|
|
78
|
+
if "EEXIST" in str(e) or isinstance(e, FileExistsError):
|
|
79
|
+
if not recursive:
|
|
80
|
+
stderr += f"mkdir: cannot create directory '{d}': File exists\n"
|
|
81
|
+
exit_code = 1
|
|
82
|
+
elif "ENOENT" in str(e):
|
|
83
|
+
stderr += f"mkdir: cannot create directory '{d}': No such file or directory\n"
|
|
84
|
+
exit_code = 1
|
|
85
|
+
else:
|
|
86
|
+
stderr += f"mkdir: cannot create directory '{d}': {e}\n"
|
|
87
|
+
exit_code = 1
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
stderr += f"mkdir: cannot create directory '{d}': No such file or directory\n"
|
|
90
|
+
exit_code = 1
|
|
91
|
+
|
|
92
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Mv command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: mv [OPTION]... SOURCE... DEST
|
|
4
|
+
|
|
5
|
+
Rename SOURCE to DEST, or move SOURCE(s) to DIRECTORY.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-f, --force do not prompt before overwriting
|
|
9
|
+
-n, --no-clobber do not overwrite an existing file
|
|
10
|
+
-v, --verbose explain what is being done
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ...types import CommandContext, ExecResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MvCommand:
|
|
17
|
+
"""The mv command."""
|
|
18
|
+
|
|
19
|
+
name = "mv"
|
|
20
|
+
|
|
21
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
22
|
+
"""Execute the mv command."""
|
|
23
|
+
force = False
|
|
24
|
+
no_clobber = False
|
|
25
|
+
verbose = False
|
|
26
|
+
paths: list[str] = []
|
|
27
|
+
|
|
28
|
+
# Parse arguments
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(args):
|
|
31
|
+
arg = args[i]
|
|
32
|
+
if arg == "--":
|
|
33
|
+
paths.extend(args[i + 1:])
|
|
34
|
+
break
|
|
35
|
+
elif arg.startswith("--"):
|
|
36
|
+
if arg == "--force":
|
|
37
|
+
force = True
|
|
38
|
+
elif arg == "--no-clobber":
|
|
39
|
+
no_clobber = True
|
|
40
|
+
elif arg == "--verbose":
|
|
41
|
+
verbose = True
|
|
42
|
+
else:
|
|
43
|
+
return ExecResult(
|
|
44
|
+
stdout="",
|
|
45
|
+
stderr=f"mv: unrecognized option '{arg}'\n",
|
|
46
|
+
exit_code=1,
|
|
47
|
+
)
|
|
48
|
+
elif arg.startswith("-") and arg != "-":
|
|
49
|
+
for c in arg[1:]:
|
|
50
|
+
if c == "f":
|
|
51
|
+
force = True
|
|
52
|
+
elif c == "n":
|
|
53
|
+
no_clobber = True
|
|
54
|
+
elif c == "v":
|
|
55
|
+
verbose = True
|
|
56
|
+
else:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"mv: invalid option -- '{c}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
paths.append(arg)
|
|
64
|
+
i += 1
|
|
65
|
+
|
|
66
|
+
if len(paths) < 2:
|
|
67
|
+
if len(paths) == 0:
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="",
|
|
70
|
+
stderr="mv: missing file operand\n",
|
|
71
|
+
exit_code=1,
|
|
72
|
+
)
|
|
73
|
+
return ExecResult(
|
|
74
|
+
stdout="",
|
|
75
|
+
stderr=f"mv: missing destination file operand after '{paths[0]}'\n",
|
|
76
|
+
exit_code=1,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
sources = paths[:-1]
|
|
80
|
+
dest = paths[-1]
|
|
81
|
+
dest_path = ctx.fs.resolve_path(ctx.cwd, dest)
|
|
82
|
+
|
|
83
|
+
# Check if destination is a directory
|
|
84
|
+
dest_is_dir = False
|
|
85
|
+
try:
|
|
86
|
+
st = await ctx.fs.stat(dest_path)
|
|
87
|
+
dest_is_dir = st.is_directory
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# Multiple sources require destination to be a directory
|
|
92
|
+
if len(sources) > 1 and not dest_is_dir:
|
|
93
|
+
return ExecResult(
|
|
94
|
+
stdout="",
|
|
95
|
+
stderr=f"mv: target '{dest}' is not a directory\n",
|
|
96
|
+
exit_code=1,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
stdout = ""
|
|
100
|
+
stderr = ""
|
|
101
|
+
exit_code = 0
|
|
102
|
+
|
|
103
|
+
for src in sources:
|
|
104
|
+
try:
|
|
105
|
+
src_path = ctx.fs.resolve_path(ctx.cwd, src)
|
|
106
|
+
|
|
107
|
+
# Check source exists
|
|
108
|
+
try:
|
|
109
|
+
await ctx.fs.stat(src_path)
|
|
110
|
+
except FileNotFoundError:
|
|
111
|
+
stderr += f"mv: cannot stat '{src}': No such file or directory\n"
|
|
112
|
+
exit_code = 1
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Determine target path
|
|
116
|
+
if dest_is_dir:
|
|
117
|
+
basename = src.rstrip("/").split("/")[-1]
|
|
118
|
+
target_path = ctx.fs.resolve_path(dest_path, basename)
|
|
119
|
+
else:
|
|
120
|
+
target_path = dest_path
|
|
121
|
+
|
|
122
|
+
# Check no-clobber (takes precedence over force)
|
|
123
|
+
if no_clobber:
|
|
124
|
+
try:
|
|
125
|
+
await ctx.fs.stat(target_path)
|
|
126
|
+
# File exists, skip
|
|
127
|
+
continue
|
|
128
|
+
except FileNotFoundError:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
await ctx.fs.mv(src_path, target_path)
|
|
132
|
+
if verbose:
|
|
133
|
+
renamed_to = dest if not dest_is_dir else f"{dest}/{src.rstrip('/').split('/')[-1]}"
|
|
134
|
+
stdout += f"renamed '{src}' -> '{renamed_to}'\n"
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
stderr += f"mv: cannot stat '{src}': No such file or directory\n"
|
|
137
|
+
exit_code = 1
|
|
138
|
+
except OSError as e:
|
|
139
|
+
stderr += f"mv: cannot move '{src}': {e}\n"
|
|
140
|
+
exit_code = 1
|
|
141
|
+
|
|
142
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|