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,63 @@
|
|
|
1
|
+
"""Tee command implementation."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TeeCommand:
|
|
7
|
+
"""The tee command."""
|
|
8
|
+
|
|
9
|
+
name = "tee"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the tee command."""
|
|
13
|
+
append = False
|
|
14
|
+
files: list[str] = []
|
|
15
|
+
|
|
16
|
+
i = 0
|
|
17
|
+
while i < len(args):
|
|
18
|
+
arg = args[i]
|
|
19
|
+
if arg == "--":
|
|
20
|
+
files.extend(args[i + 1:])
|
|
21
|
+
break
|
|
22
|
+
elif arg in ("-a", "--append"):
|
|
23
|
+
append = True
|
|
24
|
+
elif arg == "--help":
|
|
25
|
+
return ExecResult(
|
|
26
|
+
stdout="Usage: tee [OPTION]... [FILE]...\n",
|
|
27
|
+
stderr="",
|
|
28
|
+
exit_code=0,
|
|
29
|
+
)
|
|
30
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
31
|
+
return ExecResult(
|
|
32
|
+
stdout="",
|
|
33
|
+
stderr=f"tee: invalid option -- '{arg[1]}'\n",
|
|
34
|
+
exit_code=1,
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
files.append(arg)
|
|
38
|
+
i += 1
|
|
39
|
+
|
|
40
|
+
# Read stdin
|
|
41
|
+
content = ctx.stdin
|
|
42
|
+
|
|
43
|
+
# Write to files
|
|
44
|
+
stderr = ""
|
|
45
|
+
exit_code = 0
|
|
46
|
+
|
|
47
|
+
for f in files:
|
|
48
|
+
try:
|
|
49
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
50
|
+
if append:
|
|
51
|
+
try:
|
|
52
|
+
existing = await ctx.fs.read_file(path)
|
|
53
|
+
await ctx.fs.write_file(path, existing + content)
|
|
54
|
+
except FileNotFoundError:
|
|
55
|
+
await ctx.fs.write_file(path, content)
|
|
56
|
+
else:
|
|
57
|
+
await ctx.fs.write_file(path, content)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
stderr += f"tee: {f}: {e}\n"
|
|
60
|
+
exit_code = 1
|
|
61
|
+
|
|
62
|
+
# Output to stdout
|
|
63
|
+
return ExecResult(stdout=content, stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Timeout command implementation."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import re
|
|
5
|
+
from ...types import CommandContext, ExecResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TimeoutCommand:
|
|
9
|
+
"""The timeout command."""
|
|
10
|
+
|
|
11
|
+
name = "timeout"
|
|
12
|
+
|
|
13
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
14
|
+
"""Execute the timeout command."""
|
|
15
|
+
duration = None
|
|
16
|
+
kill_after = None
|
|
17
|
+
signal = "TERM"
|
|
18
|
+
preserve_status = False
|
|
19
|
+
command: list[str] = []
|
|
20
|
+
|
|
21
|
+
i = 0
|
|
22
|
+
while i < len(args):
|
|
23
|
+
arg = args[i]
|
|
24
|
+
if arg == "-k" and i + 1 < len(args):
|
|
25
|
+
i += 1
|
|
26
|
+
try:
|
|
27
|
+
kill_after = self._parse_duration(args[i])
|
|
28
|
+
except ValueError:
|
|
29
|
+
return ExecResult(
|
|
30
|
+
stdout="",
|
|
31
|
+
stderr=f"timeout: invalid time interval '{args[i]}'\n",
|
|
32
|
+
exit_code=1,
|
|
33
|
+
)
|
|
34
|
+
elif arg.startswith("-k"):
|
|
35
|
+
try:
|
|
36
|
+
kill_after = self._parse_duration(arg[2:])
|
|
37
|
+
except ValueError:
|
|
38
|
+
return ExecResult(
|
|
39
|
+
stdout="",
|
|
40
|
+
stderr=f"timeout: invalid time interval '{arg[2:]}'\n",
|
|
41
|
+
exit_code=1,
|
|
42
|
+
)
|
|
43
|
+
elif arg.startswith("--kill-after="):
|
|
44
|
+
try:
|
|
45
|
+
kill_after = self._parse_duration(arg[13:])
|
|
46
|
+
except ValueError:
|
|
47
|
+
return ExecResult(
|
|
48
|
+
stdout="",
|
|
49
|
+
stderr=f"timeout: invalid time interval '{arg[13:]}'\n",
|
|
50
|
+
exit_code=1,
|
|
51
|
+
)
|
|
52
|
+
elif arg == "--kill-after" and i + 1 < len(args):
|
|
53
|
+
i += 1
|
|
54
|
+
try:
|
|
55
|
+
kill_after = self._parse_duration(args[i])
|
|
56
|
+
except ValueError:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"timeout: invalid time interval '{args[i]}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
elif arg == "-s" and i + 1 < len(args):
|
|
63
|
+
i += 1
|
|
64
|
+
signal = args[i]
|
|
65
|
+
elif arg.startswith("-s"):
|
|
66
|
+
signal = arg[2:]
|
|
67
|
+
elif arg.startswith("--signal="):
|
|
68
|
+
signal = arg[9:]
|
|
69
|
+
elif arg == "--signal" and i + 1 < len(args):
|
|
70
|
+
i += 1
|
|
71
|
+
signal = args[i]
|
|
72
|
+
elif arg == "--preserve-status":
|
|
73
|
+
preserve_status = True
|
|
74
|
+
elif arg == "--foreground":
|
|
75
|
+
pass # Ignore
|
|
76
|
+
elif arg == "--help":
|
|
77
|
+
return ExecResult(
|
|
78
|
+
stdout=(
|
|
79
|
+
"Usage: timeout [OPTION] DURATION COMMAND [ARG]...\n"
|
|
80
|
+
"Start COMMAND, and kill it if still running after DURATION.\n\n"
|
|
81
|
+
"Options:\n"
|
|
82
|
+
" -k, --kill-after=DURATION send KILL signal after DURATION\n"
|
|
83
|
+
" -s, --signal=SIGNAL send this signal on timeout (default: TERM)\n"
|
|
84
|
+
" --preserve-status exit with the same status as COMMAND\n"
|
|
85
|
+
" --foreground run command in foreground\n"
|
|
86
|
+
" --help display this help and exit\n"
|
|
87
|
+
),
|
|
88
|
+
stderr="",
|
|
89
|
+
exit_code=0,
|
|
90
|
+
)
|
|
91
|
+
elif arg == "--":
|
|
92
|
+
# Rest are command args
|
|
93
|
+
if duration is None and i + 1 < len(args):
|
|
94
|
+
i += 1
|
|
95
|
+
try:
|
|
96
|
+
duration = self._parse_duration(args[i])
|
|
97
|
+
except ValueError:
|
|
98
|
+
return ExecResult(
|
|
99
|
+
stdout="",
|
|
100
|
+
stderr=f"timeout: invalid duration '{args[i]}'\n",
|
|
101
|
+
exit_code=1,
|
|
102
|
+
)
|
|
103
|
+
if i + 1 < len(args):
|
|
104
|
+
command = args[i + 1:]
|
|
105
|
+
break
|
|
106
|
+
elif arg.startswith("-") and len(arg) > 1 and not arg[1].isdigit():
|
|
107
|
+
return ExecResult(
|
|
108
|
+
stdout="",
|
|
109
|
+
stderr=f"timeout: invalid option -- '{arg[1]}'\n",
|
|
110
|
+
exit_code=1,
|
|
111
|
+
)
|
|
112
|
+
elif duration is None:
|
|
113
|
+
try:
|
|
114
|
+
duration = self._parse_duration(arg)
|
|
115
|
+
except ValueError:
|
|
116
|
+
return ExecResult(
|
|
117
|
+
stdout="",
|
|
118
|
+
stderr=f"timeout: invalid duration '{arg}'\n",
|
|
119
|
+
exit_code=1,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
command = args[i:]
|
|
123
|
+
break
|
|
124
|
+
i += 1
|
|
125
|
+
|
|
126
|
+
if duration is None:
|
|
127
|
+
return ExecResult(
|
|
128
|
+
stdout="",
|
|
129
|
+
stderr="timeout: missing operand\n",
|
|
130
|
+
exit_code=1,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if not command:
|
|
134
|
+
return ExecResult(
|
|
135
|
+
stdout="",
|
|
136
|
+
stderr="timeout: missing command\n",
|
|
137
|
+
exit_code=1,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Quote arguments properly for shell execution
|
|
141
|
+
def quote(s: str) -> str:
|
|
142
|
+
if not s or any(c in s for c in " \t\n'\"\\$`!"):
|
|
143
|
+
return "'" + s.replace("'", "'\"'\"'") + "'"
|
|
144
|
+
return s
|
|
145
|
+
|
|
146
|
+
cmd_str = " ".join(quote(c) for c in command)
|
|
147
|
+
|
|
148
|
+
# Execute command with timeout
|
|
149
|
+
if not ctx.exec:
|
|
150
|
+
return ExecResult(
|
|
151
|
+
stdout="",
|
|
152
|
+
stderr="timeout: cannot execute commands\n",
|
|
153
|
+
exit_code=126,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = await asyncio.wait_for(
|
|
158
|
+
ctx.exec(cmd_str, {"cwd": ctx.cwd}),
|
|
159
|
+
timeout=duration,
|
|
160
|
+
)
|
|
161
|
+
return result
|
|
162
|
+
except asyncio.TimeoutError:
|
|
163
|
+
# In a sandboxed environment, we can't actually send signals
|
|
164
|
+
# The -k and -s options are parsed but have limited effect
|
|
165
|
+
# Return 124 unless preserve_status is set (then we can't know the status)
|
|
166
|
+
if preserve_status:
|
|
167
|
+
# In real timeout, this would preserve the signal exit status
|
|
168
|
+
# We return 124 + signal number approximation
|
|
169
|
+
return ExecResult(stdout="", stderr="", exit_code=124)
|
|
170
|
+
return ExecResult(stdout="", stderr="", exit_code=124)
|
|
171
|
+
|
|
172
|
+
def _parse_duration(self, s: str) -> float:
|
|
173
|
+
"""Parse a duration string."""
|
|
174
|
+
match = re.match(r"^(\d+(?:\.\d+)?)(s|m|h|d)?$", s)
|
|
175
|
+
if not match:
|
|
176
|
+
raise ValueError(f"Invalid duration: {s}")
|
|
177
|
+
|
|
178
|
+
value = float(match.group(1))
|
|
179
|
+
suffix = match.group(2)
|
|
180
|
+
|
|
181
|
+
if suffix == "m":
|
|
182
|
+
value *= 60
|
|
183
|
+
elif suffix == "h":
|
|
184
|
+
value *= 3600
|
|
185
|
+
elif suffix == "d":
|
|
186
|
+
value *= 86400
|
|
187
|
+
|
|
188
|
+
return value
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Touch command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: touch [OPTION]... FILE...
|
|
4
|
+
|
|
5
|
+
Update the access and modification times of each FILE to the current time.
|
|
6
|
+
A FILE argument that does not exist is created empty.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-c, --no-create do not create any files
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ...types import CommandContext, ExecResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TouchCommand:
|
|
16
|
+
"""The touch command."""
|
|
17
|
+
|
|
18
|
+
name = "touch"
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
|
+
"""Execute the touch command."""
|
|
22
|
+
no_create = False
|
|
23
|
+
files: list[str] = []
|
|
24
|
+
|
|
25
|
+
# Parse arguments
|
|
26
|
+
i = 0
|
|
27
|
+
while i < len(args):
|
|
28
|
+
arg = args[i]
|
|
29
|
+
if arg == "--":
|
|
30
|
+
files.extend(args[i + 1:])
|
|
31
|
+
break
|
|
32
|
+
elif arg.startswith("--"):
|
|
33
|
+
if arg == "--no-create":
|
|
34
|
+
no_create = True
|
|
35
|
+
else:
|
|
36
|
+
return ExecResult(
|
|
37
|
+
stdout="",
|
|
38
|
+
stderr=f"touch: unrecognized option '{arg}'\n",
|
|
39
|
+
exit_code=1,
|
|
40
|
+
)
|
|
41
|
+
elif arg.startswith("-") and arg != "-":
|
|
42
|
+
for c in arg[1:]:
|
|
43
|
+
if c == "c":
|
|
44
|
+
no_create = True
|
|
45
|
+
else:
|
|
46
|
+
return ExecResult(
|
|
47
|
+
stdout="",
|
|
48
|
+
stderr=f"touch: invalid option -- '{c}'\n",
|
|
49
|
+
exit_code=1,
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
files.append(arg)
|
|
53
|
+
i += 1
|
|
54
|
+
|
|
55
|
+
if not files:
|
|
56
|
+
return ExecResult(
|
|
57
|
+
stdout="",
|
|
58
|
+
stderr="touch: missing file operand\n",
|
|
59
|
+
exit_code=1,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
stderr = ""
|
|
63
|
+
exit_code = 0
|
|
64
|
+
|
|
65
|
+
for f in files:
|
|
66
|
+
try:
|
|
67
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
68
|
+
# Check if file exists
|
|
69
|
+
try:
|
|
70
|
+
stat = await ctx.fs.stat(path)
|
|
71
|
+
if stat.is_directory:
|
|
72
|
+
# Touching a directory - we can't easily update dir mtime
|
|
73
|
+
# in current implementation, so just continue
|
|
74
|
+
continue
|
|
75
|
+
# File exists - read and re-write to update timestamp
|
|
76
|
+
content = await ctx.fs.read_file(path)
|
|
77
|
+
await ctx.fs.write_file(path, content)
|
|
78
|
+
except FileNotFoundError:
|
|
79
|
+
# File doesn't exist
|
|
80
|
+
if no_create:
|
|
81
|
+
continue
|
|
82
|
+
# Create empty file
|
|
83
|
+
await ctx.fs.write_file(path, "")
|
|
84
|
+
except FileNotFoundError:
|
|
85
|
+
stderr += f"touch: cannot touch '{f}': No such file or directory\n"
|
|
86
|
+
exit_code = 1
|
|
87
|
+
except IsADirectoryError:
|
|
88
|
+
# Touching a directory is fine, just update timestamp (no-op)
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""Tr command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: tr [OPTION]... SET1 [SET2]
|
|
4
|
+
|
|
5
|
+
Translate, squeeze, and/or delete characters from standard input,
|
|
6
|
+
writing to standard output.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-c, -C, --complement use the complement of SET1
|
|
10
|
+
-d, --delete delete characters in SET1, do not translate
|
|
11
|
+
-s, --squeeze-repeats replace each sequence of a repeated character
|
|
12
|
+
that is listed in SET1 with a single occurrence
|
|
13
|
+
|
|
14
|
+
SETs are specified as strings of characters. Interpreted sequences include:
|
|
15
|
+
\\NNN character with octal value NNN (1 to 3 octal digits)
|
|
16
|
+
\\\\ backslash
|
|
17
|
+
\\a audible BEL
|
|
18
|
+
\\b backspace
|
|
19
|
+
\\f form feed
|
|
20
|
+
\\n newline
|
|
21
|
+
\\r carriage return
|
|
22
|
+
\\t horizontal tab
|
|
23
|
+
\\v vertical tab
|
|
24
|
+
CHAR1-CHAR2 all characters from CHAR1 to CHAR2 in ascending order
|
|
25
|
+
[:alnum:] all letters and digits
|
|
26
|
+
[:alpha:] all letters
|
|
27
|
+
[:blank:] all horizontal whitespace
|
|
28
|
+
[:cntrl:] all control characters
|
|
29
|
+
[:digit:] all digits
|
|
30
|
+
[:lower:] all lowercase letters
|
|
31
|
+
[:print:] all printable characters
|
|
32
|
+
[:punct:] all punctuation characters
|
|
33
|
+
[:space:] all horizontal or vertical whitespace
|
|
34
|
+
[:upper:] all uppercase letters
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import string
|
|
38
|
+
from ...types import CommandContext, ExecResult
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TrCommand:
|
|
42
|
+
"""The tr command."""
|
|
43
|
+
|
|
44
|
+
name = "tr"
|
|
45
|
+
|
|
46
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
47
|
+
"""Execute the tr command."""
|
|
48
|
+
complement = False
|
|
49
|
+
delete = False
|
|
50
|
+
squeeze = False
|
|
51
|
+
sets: list[str] = []
|
|
52
|
+
|
|
53
|
+
# Parse arguments
|
|
54
|
+
i = 0
|
|
55
|
+
while i < len(args):
|
|
56
|
+
arg = args[i]
|
|
57
|
+
if arg == "--":
|
|
58
|
+
sets.extend(args[i + 1:])
|
|
59
|
+
break
|
|
60
|
+
elif arg.startswith("--"):
|
|
61
|
+
if arg == "--complement":
|
|
62
|
+
complement = True
|
|
63
|
+
elif arg == "--delete":
|
|
64
|
+
delete = True
|
|
65
|
+
elif arg == "--squeeze-repeats":
|
|
66
|
+
squeeze = True
|
|
67
|
+
else:
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="",
|
|
70
|
+
stderr=f"tr: unrecognized option '{arg}'\n",
|
|
71
|
+
exit_code=1,
|
|
72
|
+
)
|
|
73
|
+
elif arg.startswith("-") and arg != "-":
|
|
74
|
+
j = 1
|
|
75
|
+
while j < len(arg):
|
|
76
|
+
c = arg[j]
|
|
77
|
+
if c == "c" or c == "C":
|
|
78
|
+
complement = True
|
|
79
|
+
elif c == "d":
|
|
80
|
+
delete = True
|
|
81
|
+
elif c == "s":
|
|
82
|
+
squeeze = True
|
|
83
|
+
else:
|
|
84
|
+
return ExecResult(
|
|
85
|
+
stdout="",
|
|
86
|
+
stderr=f"tr: invalid option -- '{c}'\n",
|
|
87
|
+
exit_code=1,
|
|
88
|
+
)
|
|
89
|
+
j += 1
|
|
90
|
+
else:
|
|
91
|
+
sets.append(arg)
|
|
92
|
+
i += 1
|
|
93
|
+
|
|
94
|
+
# Validate arguments
|
|
95
|
+
if delete:
|
|
96
|
+
if len(sets) < 1:
|
|
97
|
+
return ExecResult(
|
|
98
|
+
stdout="",
|
|
99
|
+
stderr="tr: missing operand\n",
|
|
100
|
+
exit_code=1,
|
|
101
|
+
)
|
|
102
|
+
if len(sets) > 1 and not squeeze:
|
|
103
|
+
return ExecResult(
|
|
104
|
+
stdout="",
|
|
105
|
+
stderr="tr: extra operand\n",
|
|
106
|
+
exit_code=1,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
if len(sets) < 2 and not squeeze:
|
|
110
|
+
return ExecResult(
|
|
111
|
+
stdout="",
|
|
112
|
+
stderr="tr: missing operand after SET1\n",
|
|
113
|
+
exit_code=1,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Expand sets
|
|
117
|
+
try:
|
|
118
|
+
set1 = self._expand_set(sets[0]) if sets else ""
|
|
119
|
+
set2 = self._expand_set(sets[1]) if len(sets) > 1 else ""
|
|
120
|
+
except ValueError as e:
|
|
121
|
+
return ExecResult(
|
|
122
|
+
stdout="",
|
|
123
|
+
stderr=f"tr: {e}\n",
|
|
124
|
+
exit_code=1,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Apply complement
|
|
128
|
+
if complement:
|
|
129
|
+
# Create set of all characters not in set1
|
|
130
|
+
all_chars = set(chr(i) for i in range(256))
|
|
131
|
+
set1_chars = set(set1)
|
|
132
|
+
set1 = "".join(sorted(all_chars - set1_chars, key=ord))
|
|
133
|
+
|
|
134
|
+
# Process input
|
|
135
|
+
content = ctx.stdin
|
|
136
|
+
result = []
|
|
137
|
+
|
|
138
|
+
if delete and not squeeze:
|
|
139
|
+
# Delete characters in set1
|
|
140
|
+
delete_set = set(set1)
|
|
141
|
+
for c in content:
|
|
142
|
+
if c not in delete_set:
|
|
143
|
+
result.append(c)
|
|
144
|
+
elif delete and squeeze:
|
|
145
|
+
# Delete set1, squeeze set2
|
|
146
|
+
delete_set = set(set1)
|
|
147
|
+
squeeze_set = set(set2)
|
|
148
|
+
prev_char = None
|
|
149
|
+
for c in content:
|
|
150
|
+
if c in delete_set:
|
|
151
|
+
continue
|
|
152
|
+
if c in squeeze_set and c == prev_char:
|
|
153
|
+
continue
|
|
154
|
+
result.append(c)
|
|
155
|
+
prev_char = c
|
|
156
|
+
elif squeeze and not set2:
|
|
157
|
+
# Squeeze only
|
|
158
|
+
squeeze_set = set(set1)
|
|
159
|
+
prev_char = None
|
|
160
|
+
for c in content:
|
|
161
|
+
if c in squeeze_set and c == prev_char:
|
|
162
|
+
continue
|
|
163
|
+
result.append(c)
|
|
164
|
+
prev_char = c
|
|
165
|
+
else:
|
|
166
|
+
# Translate
|
|
167
|
+
trans_map = self._create_translation_map(set1, set2)
|
|
168
|
+
|
|
169
|
+
if squeeze:
|
|
170
|
+
squeeze_set = set(set2)
|
|
171
|
+
prev_char = None
|
|
172
|
+
for c in content:
|
|
173
|
+
translated = trans_map.get(c, c)
|
|
174
|
+
if translated in squeeze_set and translated == prev_char:
|
|
175
|
+
continue
|
|
176
|
+
result.append(translated)
|
|
177
|
+
prev_char = translated
|
|
178
|
+
else:
|
|
179
|
+
for c in content:
|
|
180
|
+
result.append(trans_map.get(c, c))
|
|
181
|
+
|
|
182
|
+
return ExecResult(stdout="".join(result), stderr="", exit_code=0)
|
|
183
|
+
|
|
184
|
+
def _expand_set(self, s: str) -> str:
|
|
185
|
+
"""Expand a character set specification."""
|
|
186
|
+
result = []
|
|
187
|
+
i = 0
|
|
188
|
+
|
|
189
|
+
while i < len(s):
|
|
190
|
+
# Check for character classes like [:digit:]
|
|
191
|
+
if s[i:].startswith("[:") and ":]" in s[i + 2:]:
|
|
192
|
+
end = s.index(":]", i + 2)
|
|
193
|
+
class_name = s[i + 2:end]
|
|
194
|
+
result.extend(self._get_char_class(class_name))
|
|
195
|
+
i = end + 2
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Check for escape sequences
|
|
199
|
+
if s[i] == "\\" and i + 1 < len(s):
|
|
200
|
+
c = s[i + 1]
|
|
201
|
+
if c == "a":
|
|
202
|
+
result.append("\a")
|
|
203
|
+
elif c == "b":
|
|
204
|
+
result.append("\b")
|
|
205
|
+
elif c == "f":
|
|
206
|
+
result.append("\f")
|
|
207
|
+
elif c == "n":
|
|
208
|
+
result.append("\n")
|
|
209
|
+
elif c == "r":
|
|
210
|
+
result.append("\r")
|
|
211
|
+
elif c == "t":
|
|
212
|
+
result.append("\t")
|
|
213
|
+
elif c == "v":
|
|
214
|
+
result.append("\v")
|
|
215
|
+
elif c == "\\":
|
|
216
|
+
result.append("\\")
|
|
217
|
+
elif c.isdigit():
|
|
218
|
+
# Octal escape
|
|
219
|
+
octal = ""
|
|
220
|
+
j = i + 1
|
|
221
|
+
while j < len(s) and s[j].isdigit() and len(octal) < 3:
|
|
222
|
+
if s[j] in "01234567":
|
|
223
|
+
octal += s[j]
|
|
224
|
+
j += 1
|
|
225
|
+
else:
|
|
226
|
+
break
|
|
227
|
+
if octal:
|
|
228
|
+
result.append(chr(int(octal, 8)))
|
|
229
|
+
i = j
|
|
230
|
+
continue
|
|
231
|
+
else:
|
|
232
|
+
result.append(c)
|
|
233
|
+
else:
|
|
234
|
+
result.append(c)
|
|
235
|
+
i += 2
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Check for range
|
|
239
|
+
if i + 2 < len(s) and s[i + 1] == "-":
|
|
240
|
+
start = s[i]
|
|
241
|
+
end = s[i + 2]
|
|
242
|
+
if ord(start) <= ord(end):
|
|
243
|
+
for c in range(ord(start), ord(end) + 1):
|
|
244
|
+
result.append(chr(c))
|
|
245
|
+
i += 3
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
result.append(s[i])
|
|
249
|
+
i += 1
|
|
250
|
+
|
|
251
|
+
return "".join(result)
|
|
252
|
+
|
|
253
|
+
def _get_char_class(self, name: str) -> list[str]:
|
|
254
|
+
"""Get characters in a character class."""
|
|
255
|
+
if name == "alnum":
|
|
256
|
+
return list(string.ascii_letters + string.digits)
|
|
257
|
+
elif name == "alpha":
|
|
258
|
+
return list(string.ascii_letters)
|
|
259
|
+
elif name == "blank":
|
|
260
|
+
return [" ", "\t"]
|
|
261
|
+
elif name == "cntrl":
|
|
262
|
+
return [chr(i) for i in range(32)] + [chr(127)]
|
|
263
|
+
elif name == "digit":
|
|
264
|
+
return list(string.digits)
|
|
265
|
+
elif name == "graph":
|
|
266
|
+
return [chr(i) for i in range(33, 127)]
|
|
267
|
+
elif name == "lower":
|
|
268
|
+
return list(string.ascii_lowercase)
|
|
269
|
+
elif name == "print":
|
|
270
|
+
return [chr(i) for i in range(32, 127)]
|
|
271
|
+
elif name == "punct":
|
|
272
|
+
return list(string.punctuation)
|
|
273
|
+
elif name == "space":
|
|
274
|
+
return list(string.whitespace)
|
|
275
|
+
elif name == "upper":
|
|
276
|
+
return list(string.ascii_uppercase)
|
|
277
|
+
elif name == "xdigit":
|
|
278
|
+
return list(string.hexdigits)
|
|
279
|
+
else:
|
|
280
|
+
raise ValueError(f"invalid character class '{name}'")
|
|
281
|
+
|
|
282
|
+
def _create_translation_map(self, set1: str, set2: str) -> dict[str, str]:
|
|
283
|
+
"""Create a translation map from set1 to set2."""
|
|
284
|
+
trans_map = {}
|
|
285
|
+
|
|
286
|
+
# If set2 is shorter, extend with its last character
|
|
287
|
+
if set2:
|
|
288
|
+
last_char = set2[-1]
|
|
289
|
+
set2_extended = set2 + last_char * (len(set1) - len(set2))
|
|
290
|
+
else:
|
|
291
|
+
set2_extended = ""
|
|
292
|
+
|
|
293
|
+
for i, c in enumerate(set1):
|
|
294
|
+
if i < len(set2_extended):
|
|
295
|
+
trans_map[c] = set2_extended[i]
|
|
296
|
+
|
|
297
|
+
return trans_map
|