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.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. 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,5 @@
1
+ """Mkdir command implementation."""
2
+
3
+ from .mkdir import MkdirCommand
4
+
5
+ __all__ = ["MkdirCommand"]
@@ -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,5 @@
1
+ """Mv command implementation."""
2
+
3
+ from .mv import MvCommand
4
+
5
+ __all__ = ["MvCommand"]
@@ -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)
@@ -0,0 +1,5 @@
1
+ """Nl command."""
2
+
3
+ from .nl import NlCommand
4
+
5
+ __all__ = ["NlCommand"]