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,150 @@
1
+ """Comm command implementation."""
2
+
3
+ from ...types import CommandContext, ExecResult
4
+
5
+
6
+ class CommCommand:
7
+ """The comm command - compare two sorted files line by line."""
8
+
9
+ name = "comm"
10
+
11
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
12
+ """Execute the comm command."""
13
+ suppress_col1 = False # Lines unique to file1
14
+ suppress_col2 = False # Lines unique to file2
15
+ suppress_col3 = False # Lines common to both
16
+ files: list[str] = []
17
+
18
+ i = 0
19
+ while i < len(args):
20
+ arg = args[i]
21
+ if arg == "--help":
22
+ return ExecResult(
23
+ stdout="Usage: comm [OPTION]... FILE1 FILE2\nCompare two sorted files line by line.\n",
24
+ stderr="",
25
+ exit_code=0,
26
+ )
27
+ elif arg == "-1":
28
+ suppress_col1 = True
29
+ elif arg == "-2":
30
+ suppress_col2 = True
31
+ elif arg == "-3":
32
+ suppress_col3 = True
33
+ elif arg.startswith("-") and len(arg) > 1 and not arg.startswith("--"):
34
+ # Handle combined flags like -12, -23, -13, -123
35
+ for c in arg[1:]:
36
+ if c == "1":
37
+ suppress_col1 = True
38
+ elif c == "2":
39
+ suppress_col2 = True
40
+ elif c == "3":
41
+ suppress_col3 = True
42
+ else:
43
+ return ExecResult(
44
+ stdout="",
45
+ stderr=f"comm: invalid option -- '{c}'\n",
46
+ exit_code=1,
47
+ )
48
+ elif arg == "--":
49
+ files.extend(args[i + 1:])
50
+ break
51
+ else:
52
+ files.append(arg)
53
+ i += 1
54
+
55
+ if len(files) < 2:
56
+ return ExecResult(
57
+ stdout="",
58
+ stderr="comm: missing operand\n",
59
+ exit_code=1,
60
+ )
61
+
62
+ file1, file2 = files[0], files[1]
63
+
64
+ # Read files
65
+ try:
66
+ if file1 == "-":
67
+ content1 = ctx.stdin
68
+ else:
69
+ path1 = ctx.fs.resolve_path(ctx.cwd, file1)
70
+ content1 = await ctx.fs.read_file(path1)
71
+ except FileNotFoundError:
72
+ return ExecResult(
73
+ stdout="",
74
+ stderr=f"comm: {file1}: No such file or directory\n",
75
+ exit_code=1,
76
+ )
77
+
78
+ try:
79
+ if file2 == "-":
80
+ content2 = ctx.stdin
81
+ else:
82
+ path2 = ctx.fs.resolve_path(ctx.cwd, file2)
83
+ content2 = await ctx.fs.read_file(path2)
84
+ except FileNotFoundError:
85
+ return ExecResult(
86
+ stdout="",
87
+ stderr=f"comm: {file2}: No such file or directory\n",
88
+ exit_code=1,
89
+ )
90
+
91
+ lines1 = content1.rstrip("\n").split("\n") if content1.strip() else []
92
+ lines2 = content2.rstrip("\n").split("\n") if content2.strip() else []
93
+
94
+ result = self._compare(lines1, lines2, suppress_col1, suppress_col2, suppress_col3)
95
+ return ExecResult(stdout=result, stderr="", exit_code=0)
96
+
97
+ def _compare(
98
+ self,
99
+ lines1: list[str],
100
+ lines2: list[str],
101
+ suppress_col1: bool,
102
+ suppress_col2: bool,
103
+ suppress_col3: bool,
104
+ ) -> str:
105
+ """Compare two sorted lists of lines."""
106
+ result_lines = []
107
+ i, j = 0, 0
108
+
109
+ while i < len(lines1) or j < len(lines2):
110
+ if i >= len(lines1):
111
+ # Only file2 lines left (unique to file2 = column 2)
112
+ if not suppress_col2:
113
+ prefix = ""
114
+ if not suppress_col1:
115
+ prefix += "\t"
116
+ result_lines.append(prefix + lines2[j])
117
+ j += 1
118
+ elif j >= len(lines2):
119
+ # Only file1 lines left (unique to file1 = column 1)
120
+ if not suppress_col1:
121
+ result_lines.append(lines1[i])
122
+ i += 1
123
+ elif lines1[i] < lines2[j]:
124
+ # Line unique to file1 (column 1)
125
+ if not suppress_col1:
126
+ result_lines.append(lines1[i])
127
+ i += 1
128
+ elif lines1[i] > lines2[j]:
129
+ # Line unique to file2 (column 2)
130
+ if not suppress_col2:
131
+ prefix = ""
132
+ if not suppress_col1:
133
+ prefix += "\t"
134
+ result_lines.append(prefix + lines2[j])
135
+ j += 1
136
+ else:
137
+ # Lines are equal (common = column 3)
138
+ if not suppress_col3:
139
+ prefix = ""
140
+ if not suppress_col1:
141
+ prefix += "\t"
142
+ if not suppress_col2:
143
+ prefix += "\t"
144
+ result_lines.append(prefix + lines1[i])
145
+ i += 1
146
+ j += 1
147
+
148
+ if result_lines:
149
+ return "\n".join(result_lines) + "\n"
150
+ return ""
@@ -0,0 +1,5 @@
1
+ """Compression commands."""
2
+
3
+ from .compression import GzipCommand, GunzipCommand, ZcatCommand
4
+
5
+ __all__ = ["GzipCommand", "GunzipCommand", "ZcatCommand"]
@@ -0,0 +1,298 @@
1
+ """Compression command implementations (gzip, gunzip, zcat)."""
2
+
3
+ import gzip
4
+ import struct
5
+ from ...types import CommandContext, ExecResult
6
+
7
+
8
+ class GzipCommand:
9
+ """The gzip command - compress files."""
10
+
11
+ name = "gzip"
12
+
13
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
14
+ """Execute the gzip command."""
15
+ decompress = False
16
+ keep_original = False
17
+ force = False
18
+ stdout_mode = False
19
+ verbose = False
20
+ quiet = False
21
+ list_mode = False
22
+ test_mode = False
23
+ recursive = False
24
+ suffix = ".gz"
25
+ level = 6
26
+ files: list[str] = []
27
+
28
+ i = 0
29
+ while i < len(args):
30
+ arg = args[i]
31
+ if arg in ("-d", "--decompress", "--uncompress"):
32
+ decompress = True
33
+ elif arg in ("-k", "--keep"):
34
+ keep_original = True
35
+ elif arg in ("-f", "--force"):
36
+ force = True
37
+ elif arg in ("-c", "--stdout", "--to-stdout"):
38
+ stdout_mode = True
39
+ elif arg in ("-v", "--verbose"):
40
+ verbose = True
41
+ elif arg in ("-q", "--quiet"):
42
+ quiet = True
43
+ elif arg in ("-l", "--list"):
44
+ list_mode = True
45
+ elif arg in ("-t", "--test"):
46
+ test_mode = True
47
+ elif arg in ("-r", "--recursive"):
48
+ recursive = True
49
+ elif arg in ("-S", "--suffix"):
50
+ i += 1
51
+ if i < len(args):
52
+ suffix = args[i]
53
+ if not suffix.startswith("."):
54
+ suffix = "." + suffix
55
+ elif arg.startswith("--suffix="):
56
+ suffix = arg[9:]
57
+ if not suffix.startswith("."):
58
+ suffix = "." + suffix
59
+ elif arg in ("-1", "--fast"):
60
+ level = 1
61
+ elif arg in ("-9", "--best"):
62
+ level = 9
63
+ elif arg.startswith("-") and len(arg) == 2 and arg[1].isdigit():
64
+ level = int(arg[1])
65
+ elif arg == "--help":
66
+ return ExecResult(
67
+ stdout="Usage: gzip [OPTION]... [FILE]...\n",
68
+ stderr="",
69
+ exit_code=0,
70
+ )
71
+ elif arg == "--":
72
+ files.extend(args[i + 1:])
73
+ break
74
+ elif arg.startswith("-"):
75
+ # Handle combined short options
76
+ for c in arg[1:]:
77
+ if c == "d":
78
+ decompress = True
79
+ elif c == "k":
80
+ keep_original = True
81
+ elif c == "f":
82
+ force = True
83
+ elif c == "c":
84
+ stdout_mode = True
85
+ elif c == "v":
86
+ verbose = True
87
+ elif c == "q":
88
+ quiet = True
89
+ elif c == "l":
90
+ list_mode = True
91
+ elif c == "t":
92
+ test_mode = True
93
+ elif c == "r":
94
+ recursive = True
95
+ elif c.isdigit():
96
+ level = int(c)
97
+ else:
98
+ files.append(arg)
99
+ i += 1
100
+
101
+ # Read from stdin if no files
102
+ if not files:
103
+ if ctx.stdin:
104
+ return await self._process_stdin(ctx, decompress, level)
105
+ return ExecResult(
106
+ stdout="",
107
+ stderr="gzip: missing operand\n",
108
+ exit_code=1,
109
+ )
110
+
111
+ # Expand files if recursive
112
+ if recursive:
113
+ files = await self._expand_recursive(ctx, files)
114
+
115
+ stdout_parts = []
116
+ stderr_parts = []
117
+ exit_code = 0
118
+
119
+ # List mode
120
+ if list_mode:
121
+ stdout_parts.append(f"{'compressed':>12} {'uncompressed':>12} {'ratio':>6} name\n")
122
+ total_compressed = 0
123
+ total_uncompressed = 0
124
+
125
+ for file in files:
126
+ try:
127
+ path = ctx.fs.resolve_path(ctx.cwd, file)
128
+ content = await ctx.fs.read_file_bytes(path)
129
+ compressed_size = len(content)
130
+
131
+ try:
132
+ decompressed = gzip.decompress(content)
133
+ uncompressed_size = len(decompressed)
134
+ ratio = (1.0 - compressed_size / uncompressed_size) * 100 if uncompressed_size > 0 else 0
135
+ stdout_parts.append(f"{compressed_size:>12} {uncompressed_size:>12} {ratio:>5.1f}% {file}\n")
136
+ total_compressed += compressed_size
137
+ total_uncompressed += uncompressed_size
138
+ except Exception:
139
+ if not quiet:
140
+ stderr_parts.append(f"gzip: {file}: not in gzip format\n")
141
+ exit_code = 1
142
+ except FileNotFoundError:
143
+ if not quiet:
144
+ stderr_parts.append(f"gzip: {file}: No such file or directory\n")
145
+ exit_code = 1
146
+
147
+ if len(files) > 1 and total_uncompressed > 0:
148
+ total_ratio = (1.0 - total_compressed / total_uncompressed) * 100
149
+ stdout_parts.append(f"{total_compressed:>12} {total_uncompressed:>12} {total_ratio:>5.1f}% (totals)\n")
150
+
151
+ return ExecResult(stdout="".join(stdout_parts), stderr="".join(stderr_parts), exit_code=exit_code)
152
+
153
+ # Test mode
154
+ if test_mode:
155
+ for file in files:
156
+ try:
157
+ path = ctx.fs.resolve_path(ctx.cwd, file)
158
+ content = await ctx.fs.read_file_bytes(path)
159
+ try:
160
+ gzip.decompress(content)
161
+ if verbose and not quiet:
162
+ stderr_parts.append(f"{file}:\tOK\n")
163
+ except Exception as e:
164
+ if not quiet:
165
+ stderr_parts.append(f"gzip: {file}: {e}\n")
166
+ exit_code = 1
167
+ except FileNotFoundError:
168
+ if not quiet:
169
+ stderr_parts.append(f"gzip: {file}: No such file or directory\n")
170
+ exit_code = 1
171
+
172
+ return ExecResult(stdout="", stderr="".join(stderr_parts), exit_code=exit_code)
173
+
174
+ for file in files:
175
+ try:
176
+ path = ctx.fs.resolve_path(ctx.cwd, file)
177
+ content = await ctx.fs.read_file_bytes(path)
178
+ original_size = len(content)
179
+
180
+ if decompress:
181
+ if not file.endswith(suffix) and not force:
182
+ if not quiet:
183
+ stderr_parts.append(f"gzip: {file}: unknown suffix -- ignored\n")
184
+ continue
185
+ try:
186
+ result = gzip.decompress(content)
187
+ except Exception as e:
188
+ if not quiet:
189
+ stderr_parts.append(f"gzip: {file}: {e}\n")
190
+ exit_code = 1
191
+ continue
192
+
193
+ if stdout_mode:
194
+ stdout_parts.append(result.decode("utf-8", errors="replace"))
195
+ else:
196
+ # Remove suffix to get output path
197
+ if path.endswith(suffix):
198
+ new_path = path[:-len(suffix)]
199
+ else:
200
+ new_path = path + ".out"
201
+ await ctx.fs.write_file(new_path, result)
202
+ if not keep_original:
203
+ await ctx.fs.rm(path)
204
+
205
+ if verbose and not quiet:
206
+ ratio = (1.0 - original_size / len(result)) * 100 if len(result) > 0 else 0
207
+ stderr_parts.append(f"{file}:\t{ratio:.1f}% -- replaced with {new_path if not stdout_mode else 'stdout'}\n")
208
+ else:
209
+ result = gzip.compress(content, compresslevel=level)
210
+
211
+ if stdout_mode:
212
+ # Can't output binary to stdout in text mode
213
+ stdout_parts.append(f"<binary gzip data, {len(result)} bytes>")
214
+ else:
215
+ new_path = path + suffix
216
+ await ctx.fs.write_file(new_path, result)
217
+ if not keep_original:
218
+ await ctx.fs.rm(path)
219
+
220
+ if verbose and not quiet:
221
+ ratio = (1.0 - len(result) / original_size) * 100 if original_size > 0 else 0
222
+ stderr_parts.append(f"{file}:\t{ratio:.1f}% -- replaced with {new_path if not stdout_mode else 'stdout'}\n")
223
+
224
+ except FileNotFoundError:
225
+ if not quiet:
226
+ stderr_parts.append(f"gzip: {file}: No such file or directory\n")
227
+ exit_code = 1
228
+ except IsADirectoryError:
229
+ if not quiet:
230
+ stderr_parts.append(f"gzip: {file}: Is a directory\n")
231
+ exit_code = 1
232
+
233
+ stdout = "".join(stdout_parts)
234
+ stderr = "".join(stderr_parts)
235
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
236
+
237
+ async def _expand_recursive(self, ctx: CommandContext, paths: list[str]) -> list[str]:
238
+ """Expand directories recursively to list of files."""
239
+ result = []
240
+ for path in paths:
241
+ full_path = ctx.fs.resolve_path(ctx.cwd, path)
242
+ try:
243
+ stat = await ctx.fs.stat(full_path)
244
+ if stat.is_directory:
245
+ # List directory contents
246
+ entries = await ctx.fs.readdir(full_path)
247
+ sub_paths = [f"{path}/{e}" for e in entries]
248
+ result.extend(await self._expand_recursive(ctx, sub_paths))
249
+ else:
250
+ result.append(path)
251
+ except Exception:
252
+ result.append(path) # Let the main loop handle errors
253
+ return result
254
+
255
+ async def _process_stdin(
256
+ self, ctx: CommandContext, decompress: bool, level: int
257
+ ) -> ExecResult:
258
+ """Process stdin."""
259
+ try:
260
+ content = ctx.stdin.encode("utf-8")
261
+ if decompress:
262
+ result = gzip.decompress(content)
263
+ return ExecResult(
264
+ stdout=result.decode("utf-8", errors="replace"),
265
+ stderr="",
266
+ exit_code=0,
267
+ )
268
+ else:
269
+ result = gzip.compress(content, compresslevel=level)
270
+ return ExecResult(
271
+ stdout=f"<binary gzip data, {len(result)} bytes>",
272
+ stderr="",
273
+ exit_code=0,
274
+ )
275
+ except Exception as e:
276
+ return ExecResult(stdout="", stderr=f"gzip: {e}\n", exit_code=1)
277
+
278
+
279
+ class GunzipCommand:
280
+ """The gunzip command - decompress files."""
281
+
282
+ name = "gunzip"
283
+
284
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
285
+ """Execute gunzip (gzip -d)."""
286
+ gzip_cmd = GzipCommand()
287
+ return await gzip_cmd.execute(["-d"] + args, ctx)
288
+
289
+
290
+ class ZcatCommand:
291
+ """The zcat command - decompress to stdout."""
292
+
293
+ name = "zcat"
294
+
295
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
296
+ """Execute zcat (gzip -dc)."""
297
+ gzip_cmd = GzipCommand()
298
+ return await gzip_cmd.execute(["-dc"] + args, ctx)
@@ -0,0 +1,5 @@
1
+ """Cp command implementation."""
2
+
3
+ from .cp import CpCommand
4
+
5
+ __all__ = ["CpCommand"]
@@ -0,0 +1,149 @@
1
+ """Cp command implementation.
2
+
3
+ Usage: cp [OPTION]... SOURCE... DEST
4
+
5
+ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.
6
+
7
+ Options:
8
+ -r, -R, --recursive copy directories recursively
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 CpCommand:
17
+ """The cp command."""
18
+
19
+ name = "cp"
20
+
21
+ async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
22
+ """Execute the cp command."""
23
+ recursive = 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 == "--recursive":
37
+ recursive = 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"cp: unrecognized option '{arg}'\n",
46
+ exit_code=1,
47
+ )
48
+ elif arg.startswith("-") and arg != "-":
49
+ for c in arg[1:]:
50
+ if c in ("r", "R"):
51
+ recursive = 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"cp: 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="cp: missing file operand\n",
71
+ exit_code=1,
72
+ )
73
+ return ExecResult(
74
+ stdout="",
75
+ stderr=f"cp: 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"cp: 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 if source is a directory
108
+ try:
109
+ st = await ctx.fs.stat(src_path)
110
+ if st.is_directory and not recursive:
111
+ stderr += f"cp: -r not specified; omitting directory '{src}'\n"
112
+ exit_code = 1
113
+ continue
114
+ except FileNotFoundError:
115
+ stderr += f"cp: cannot stat '{src}': No such file or directory\n"
116
+ exit_code = 1
117
+ continue
118
+
119
+ # Determine target path
120
+ if dest_is_dir:
121
+ # Get basename of source
122
+ basename = src.rstrip("/").split("/")[-1]
123
+ target_path = ctx.fs.resolve_path(dest_path, basename)
124
+ else:
125
+ target_path = dest_path
126
+
127
+ # Check no-clobber
128
+ if no_clobber:
129
+ try:
130
+ await ctx.fs.stat(target_path)
131
+ # File exists, skip
132
+ continue
133
+ except FileNotFoundError:
134
+ pass
135
+
136
+ await ctx.fs.cp(src_path, target_path, recursive=recursive)
137
+ if verbose:
138
+ stdout += f"'{src}' -> '{dest}'\n"
139
+ except FileNotFoundError:
140
+ stderr += f"cp: cannot stat '{src}': No such file or directory\n"
141
+ exit_code = 1
142
+ except IsADirectoryError:
143
+ stderr += f"cp: -r not specified; omitting directory '{src}'\n"
144
+ exit_code = 1
145
+ except OSError as e:
146
+ stderr += f"cp: cannot copy '{src}': {e}\n"
147
+ exit_code = 1
148
+
149
+ return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
@@ -0,0 +1,5 @@
1
+ """Curl command."""
2
+
3
+ from .curl import CurlCommand
4
+
5
+ __all__ = ["CurlCommand"]