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,138 @@
|
|
|
1
|
+
"""Base64 command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: base64 [OPTION]... [FILE]
|
|
4
|
+
|
|
5
|
+
Base64 encode or decode FILE, or standard input.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-d, --decode decode data
|
|
9
|
+
-w, --wrap=COLS wrap encoded lines after COLS characters (default 76)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import base64 as b64
|
|
13
|
+
from ...types import CommandContext, ExecResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Base64Command:
|
|
17
|
+
"""The base64 command."""
|
|
18
|
+
|
|
19
|
+
name = "base64"
|
|
20
|
+
|
|
21
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
22
|
+
"""Execute the base64 command."""
|
|
23
|
+
decode = False
|
|
24
|
+
wrap_cols = 76
|
|
25
|
+
files: list[str] = []
|
|
26
|
+
|
|
27
|
+
# Parse arguments
|
|
28
|
+
i = 0
|
|
29
|
+
while i < len(args):
|
|
30
|
+
arg = args[i]
|
|
31
|
+
if arg == "--":
|
|
32
|
+
files.extend(args[i + 1:])
|
|
33
|
+
break
|
|
34
|
+
elif arg.startswith("--"):
|
|
35
|
+
if arg == "--decode":
|
|
36
|
+
decode = True
|
|
37
|
+
elif arg.startswith("--wrap="):
|
|
38
|
+
try:
|
|
39
|
+
wrap_cols = int(arg[7:])
|
|
40
|
+
except ValueError:
|
|
41
|
+
return ExecResult(
|
|
42
|
+
stdout="",
|
|
43
|
+
stderr=f"base64: invalid wrap size: '{arg[7:]}'\n",
|
|
44
|
+
exit_code=1,
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
return ExecResult(
|
|
48
|
+
stdout="",
|
|
49
|
+
stderr=f"base64: unrecognized option '{arg}'\n",
|
|
50
|
+
exit_code=1,
|
|
51
|
+
)
|
|
52
|
+
elif arg.startswith("-") and arg != "-":
|
|
53
|
+
j = 1
|
|
54
|
+
while j < len(arg):
|
|
55
|
+
c = arg[j]
|
|
56
|
+
if c == "d":
|
|
57
|
+
decode = True
|
|
58
|
+
elif c == "w":
|
|
59
|
+
# -w requires a value
|
|
60
|
+
if j + 1 < len(arg):
|
|
61
|
+
try:
|
|
62
|
+
wrap_cols = int(arg[j + 1:])
|
|
63
|
+
except ValueError:
|
|
64
|
+
return ExecResult(
|
|
65
|
+
stdout="",
|
|
66
|
+
stderr=f"base64: invalid wrap size\n",
|
|
67
|
+
exit_code=1,
|
|
68
|
+
)
|
|
69
|
+
break
|
|
70
|
+
elif i + 1 < len(args):
|
|
71
|
+
i += 1
|
|
72
|
+
try:
|
|
73
|
+
wrap_cols = int(args[i])
|
|
74
|
+
except ValueError:
|
|
75
|
+
return ExecResult(
|
|
76
|
+
stdout="",
|
|
77
|
+
stderr=f"base64: invalid wrap size: '{args[i]}'\n",
|
|
78
|
+
exit_code=1,
|
|
79
|
+
)
|
|
80
|
+
break
|
|
81
|
+
else:
|
|
82
|
+
return ExecResult(
|
|
83
|
+
stdout="",
|
|
84
|
+
stderr="base64: option requires an argument -- 'w'\n",
|
|
85
|
+
exit_code=1,
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
return ExecResult(
|
|
89
|
+
stdout="",
|
|
90
|
+
stderr=f"base64: invalid option -- '{c}'\n",
|
|
91
|
+
exit_code=1,
|
|
92
|
+
)
|
|
93
|
+
j += 1
|
|
94
|
+
else:
|
|
95
|
+
files.append(arg)
|
|
96
|
+
i += 1
|
|
97
|
+
|
|
98
|
+
# Default to stdin
|
|
99
|
+
if not files:
|
|
100
|
+
files = ["-"]
|
|
101
|
+
|
|
102
|
+
stdout = ""
|
|
103
|
+
stderr = ""
|
|
104
|
+
exit_code = 0
|
|
105
|
+
|
|
106
|
+
for f in files:
|
|
107
|
+
try:
|
|
108
|
+
if f == "-":
|
|
109
|
+
content = ctx.stdin
|
|
110
|
+
else:
|
|
111
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
112
|
+
content = await ctx.fs.read_file(path)
|
|
113
|
+
|
|
114
|
+
if decode:
|
|
115
|
+
# Decode base64
|
|
116
|
+
# Strip whitespace
|
|
117
|
+
content = "".join(content.split())
|
|
118
|
+
try:
|
|
119
|
+
decoded = b64.b64decode(content)
|
|
120
|
+
stdout += decoded.decode("utf-8", errors="replace")
|
|
121
|
+
except Exception as e:
|
|
122
|
+
stderr += f"base64: invalid input\n"
|
|
123
|
+
exit_code = 1
|
|
124
|
+
else:
|
|
125
|
+
# Encode to base64
|
|
126
|
+
encoded = b64.b64encode(content.encode("utf-8")).decode("utf-8")
|
|
127
|
+
# Wrap lines
|
|
128
|
+
if wrap_cols > 0:
|
|
129
|
+
lines = [encoded[i:i + wrap_cols] for i in range(0, len(encoded), wrap_cols)]
|
|
130
|
+
stdout += "\n".join(lines) + "\n"
|
|
131
|
+
else:
|
|
132
|
+
stdout += encoded
|
|
133
|
+
|
|
134
|
+
except FileNotFoundError:
|
|
135
|
+
stderr += f"base64: {f}: No such file or directory\n"
|
|
136
|
+
exit_code = 1
|
|
137
|
+
|
|
138
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Basename command implementation."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from ...types import CommandContext, ExecResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BasenameCommand:
|
|
8
|
+
"""The basename command."""
|
|
9
|
+
|
|
10
|
+
name = "basename"
|
|
11
|
+
|
|
12
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
13
|
+
"""Execute the basename command."""
|
|
14
|
+
suffix = None
|
|
15
|
+
multiple = False
|
|
16
|
+
paths: list[str] = []
|
|
17
|
+
|
|
18
|
+
i = 0
|
|
19
|
+
while i < len(args):
|
|
20
|
+
arg = args[i]
|
|
21
|
+
if arg == "-s" and i + 1 < len(args):
|
|
22
|
+
i += 1
|
|
23
|
+
suffix = args[i]
|
|
24
|
+
elif arg.startswith("--suffix="):
|
|
25
|
+
suffix = arg[9:]
|
|
26
|
+
elif arg == "-a" or arg == "--multiple":
|
|
27
|
+
multiple = True
|
|
28
|
+
elif arg == "--help":
|
|
29
|
+
return ExecResult(
|
|
30
|
+
stdout="Usage: basename NAME [SUFFIX]\n basename OPTION... NAME...\n",
|
|
31
|
+
stderr="",
|
|
32
|
+
exit_code=0,
|
|
33
|
+
)
|
|
34
|
+
elif arg == "--":
|
|
35
|
+
paths.extend(args[i + 1:])
|
|
36
|
+
break
|
|
37
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
38
|
+
return ExecResult(
|
|
39
|
+
stdout="",
|
|
40
|
+
stderr=f"basename: invalid option -- '{arg[1]}'\n",
|
|
41
|
+
exit_code=1,
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
paths.append(arg)
|
|
45
|
+
i += 1
|
|
46
|
+
|
|
47
|
+
if not paths:
|
|
48
|
+
return ExecResult(
|
|
49
|
+
stdout="",
|
|
50
|
+
stderr="basename: missing operand\n",
|
|
51
|
+
exit_code=1,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# If not multiple mode and exactly 2 args, second is suffix
|
|
55
|
+
if not multiple and not suffix and len(paths) == 2:
|
|
56
|
+
suffix = paths[1]
|
|
57
|
+
paths = [paths[0]]
|
|
58
|
+
|
|
59
|
+
results = []
|
|
60
|
+
for path in paths:
|
|
61
|
+
base = os.path.basename(path.rstrip("/"))
|
|
62
|
+
if not base:
|
|
63
|
+
base = "/"
|
|
64
|
+
if suffix and base.endswith(suffix):
|
|
65
|
+
base = base[:-len(suffix)]
|
|
66
|
+
results.append(base)
|
|
67
|
+
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="\n".join(results) + "\n",
|
|
70
|
+
stderr="",
|
|
71
|
+
exit_code=0,
|
|
72
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Bash and sh command implementations."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BashCommand:
|
|
7
|
+
"""The bash command - execute shell commands or scripts."""
|
|
8
|
+
|
|
9
|
+
name = "bash"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the bash command."""
|
|
13
|
+
if "--help" in args:
|
|
14
|
+
return ExecResult(
|
|
15
|
+
stdout=(
|
|
16
|
+
"Usage: bash [OPTIONS] [SCRIPT_FILE] [ARGUMENTS...]\n"
|
|
17
|
+
"Execute shell commands or scripts.\n\n"
|
|
18
|
+
"Options:\n"
|
|
19
|
+
" -c COMMAND execute COMMAND string\n"
|
|
20
|
+
" --help display this help and exit\n\n"
|
|
21
|
+
"Without -c, reads and executes commands from SCRIPT_FILE.\n"
|
|
22
|
+
),
|
|
23
|
+
stderr="",
|
|
24
|
+
exit_code=0,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Handle -c flag
|
|
28
|
+
# With -c: bash -c 'command' arg0 arg1 arg2
|
|
29
|
+
# arg0 becomes $0, arg1 becomes $1, arg2 becomes $2
|
|
30
|
+
if len(args) >= 2 and args[0] == "-c":
|
|
31
|
+
command = args[1]
|
|
32
|
+
script_name = args[2] if len(args) > 2 else "bash"
|
|
33
|
+
script_args = args[3:] if len(args) > 3 else []
|
|
34
|
+
return await self._execute_script(command, script_name, script_args, ctx)
|
|
35
|
+
|
|
36
|
+
# No arguments - in real bash this would be interactive mode
|
|
37
|
+
# In our implementation, we just return success
|
|
38
|
+
if not args:
|
|
39
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
40
|
+
|
|
41
|
+
# Read and execute script file
|
|
42
|
+
script_path = args[0]
|
|
43
|
+
script_args = args[1:]
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
full_path = ctx.fs.resolve_path(ctx.cwd, script_path)
|
|
47
|
+
script_content = await ctx.fs.read_file(full_path)
|
|
48
|
+
return await self._execute_script(script_content, script_path, script_args, ctx)
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
return ExecResult(
|
|
51
|
+
stdout="",
|
|
52
|
+
stderr=f"bash: {script_path}: No such file or directory\n",
|
|
53
|
+
exit_code=127,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def _execute_script(
|
|
57
|
+
self,
|
|
58
|
+
script: str,
|
|
59
|
+
script_name: str,
|
|
60
|
+
script_args: list[str],
|
|
61
|
+
ctx: CommandContext,
|
|
62
|
+
) -> ExecResult:
|
|
63
|
+
"""Execute a script with positional parameters."""
|
|
64
|
+
if not ctx.exec:
|
|
65
|
+
return ExecResult(
|
|
66
|
+
stdout="",
|
|
67
|
+
stderr="bash: internal error: exec function not available\n",
|
|
68
|
+
exit_code=1,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Build positional parameters for the exec env option
|
|
72
|
+
positional_env: dict[str, str] = {
|
|
73
|
+
"0": script_name,
|
|
74
|
+
"#": str(len(script_args)),
|
|
75
|
+
"@": " ".join(script_args),
|
|
76
|
+
"*": " ".join(script_args),
|
|
77
|
+
}
|
|
78
|
+
for i, arg in enumerate(script_args):
|
|
79
|
+
positional_env[str(i + 1)] = arg
|
|
80
|
+
|
|
81
|
+
# Skip shebang line if present
|
|
82
|
+
script_to_run = script
|
|
83
|
+
if script_to_run.startswith("#!"):
|
|
84
|
+
first_newline = script_to_run.find("\n")
|
|
85
|
+
if first_newline != -1:
|
|
86
|
+
script_to_run = script_to_run[first_newline + 1:]
|
|
87
|
+
|
|
88
|
+
# Process the script line by line, filtering out comments and empty lines
|
|
89
|
+
lines = script_to_run.split("\n")
|
|
90
|
+
commands: list[str] = []
|
|
91
|
+
for line in lines:
|
|
92
|
+
trimmed = line.strip()
|
|
93
|
+
# Skip empty lines and comment lines
|
|
94
|
+
if trimmed and not trimmed.startswith("#"):
|
|
95
|
+
commands.append(trimmed)
|
|
96
|
+
|
|
97
|
+
# Execute all commands joined by semicolons
|
|
98
|
+
command_string = "; ".join(commands)
|
|
99
|
+
result = await ctx.exec(command_string, {"env": positional_env, "cwd": ctx.cwd})
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ShCommand:
|
|
104
|
+
"""The sh command - execute shell commands or scripts (POSIX shell)."""
|
|
105
|
+
|
|
106
|
+
name = "sh"
|
|
107
|
+
|
|
108
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
109
|
+
"""Execute the sh command."""
|
|
110
|
+
if "--help" in args:
|
|
111
|
+
return ExecResult(
|
|
112
|
+
stdout=(
|
|
113
|
+
"Usage: sh [OPTIONS] [SCRIPT_FILE] [ARGUMENTS...]\n"
|
|
114
|
+
"Execute shell commands or scripts (POSIX shell).\n\n"
|
|
115
|
+
"Options:\n"
|
|
116
|
+
" -c COMMAND execute COMMAND string\n"
|
|
117
|
+
" --help display this help and exit\n\n"
|
|
118
|
+
"Without -c, reads and executes commands from SCRIPT_FILE.\n"
|
|
119
|
+
),
|
|
120
|
+
stderr="",
|
|
121
|
+
exit_code=0,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Same implementation as bash
|
|
125
|
+
# Handle -c flag
|
|
126
|
+
if len(args) >= 2 and args[0] == "-c":
|
|
127
|
+
command = args[1]
|
|
128
|
+
script_name = args[2] if len(args) > 2 else "sh"
|
|
129
|
+
script_args = args[3:] if len(args) > 3 else []
|
|
130
|
+
return await self._execute_script(command, script_name, script_args, ctx)
|
|
131
|
+
|
|
132
|
+
if not args:
|
|
133
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
134
|
+
|
|
135
|
+
script_path = args[0]
|
|
136
|
+
script_args = args[1:]
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
full_path = ctx.fs.resolve_path(ctx.cwd, script_path)
|
|
140
|
+
script_content = await ctx.fs.read_file(full_path)
|
|
141
|
+
return await self._execute_script(script_content, script_path, script_args, ctx)
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
return ExecResult(
|
|
144
|
+
stdout="",
|
|
145
|
+
stderr=f"sh: {script_path}: No such file or directory\n",
|
|
146
|
+
exit_code=127,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def _execute_script(
|
|
150
|
+
self,
|
|
151
|
+
script: str,
|
|
152
|
+
script_name: str,
|
|
153
|
+
script_args: list[str],
|
|
154
|
+
ctx: CommandContext,
|
|
155
|
+
) -> ExecResult:
|
|
156
|
+
"""Execute a script with positional parameters."""
|
|
157
|
+
if not ctx.exec:
|
|
158
|
+
return ExecResult(
|
|
159
|
+
stdout="",
|
|
160
|
+
stderr="sh: internal error: exec function not available\n",
|
|
161
|
+
exit_code=1,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
positional_env: dict[str, str] = {
|
|
165
|
+
"0": script_name,
|
|
166
|
+
"#": str(len(script_args)),
|
|
167
|
+
"@": " ".join(script_args),
|
|
168
|
+
"*": " ".join(script_args),
|
|
169
|
+
}
|
|
170
|
+
for i, arg in enumerate(script_args):
|
|
171
|
+
positional_env[str(i + 1)] = arg
|
|
172
|
+
|
|
173
|
+
script_to_run = script
|
|
174
|
+
if script_to_run.startswith("#!"):
|
|
175
|
+
first_newline = script_to_run.find("\n")
|
|
176
|
+
if first_newline != -1:
|
|
177
|
+
script_to_run = script_to_run[first_newline + 1:]
|
|
178
|
+
|
|
179
|
+
lines = script_to_run.split("\n")
|
|
180
|
+
commands: list[str] = []
|
|
181
|
+
for line in lines:
|
|
182
|
+
trimmed = line.strip()
|
|
183
|
+
if trimmed and not trimmed.startswith("#"):
|
|
184
|
+
commands.append(trimmed)
|
|
185
|
+
|
|
186
|
+
command_string = "; ".join(commands)
|
|
187
|
+
result = await ctx.exec(command_string, {"env": positional_env, "cwd": ctx.cwd})
|
|
188
|
+
return result
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Cat command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: cat [OPTION]... [FILE]...
|
|
4
|
+
|
|
5
|
+
Concatenate FILE(s) to standard output.
|
|
6
|
+
With no FILE, or when FILE is -, read standard input.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-n, --number number all output lines
|
|
10
|
+
-b, --number-nonblank number nonempty output lines
|
|
11
|
+
-s, --squeeze-blank suppress repeated empty output lines
|
|
12
|
+
-E, --show-ends display $ at end of each line
|
|
13
|
+
-T, --show-tabs display TAB characters as ^I
|
|
14
|
+
-v, --show-nonprinting use ^ and M- notation, except for LFD and TAB
|
|
15
|
+
-A, --show-all equivalent to -vET
|
|
16
|
+
-e equivalent to -vE
|
|
17
|
+
-t equivalent to -vT
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from ...types import Command, CommandContext, ExecResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CatCommand:
|
|
24
|
+
"""The cat command."""
|
|
25
|
+
|
|
26
|
+
name = "cat"
|
|
27
|
+
|
|
28
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
29
|
+
"""Execute the cat command."""
|
|
30
|
+
# Options
|
|
31
|
+
number_lines = False
|
|
32
|
+
number_nonblank = False
|
|
33
|
+
squeeze_blank = False
|
|
34
|
+
show_ends = False
|
|
35
|
+
show_tabs = False
|
|
36
|
+
show_nonprinting = False
|
|
37
|
+
|
|
38
|
+
files: list[str] = []
|
|
39
|
+
|
|
40
|
+
# Parse arguments
|
|
41
|
+
i = 0
|
|
42
|
+
while i < len(args):
|
|
43
|
+
arg = args[i]
|
|
44
|
+
if arg == "--":
|
|
45
|
+
files.extend(args[i + 1 :])
|
|
46
|
+
break
|
|
47
|
+
elif arg.startswith("--"):
|
|
48
|
+
if arg == "--number":
|
|
49
|
+
number_lines = True
|
|
50
|
+
elif arg == "--number-nonblank":
|
|
51
|
+
number_nonblank = True
|
|
52
|
+
elif arg == "--squeeze-blank":
|
|
53
|
+
squeeze_blank = True
|
|
54
|
+
elif arg == "--show-ends":
|
|
55
|
+
show_ends = True
|
|
56
|
+
elif arg == "--show-tabs":
|
|
57
|
+
show_tabs = True
|
|
58
|
+
elif arg == "--show-nonprinting":
|
|
59
|
+
show_nonprinting = True
|
|
60
|
+
elif arg == "--show-all":
|
|
61
|
+
show_nonprinting = True
|
|
62
|
+
show_ends = True
|
|
63
|
+
show_tabs = True
|
|
64
|
+
else:
|
|
65
|
+
return ExecResult(
|
|
66
|
+
stdout="",
|
|
67
|
+
stderr=f"cat: unrecognized option '{arg}'\n",
|
|
68
|
+
exit_code=1,
|
|
69
|
+
)
|
|
70
|
+
elif arg.startswith("-") and arg != "-":
|
|
71
|
+
for c in arg[1:]:
|
|
72
|
+
if c == "n":
|
|
73
|
+
number_lines = True
|
|
74
|
+
elif c == "b":
|
|
75
|
+
number_nonblank = True
|
|
76
|
+
elif c == "s":
|
|
77
|
+
squeeze_blank = True
|
|
78
|
+
elif c == "E":
|
|
79
|
+
show_ends = True
|
|
80
|
+
elif c == "T":
|
|
81
|
+
show_tabs = True
|
|
82
|
+
elif c == "v":
|
|
83
|
+
show_nonprinting = True
|
|
84
|
+
elif c == "A":
|
|
85
|
+
show_nonprinting = True
|
|
86
|
+
show_ends = True
|
|
87
|
+
show_tabs = True
|
|
88
|
+
elif c == "e":
|
|
89
|
+
show_nonprinting = True
|
|
90
|
+
show_ends = True
|
|
91
|
+
elif c == "t":
|
|
92
|
+
show_nonprinting = True
|
|
93
|
+
show_tabs = True
|
|
94
|
+
else:
|
|
95
|
+
return ExecResult(
|
|
96
|
+
stdout="",
|
|
97
|
+
stderr=f"cat: invalid option -- '{c}'\n",
|
|
98
|
+
exit_code=1,
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
files.append(arg)
|
|
102
|
+
i += 1
|
|
103
|
+
|
|
104
|
+
# If number_nonblank is set, don't number all lines
|
|
105
|
+
if number_nonblank:
|
|
106
|
+
number_lines = False
|
|
107
|
+
|
|
108
|
+
# If no files, read from stdin
|
|
109
|
+
if not files:
|
|
110
|
+
files = ["-"]
|
|
111
|
+
|
|
112
|
+
stdout = ""
|
|
113
|
+
stderr = ""
|
|
114
|
+
exit_code = 0
|
|
115
|
+
line_number = 1
|
|
116
|
+
prev_blank = False
|
|
117
|
+
|
|
118
|
+
for file in files:
|
|
119
|
+
try:
|
|
120
|
+
if file == "-":
|
|
121
|
+
content = ctx.stdin
|
|
122
|
+
else:
|
|
123
|
+
# Resolve path
|
|
124
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
125
|
+
content = await ctx.fs.read_file(path)
|
|
126
|
+
|
|
127
|
+
lines = content.split("\n")
|
|
128
|
+
|
|
129
|
+
# Process each line
|
|
130
|
+
for j, line in enumerate(lines):
|
|
131
|
+
# Don't output trailing empty string from split
|
|
132
|
+
if j == len(lines) - 1 and line == "":
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
is_blank = line == ""
|
|
136
|
+
|
|
137
|
+
# Squeeze blank lines
|
|
138
|
+
if squeeze_blank and is_blank and prev_blank:
|
|
139
|
+
continue
|
|
140
|
+
prev_blank = is_blank
|
|
141
|
+
|
|
142
|
+
# Process line
|
|
143
|
+
output_line = line
|
|
144
|
+
|
|
145
|
+
# Show tabs
|
|
146
|
+
if show_tabs:
|
|
147
|
+
output_line = output_line.replace("\t", "^I")
|
|
148
|
+
|
|
149
|
+
# Show nonprinting (simplified - just shows tab replacement already done)
|
|
150
|
+
# Full implementation would handle all non-printable chars
|
|
151
|
+
|
|
152
|
+
# Show ends
|
|
153
|
+
if show_ends:
|
|
154
|
+
output_line += "$"
|
|
155
|
+
|
|
156
|
+
# Number lines
|
|
157
|
+
if number_lines or (number_nonblank and not is_blank):
|
|
158
|
+
output_line = f"{line_number:6}\t{output_line}"
|
|
159
|
+
line_number += 1
|
|
160
|
+
|
|
161
|
+
stdout += output_line + "\n"
|
|
162
|
+
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
stderr += f"cat: {file}: No such file or directory\n"
|
|
165
|
+
exit_code = 1
|
|
166
|
+
except IsADirectoryError:
|
|
167
|
+
stderr += f"cat: {file}: Is a directory\n"
|
|
168
|
+
exit_code = 1
|
|
169
|
+
except PermissionError:
|
|
170
|
+
stderr += f"cat: {file}: Permission denied\n"
|
|
171
|
+
exit_code = 1
|
|
172
|
+
|
|
173
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|