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,179 @@
|
|
|
1
|
+
"""Checksum command implementations (md5sum, sha1sum, sha256sum)."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from ...types import CommandContext, ExecResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChecksumCommand:
|
|
8
|
+
"""Base class for checksum commands."""
|
|
9
|
+
|
|
10
|
+
name = "checksum"
|
|
11
|
+
algorithm = "md5"
|
|
12
|
+
|
|
13
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
14
|
+
"""Execute the checksum command."""
|
|
15
|
+
check_mode = False
|
|
16
|
+
binary_mode = False
|
|
17
|
+
quiet = False
|
|
18
|
+
status_only = False
|
|
19
|
+
files: list[str] = []
|
|
20
|
+
|
|
21
|
+
i = 0
|
|
22
|
+
while i < len(args):
|
|
23
|
+
arg = args[i]
|
|
24
|
+
if arg == "-c" or arg == "--check":
|
|
25
|
+
check_mode = True
|
|
26
|
+
elif arg == "-b" or arg == "--binary":
|
|
27
|
+
binary_mode = True
|
|
28
|
+
elif arg == "-t" or arg == "--text":
|
|
29
|
+
binary_mode = False
|
|
30
|
+
elif arg == "--quiet":
|
|
31
|
+
quiet = True
|
|
32
|
+
elif arg == "--status":
|
|
33
|
+
status_only = True
|
|
34
|
+
elif arg == "--help":
|
|
35
|
+
return ExecResult(
|
|
36
|
+
stdout=f"Usage: {self.name} [OPTION]... [FILE]...\n",
|
|
37
|
+
stderr="",
|
|
38
|
+
exit_code=0,
|
|
39
|
+
)
|
|
40
|
+
elif arg == "--":
|
|
41
|
+
files.extend(args[i + 1:])
|
|
42
|
+
break
|
|
43
|
+
elif arg.startswith("-") and arg != "-":
|
|
44
|
+
return ExecResult(
|
|
45
|
+
stdout="",
|
|
46
|
+
stderr=f"{self.name}: invalid option -- '{arg[1]}'\n",
|
|
47
|
+
exit_code=1,
|
|
48
|
+
)
|
|
49
|
+
else:
|
|
50
|
+
files.append(arg)
|
|
51
|
+
i += 1
|
|
52
|
+
|
|
53
|
+
if not files:
|
|
54
|
+
files = ["-"]
|
|
55
|
+
|
|
56
|
+
if check_mode:
|
|
57
|
+
return await self._check_sums(files, ctx, quiet, status_only)
|
|
58
|
+
else:
|
|
59
|
+
return await self._compute_sums(files, ctx, binary_mode)
|
|
60
|
+
|
|
61
|
+
async def _compute_sums(
|
|
62
|
+
self, files: list[str], ctx: CommandContext, binary_mode: bool
|
|
63
|
+
) -> ExecResult:
|
|
64
|
+
"""Compute checksums for files."""
|
|
65
|
+
stdout_parts = []
|
|
66
|
+
stderr = ""
|
|
67
|
+
exit_code = 0
|
|
68
|
+
|
|
69
|
+
for file in files:
|
|
70
|
+
try:
|
|
71
|
+
if file == "-":
|
|
72
|
+
content = ctx.stdin.encode("utf-8")
|
|
73
|
+
else:
|
|
74
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
75
|
+
content = await ctx.fs.read_file_bytes(path)
|
|
76
|
+
|
|
77
|
+
h = hashlib.new(self.algorithm)
|
|
78
|
+
h.update(content)
|
|
79
|
+
checksum = h.hexdigest()
|
|
80
|
+
|
|
81
|
+
mode_char = "*" if binary_mode else " "
|
|
82
|
+
stdout_parts.append(f"{checksum} {mode_char}{file}")
|
|
83
|
+
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
stderr += f"{self.name}: {file}: No such file or directory\n"
|
|
86
|
+
exit_code = 1
|
|
87
|
+
except IsADirectoryError:
|
|
88
|
+
stderr += f"{self.name}: {file}: Is a directory\n"
|
|
89
|
+
exit_code = 1
|
|
90
|
+
|
|
91
|
+
stdout = "\n".join(stdout_parts)
|
|
92
|
+
if stdout:
|
|
93
|
+
stdout += "\n"
|
|
94
|
+
|
|
95
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
96
|
+
|
|
97
|
+
async def _check_sums(
|
|
98
|
+
self, files: list[str], ctx: CommandContext, quiet: bool, status_only: bool
|
|
99
|
+
) -> ExecResult:
|
|
100
|
+
"""Check checksums from files."""
|
|
101
|
+
stdout_parts = []
|
|
102
|
+
stderr = ""
|
|
103
|
+
failed_count = 0
|
|
104
|
+
total_count = 0
|
|
105
|
+
|
|
106
|
+
for file in files:
|
|
107
|
+
try:
|
|
108
|
+
if file == "-":
|
|
109
|
+
content = ctx.stdin
|
|
110
|
+
else:
|
|
111
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
112
|
+
content = await ctx.fs.read_file(path)
|
|
113
|
+
|
|
114
|
+
for line in content.strip().split("\n"):
|
|
115
|
+
if not line:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Parse checksum line: "hash filename" or "hash *filename"
|
|
119
|
+
parts = line.split(None, 1)
|
|
120
|
+
if len(parts) != 2:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
expected_hash = parts[0]
|
|
124
|
+
filename = parts[1].lstrip("* ")
|
|
125
|
+
total_count += 1
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
file_path = ctx.fs.resolve_path(ctx.cwd, filename)
|
|
129
|
+
file_content = await ctx.fs.read_file_bytes(file_path)
|
|
130
|
+
h = hashlib.new(self.algorithm)
|
|
131
|
+
h.update(file_content)
|
|
132
|
+
actual_hash = h.hexdigest()
|
|
133
|
+
|
|
134
|
+
if actual_hash == expected_hash:
|
|
135
|
+
if not quiet and not status_only:
|
|
136
|
+
stdout_parts.append(f"{filename}: OK")
|
|
137
|
+
else:
|
|
138
|
+
failed_count += 1
|
|
139
|
+
if not status_only:
|
|
140
|
+
stdout_parts.append(f"{filename}: FAILED")
|
|
141
|
+
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
failed_count += 1
|
|
144
|
+
if not status_only:
|
|
145
|
+
stderr += f"{self.name}: {filename}: No such file or directory\n"
|
|
146
|
+
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
stderr += f"{self.name}: {file}: No such file or directory\n"
|
|
149
|
+
|
|
150
|
+
if failed_count > 0 and not status_only:
|
|
151
|
+
stderr += f"{self.name}: WARNING: {failed_count} computed checksum did NOT match\n"
|
|
152
|
+
|
|
153
|
+
stdout = "\n".join(stdout_parts)
|
|
154
|
+
if stdout:
|
|
155
|
+
stdout += "\n"
|
|
156
|
+
|
|
157
|
+
exit_code = 1 if failed_count > 0 else 0
|
|
158
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class Md5sumCommand(ChecksumCommand):
|
|
162
|
+
"""The md5sum command."""
|
|
163
|
+
|
|
164
|
+
name = "md5sum"
|
|
165
|
+
algorithm = "md5"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Sha1sumCommand(ChecksumCommand):
|
|
169
|
+
"""The sha1sum command."""
|
|
170
|
+
|
|
171
|
+
name = "sha1sum"
|
|
172
|
+
algorithm = "sha1"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Sha256sumCommand(ChecksumCommand):
|
|
176
|
+
"""The sha256sum command."""
|
|
177
|
+
|
|
178
|
+
name = "sha256sum"
|
|
179
|
+
algorithm = "sha256"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Chmod command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: chmod [OPTION]... MODE FILE...
|
|
4
|
+
|
|
5
|
+
Change the mode of each FILE to MODE.
|
|
6
|
+
|
|
7
|
+
MODE can be:
|
|
8
|
+
- Octal number (e.g., 755, 644)
|
|
9
|
+
- Symbolic mode (e.g., u+x, g-w, o=r, a+x)
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
-R, --recursive change files and directories recursively
|
|
13
|
+
-v, --verbose output a diagnostic for every file processed
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from ...types import CommandContext, ExecResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ChmodCommand:
|
|
21
|
+
"""The chmod command."""
|
|
22
|
+
|
|
23
|
+
name = "chmod"
|
|
24
|
+
|
|
25
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
26
|
+
"""Execute the chmod command."""
|
|
27
|
+
recursive = False
|
|
28
|
+
verbose = False
|
|
29
|
+
mode_str = None
|
|
30
|
+
files: list[str] = []
|
|
31
|
+
|
|
32
|
+
# Parse arguments
|
|
33
|
+
i = 0
|
|
34
|
+
while i < len(args):
|
|
35
|
+
arg = args[i]
|
|
36
|
+
if arg == "--":
|
|
37
|
+
files.extend(args[i + 1:])
|
|
38
|
+
break
|
|
39
|
+
elif arg.startswith("--"):
|
|
40
|
+
if arg == "--recursive":
|
|
41
|
+
recursive = True
|
|
42
|
+
elif arg == "--verbose":
|
|
43
|
+
verbose = True
|
|
44
|
+
else:
|
|
45
|
+
return ExecResult(
|
|
46
|
+
stdout="",
|
|
47
|
+
stderr=f"chmod: unrecognized option '{arg}'\n",
|
|
48
|
+
exit_code=1,
|
|
49
|
+
)
|
|
50
|
+
elif arg.startswith("-") and arg != "-" and not re.match(r'^-[0-7]+$', arg):
|
|
51
|
+
for c in arg[1:]:
|
|
52
|
+
if c == "R":
|
|
53
|
+
recursive = True
|
|
54
|
+
elif c == "v":
|
|
55
|
+
verbose = True
|
|
56
|
+
else:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"chmod: invalid option -- '{c}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
elif mode_str is None:
|
|
63
|
+
mode_str = arg
|
|
64
|
+
else:
|
|
65
|
+
files.append(arg)
|
|
66
|
+
i += 1
|
|
67
|
+
|
|
68
|
+
if mode_str is None:
|
|
69
|
+
return ExecResult(
|
|
70
|
+
stdout="",
|
|
71
|
+
stderr="chmod: missing operand\n",
|
|
72
|
+
exit_code=1,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if not files:
|
|
76
|
+
return ExecResult(
|
|
77
|
+
stdout="",
|
|
78
|
+
stderr=f"chmod: missing operand after '{mode_str}'\n",
|
|
79
|
+
exit_code=1,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
stdout = ""
|
|
83
|
+
stderr = ""
|
|
84
|
+
exit_code = 0
|
|
85
|
+
|
|
86
|
+
for f in files:
|
|
87
|
+
try:
|
|
88
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
89
|
+
await self._chmod_path(ctx, path, mode_str, recursive, verbose, f)
|
|
90
|
+
if verbose:
|
|
91
|
+
stdout += f"mode of '{f}' changed\n"
|
|
92
|
+
except FileNotFoundError:
|
|
93
|
+
stderr += f"chmod: cannot access '{f}': No such file or directory\n"
|
|
94
|
+
exit_code = 1
|
|
95
|
+
except ValueError as e:
|
|
96
|
+
stderr += f"chmod: invalid mode: '{mode_str}'\n"
|
|
97
|
+
exit_code = 1
|
|
98
|
+
except OSError as e:
|
|
99
|
+
stderr += f"chmod: changing permissions of '{f}': {e}\n"
|
|
100
|
+
exit_code = 1
|
|
101
|
+
|
|
102
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
103
|
+
|
|
104
|
+
async def _chmod_path(
|
|
105
|
+
self,
|
|
106
|
+
ctx: CommandContext,
|
|
107
|
+
path: str,
|
|
108
|
+
mode_str: str,
|
|
109
|
+
recursive: bool,
|
|
110
|
+
verbose: bool,
|
|
111
|
+
display_name: str,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Change mode of a path, optionally recursively."""
|
|
114
|
+
# Get current stat
|
|
115
|
+
st = await ctx.fs.stat(path)
|
|
116
|
+
current_mode = st.mode & 0o7777 # Get permission bits
|
|
117
|
+
|
|
118
|
+
# Calculate new mode
|
|
119
|
+
new_mode = self._parse_mode(mode_str, current_mode)
|
|
120
|
+
|
|
121
|
+
# Apply mode
|
|
122
|
+
await ctx.fs.chmod(path, new_mode)
|
|
123
|
+
|
|
124
|
+
# Recurse into directories
|
|
125
|
+
if recursive and st.is_directory:
|
|
126
|
+
entries = await ctx.fs.readdir(path)
|
|
127
|
+
for entry in entries:
|
|
128
|
+
child_path = f"{path}/{entry}"
|
|
129
|
+
child_name = f"{display_name}/{entry}"
|
|
130
|
+
await self._chmod_path(ctx, child_path, mode_str, recursive, verbose, child_name)
|
|
131
|
+
|
|
132
|
+
def _parse_mode(self, mode_str: str, current_mode: int) -> int:
|
|
133
|
+
"""Parse mode string and return numeric mode."""
|
|
134
|
+
# Try octal first
|
|
135
|
+
if re.match(r'^[0-7]+$', mode_str):
|
|
136
|
+
return int(mode_str, 8)
|
|
137
|
+
|
|
138
|
+
# Symbolic mode parsing
|
|
139
|
+
# Format: [ugoa]*([-+=]([rwxXst]*|[ugo]))+
|
|
140
|
+
new_mode = current_mode
|
|
141
|
+
|
|
142
|
+
# Split into clauses separated by commas
|
|
143
|
+
for clause in mode_str.split(","):
|
|
144
|
+
new_mode = self._apply_symbolic_mode(clause.strip(), new_mode)
|
|
145
|
+
|
|
146
|
+
return new_mode
|
|
147
|
+
|
|
148
|
+
def _apply_symbolic_mode(self, clause: str, current_mode: int) -> int:
|
|
149
|
+
"""Apply a single symbolic mode clause."""
|
|
150
|
+
# Parse who (ugoa)
|
|
151
|
+
who_chars = ""
|
|
152
|
+
i = 0
|
|
153
|
+
while i < len(clause) and clause[i] in "ugoa":
|
|
154
|
+
who_chars += clause[i]
|
|
155
|
+
i += 1
|
|
156
|
+
|
|
157
|
+
# Default to 'a' if no who specified
|
|
158
|
+
if not who_chars:
|
|
159
|
+
who_chars = "a"
|
|
160
|
+
|
|
161
|
+
# Parse operations
|
|
162
|
+
while i < len(clause):
|
|
163
|
+
if clause[i] not in "+-=":
|
|
164
|
+
raise ValueError(f"Invalid operator in mode: {clause[i]}")
|
|
165
|
+
|
|
166
|
+
op = clause[i]
|
|
167
|
+
i += 1
|
|
168
|
+
|
|
169
|
+
# Parse permissions
|
|
170
|
+
perm_bits = 0
|
|
171
|
+
while i < len(clause) and clause[i] in "rwxXst":
|
|
172
|
+
c = clause[i]
|
|
173
|
+
if c == "r":
|
|
174
|
+
perm_bits |= 0o4
|
|
175
|
+
elif c == "w":
|
|
176
|
+
perm_bits |= 0o2
|
|
177
|
+
elif c == "x":
|
|
178
|
+
perm_bits |= 0o1
|
|
179
|
+
elif c == "X":
|
|
180
|
+
# Execute only if directory or already executable
|
|
181
|
+
if current_mode & 0o111:
|
|
182
|
+
perm_bits |= 0o1
|
|
183
|
+
elif c == "s":
|
|
184
|
+
# setuid/setgid - not fully implemented
|
|
185
|
+
pass
|
|
186
|
+
elif c == "t":
|
|
187
|
+
# sticky bit - not fully implemented
|
|
188
|
+
pass
|
|
189
|
+
i += 1
|
|
190
|
+
|
|
191
|
+
# Calculate mask based on who
|
|
192
|
+
mask = 0
|
|
193
|
+
if "u" in who_chars or "a" in who_chars:
|
|
194
|
+
mask |= perm_bits << 6
|
|
195
|
+
if "g" in who_chars or "a" in who_chars:
|
|
196
|
+
mask |= perm_bits << 3
|
|
197
|
+
if "o" in who_chars or "a" in who_chars:
|
|
198
|
+
mask |= perm_bits
|
|
199
|
+
|
|
200
|
+
# Apply operation
|
|
201
|
+
if op == "+":
|
|
202
|
+
current_mode |= mask
|
|
203
|
+
elif op == "-":
|
|
204
|
+
current_mode &= ~mask
|
|
205
|
+
elif op == "=":
|
|
206
|
+
# Clear and set
|
|
207
|
+
clear_mask = 0
|
|
208
|
+
if "u" in who_chars or "a" in who_chars:
|
|
209
|
+
clear_mask |= 0o700
|
|
210
|
+
if "g" in who_chars or "a" in who_chars:
|
|
211
|
+
clear_mask |= 0o070
|
|
212
|
+
if "o" in who_chars or "a" in who_chars:
|
|
213
|
+
clear_mask |= 0o007
|
|
214
|
+
current_mode = (current_mode & ~clear_mask) | mask
|
|
215
|
+
|
|
216
|
+
return current_mode
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Column command implementation."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ColumnCommand:
|
|
7
|
+
"""The column command - columnate lists."""
|
|
8
|
+
|
|
9
|
+
name = "column"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the column command."""
|
|
13
|
+
table_mode = False
|
|
14
|
+
separator = None
|
|
15
|
+
output_separator = " "
|
|
16
|
+
no_merge = False
|
|
17
|
+
column_width = 80
|
|
18
|
+
files: list[str] = []
|
|
19
|
+
|
|
20
|
+
i = 0
|
|
21
|
+
while i < len(args):
|
|
22
|
+
arg = args[i]
|
|
23
|
+
if arg == "--help":
|
|
24
|
+
return ExecResult(
|
|
25
|
+
stdout="Usage: column [OPTION]... [FILE]...\nColumnate lists.\n",
|
|
26
|
+
stderr="",
|
|
27
|
+
exit_code=0,
|
|
28
|
+
)
|
|
29
|
+
elif arg in ("-t", "--table"):
|
|
30
|
+
table_mode = True
|
|
31
|
+
elif arg in ("-n", "--no-merge"):
|
|
32
|
+
no_merge = True
|
|
33
|
+
elif arg == "-s" and i + 1 < len(args):
|
|
34
|
+
i += 1
|
|
35
|
+
separator = args[i]
|
|
36
|
+
elif arg.startswith("-s"):
|
|
37
|
+
separator = arg[2:]
|
|
38
|
+
elif arg == "-o" and i + 1 < len(args):
|
|
39
|
+
i += 1
|
|
40
|
+
output_separator = args[i]
|
|
41
|
+
elif arg.startswith("-o"):
|
|
42
|
+
output_separator = arg[2:]
|
|
43
|
+
elif arg == "-c" and i + 1 < len(args):
|
|
44
|
+
i += 1
|
|
45
|
+
try:
|
|
46
|
+
column_width = int(args[i])
|
|
47
|
+
except ValueError:
|
|
48
|
+
return ExecResult(
|
|
49
|
+
stdout="",
|
|
50
|
+
stderr=f"column: invalid column width: '{args[i]}'\n",
|
|
51
|
+
exit_code=1,
|
|
52
|
+
)
|
|
53
|
+
elif arg.startswith("-c"):
|
|
54
|
+
try:
|
|
55
|
+
column_width = int(arg[2:])
|
|
56
|
+
except ValueError:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"column: invalid column width: '{arg[2:]}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
elif arg == "--":
|
|
63
|
+
files.extend(args[i + 1:])
|
|
64
|
+
break
|
|
65
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
66
|
+
return ExecResult(
|
|
67
|
+
stdout="",
|
|
68
|
+
stderr=f"column: invalid option -- '{arg[1]}'\n",
|
|
69
|
+
exit_code=1,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
files.append(arg)
|
|
73
|
+
i += 1
|
|
74
|
+
|
|
75
|
+
# Read content
|
|
76
|
+
content = ""
|
|
77
|
+
if not files:
|
|
78
|
+
content = ctx.stdin
|
|
79
|
+
else:
|
|
80
|
+
parts = []
|
|
81
|
+
for file in files:
|
|
82
|
+
try:
|
|
83
|
+
if file == "-":
|
|
84
|
+
parts.append(ctx.stdin)
|
|
85
|
+
else:
|
|
86
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
87
|
+
parts.append(await ctx.fs.read_file(path))
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
return ExecResult(
|
|
90
|
+
stdout="",
|
|
91
|
+
stderr=f"column: {file}: No such file or directory\n",
|
|
92
|
+
exit_code=1,
|
|
93
|
+
)
|
|
94
|
+
content = "".join(parts)
|
|
95
|
+
|
|
96
|
+
if not content.strip():
|
|
97
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
98
|
+
|
|
99
|
+
if table_mode:
|
|
100
|
+
result = self._format_table(content, separator, output_separator, no_merge)
|
|
101
|
+
else:
|
|
102
|
+
result = self._format_columns(content, column_width)
|
|
103
|
+
|
|
104
|
+
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
105
|
+
|
|
106
|
+
def _format_table(
|
|
107
|
+
self, content: str, separator: str | None, output_separator: str, no_merge: bool
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Format content as a table."""
|
|
110
|
+
lines = content.rstrip("\n").split("\n")
|
|
111
|
+
rows: list[list[str]] = []
|
|
112
|
+
|
|
113
|
+
for line in lines:
|
|
114
|
+
if separator:
|
|
115
|
+
if no_merge:
|
|
116
|
+
fields = line.split(separator)
|
|
117
|
+
else:
|
|
118
|
+
fields = [f for f in line.split(separator) if f]
|
|
119
|
+
else:
|
|
120
|
+
if no_merge:
|
|
121
|
+
fields = line.split()
|
|
122
|
+
else:
|
|
123
|
+
fields = line.split()
|
|
124
|
+
rows.append(fields)
|
|
125
|
+
|
|
126
|
+
if not rows:
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
# Calculate column widths
|
|
130
|
+
max_cols = max(len(row) for row in rows)
|
|
131
|
+
col_widths = [0] * max_cols
|
|
132
|
+
|
|
133
|
+
for row in rows:
|
|
134
|
+
for i, field in enumerate(row):
|
|
135
|
+
if i < max_cols:
|
|
136
|
+
col_widths[i] = max(col_widths[i], len(field))
|
|
137
|
+
|
|
138
|
+
# Format output
|
|
139
|
+
result_lines = []
|
|
140
|
+
for row in rows:
|
|
141
|
+
formatted = []
|
|
142
|
+
for i, field in enumerate(row):
|
|
143
|
+
if i < len(row) - 1:
|
|
144
|
+
formatted.append(field.ljust(col_widths[i]))
|
|
145
|
+
else:
|
|
146
|
+
formatted.append(field)
|
|
147
|
+
result_lines.append(output_separator.join(formatted))
|
|
148
|
+
|
|
149
|
+
return "\n".join(result_lines) + "\n"
|
|
150
|
+
|
|
151
|
+
def _format_columns(self, content: str, width: int) -> str:
|
|
152
|
+
"""Format content as columns (fill mode)."""
|
|
153
|
+
lines = content.rstrip("\n").split("\n")
|
|
154
|
+
items = []
|
|
155
|
+
for line in lines:
|
|
156
|
+
items.extend(line.split())
|
|
157
|
+
|
|
158
|
+
if not items:
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
# Find maximum item width
|
|
162
|
+
max_item_width = max(len(item) for item in items)
|
|
163
|
+
col_width = max_item_width + 2
|
|
164
|
+
|
|
165
|
+
# Calculate number of columns
|
|
166
|
+
num_cols = max(1, width // col_width)
|
|
167
|
+
|
|
168
|
+
# Format output
|
|
169
|
+
result_lines = []
|
|
170
|
+
for i in range(0, len(items), num_cols):
|
|
171
|
+
row_items = items[i:i + num_cols]
|
|
172
|
+
formatted = []
|
|
173
|
+
for j, item in enumerate(row_items):
|
|
174
|
+
if j < len(row_items) - 1:
|
|
175
|
+
formatted.append(item.ljust(col_width))
|
|
176
|
+
else:
|
|
177
|
+
formatted.append(item)
|
|
178
|
+
result_lines.append("".join(formatted))
|
|
179
|
+
|
|
180
|
+
return "\n".join(result_lines) + "\n"
|