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,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,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,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)
|