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
|
+
"""Strings command implementation."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StringsCommand:
|
|
7
|
+
"""The strings command - find printable strings in files."""
|
|
8
|
+
|
|
9
|
+
name = "strings"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the strings command."""
|
|
13
|
+
min_length = 4
|
|
14
|
+
show_offset = False
|
|
15
|
+
offset_format = "o" # octal by default
|
|
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: strings [OPTION]... [FILE]...\nFind printable strings in files.\n",
|
|
24
|
+
stderr="",
|
|
25
|
+
exit_code=0,
|
|
26
|
+
)
|
|
27
|
+
elif arg == "-n" and i + 1 < len(args):
|
|
28
|
+
i += 1
|
|
29
|
+
try:
|
|
30
|
+
min_length = int(args[i])
|
|
31
|
+
except ValueError:
|
|
32
|
+
return ExecResult(
|
|
33
|
+
stdout="",
|
|
34
|
+
stderr=f"strings: invalid minimum string length: '{args[i]}'\n",
|
|
35
|
+
exit_code=1,
|
|
36
|
+
)
|
|
37
|
+
elif arg.startswith("-n"):
|
|
38
|
+
try:
|
|
39
|
+
min_length = int(arg[2:])
|
|
40
|
+
except ValueError:
|
|
41
|
+
return ExecResult(
|
|
42
|
+
stdout="",
|
|
43
|
+
stderr=f"strings: invalid minimum string length: '{arg[2:]}'\n",
|
|
44
|
+
exit_code=1,
|
|
45
|
+
)
|
|
46
|
+
elif arg == "-o":
|
|
47
|
+
show_offset = True
|
|
48
|
+
offset_format = "o"
|
|
49
|
+
elif arg == "-t" and i + 1 < len(args):
|
|
50
|
+
i += 1
|
|
51
|
+
show_offset = True
|
|
52
|
+
offset_format = args[i]
|
|
53
|
+
elif arg.startswith("-t"):
|
|
54
|
+
show_offset = True
|
|
55
|
+
offset_format = arg[2:]
|
|
56
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
57
|
+
# Could be -N for minimum length
|
|
58
|
+
try:
|
|
59
|
+
min_length = int(arg[1:])
|
|
60
|
+
except ValueError:
|
|
61
|
+
return ExecResult(
|
|
62
|
+
stdout="",
|
|
63
|
+
stderr=f"strings: invalid option -- '{arg[1]}'\n",
|
|
64
|
+
exit_code=1,
|
|
65
|
+
)
|
|
66
|
+
elif arg == "--":
|
|
67
|
+
files.extend(args[i + 1:])
|
|
68
|
+
break
|
|
69
|
+
else:
|
|
70
|
+
files.append(arg)
|
|
71
|
+
i += 1
|
|
72
|
+
|
|
73
|
+
if min_length <= 0:
|
|
74
|
+
return ExecResult(
|
|
75
|
+
stdout="",
|
|
76
|
+
stderr="strings: minimum string length must be greater than 0\n",
|
|
77
|
+
exit_code=1,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Read from stdin if no files
|
|
81
|
+
if not files:
|
|
82
|
+
content = ctx.stdin.encode("utf-8", errors="replace")
|
|
83
|
+
result = self._find_strings(content, min_length, show_offset, offset_format)
|
|
84
|
+
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
85
|
+
|
|
86
|
+
stdout_parts = []
|
|
87
|
+
stderr = ""
|
|
88
|
+
exit_code = 0
|
|
89
|
+
|
|
90
|
+
for file in files:
|
|
91
|
+
try:
|
|
92
|
+
if file == "-":
|
|
93
|
+
content = ctx.stdin.encode("utf-8", errors="replace")
|
|
94
|
+
else:
|
|
95
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
96
|
+
content = await ctx.fs.read_file_bytes(path)
|
|
97
|
+
|
|
98
|
+
result = self._find_strings(content, min_length, show_offset, offset_format)
|
|
99
|
+
stdout_parts.append(result)
|
|
100
|
+
|
|
101
|
+
except FileNotFoundError:
|
|
102
|
+
stderr += f"strings: {file}: No such file or directory\n"
|
|
103
|
+
exit_code = 1
|
|
104
|
+
|
|
105
|
+
return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
|
|
106
|
+
|
|
107
|
+
def _find_strings(
|
|
108
|
+
self, data: bytes, min_length: int, show_offset: bool = False, offset_format: str = "o"
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Find printable strings in binary data."""
|
|
111
|
+
result = []
|
|
112
|
+
current = []
|
|
113
|
+
start_offset = 0
|
|
114
|
+
|
|
115
|
+
for idx, byte in enumerate(data):
|
|
116
|
+
# Check if byte is printable ASCII (32-126) or tab/newline
|
|
117
|
+
if 32 <= byte <= 126 or byte in (9, 10, 13):
|
|
118
|
+
if not current:
|
|
119
|
+
start_offset = idx
|
|
120
|
+
current.append(chr(byte))
|
|
121
|
+
else:
|
|
122
|
+
if len(current) >= min_length:
|
|
123
|
+
s = "".join(current)
|
|
124
|
+
if show_offset:
|
|
125
|
+
if offset_format == "x":
|
|
126
|
+
result.append(f"{start_offset:7x} {s}")
|
|
127
|
+
elif offset_format == "d":
|
|
128
|
+
result.append(f"{start_offset:7d} {s}")
|
|
129
|
+
else: # octal
|
|
130
|
+
result.append(f"{start_offset:7o} {s}")
|
|
131
|
+
else:
|
|
132
|
+
result.append(s)
|
|
133
|
+
current = []
|
|
134
|
+
|
|
135
|
+
# Check final string
|
|
136
|
+
if len(current) >= min_length:
|
|
137
|
+
s = "".join(current)
|
|
138
|
+
if show_offset:
|
|
139
|
+
if offset_format == "x":
|
|
140
|
+
result.append(f"{start_offset:7x} {s}")
|
|
141
|
+
elif offset_format == "d":
|
|
142
|
+
result.append(f"{start_offset:7d} {s}")
|
|
143
|
+
else: # octal
|
|
144
|
+
result.append(f"{start_offset:7o} {s}")
|
|
145
|
+
else:
|
|
146
|
+
result.append(s)
|
|
147
|
+
|
|
148
|
+
if result:
|
|
149
|
+
return "\n".join(result) + "\n"
|
|
150
|
+
return ""
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Tac command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: tac [OPTION]... [FILE]...
|
|
4
|
+
|
|
5
|
+
Concatenate and print files in reverse.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-b, --before attach the separator before instead of after
|
|
9
|
+
-r, --regex interpret the separator as a regular expression
|
|
10
|
+
-s, --separator=STRING use STRING as the separator instead of newline
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from ...types import CommandContext, ExecResult
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TacCommand:
|
|
18
|
+
"""The tac command - reverse lines."""
|
|
19
|
+
|
|
20
|
+
name = "tac"
|
|
21
|
+
|
|
22
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
23
|
+
"""Execute the tac command."""
|
|
24
|
+
files: list[str] = []
|
|
25
|
+
separator = "\n"
|
|
26
|
+
before = False
|
|
27
|
+
regex_mode = False
|
|
28
|
+
|
|
29
|
+
# Parse arguments
|
|
30
|
+
i = 0
|
|
31
|
+
while i < len(args):
|
|
32
|
+
arg = args[i]
|
|
33
|
+
if arg == "--help":
|
|
34
|
+
return ExecResult(
|
|
35
|
+
stdout="Usage: tac [OPTION]... [FILE]...\n",
|
|
36
|
+
stderr="",
|
|
37
|
+
exit_code=0,
|
|
38
|
+
)
|
|
39
|
+
elif arg == "--":
|
|
40
|
+
files.extend(args[i + 1:])
|
|
41
|
+
break
|
|
42
|
+
elif arg.startswith("--"):
|
|
43
|
+
if arg == "--before":
|
|
44
|
+
before = True
|
|
45
|
+
elif arg == "--regex":
|
|
46
|
+
regex_mode = True
|
|
47
|
+
elif arg.startswith("--separator="):
|
|
48
|
+
separator = arg[12:]
|
|
49
|
+
else:
|
|
50
|
+
return ExecResult(
|
|
51
|
+
stdout="",
|
|
52
|
+
stderr=f"tac: unrecognized option '{arg}'\n",
|
|
53
|
+
exit_code=1,
|
|
54
|
+
)
|
|
55
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
56
|
+
j = 1
|
|
57
|
+
while j < len(arg):
|
|
58
|
+
c = arg[j]
|
|
59
|
+
if c == "b":
|
|
60
|
+
before = True
|
|
61
|
+
elif c == "r":
|
|
62
|
+
regex_mode = True
|
|
63
|
+
elif c == "s":
|
|
64
|
+
# -s requires a value
|
|
65
|
+
if j + 1 < len(arg):
|
|
66
|
+
separator = arg[j + 1:]
|
|
67
|
+
break
|
|
68
|
+
elif i + 1 < len(args):
|
|
69
|
+
i += 1
|
|
70
|
+
separator = args[i]
|
|
71
|
+
break
|
|
72
|
+
else:
|
|
73
|
+
return ExecResult(
|
|
74
|
+
stdout="",
|
|
75
|
+
stderr="tac: option requires an argument -- 's'\n",
|
|
76
|
+
exit_code=1,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
return ExecResult(
|
|
80
|
+
stdout="",
|
|
81
|
+
stderr=f"tac: invalid option -- '{c}'\n",
|
|
82
|
+
exit_code=1,
|
|
83
|
+
)
|
|
84
|
+
j += 1
|
|
85
|
+
else:
|
|
86
|
+
files.append(arg)
|
|
87
|
+
i += 1
|
|
88
|
+
|
|
89
|
+
# Read content
|
|
90
|
+
if files:
|
|
91
|
+
content_parts = []
|
|
92
|
+
for f in files:
|
|
93
|
+
try:
|
|
94
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
95
|
+
content_parts.append(await ctx.fs.read_file(path))
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
return ExecResult(
|
|
98
|
+
stdout="",
|
|
99
|
+
stderr=f"tac: {f}: No such file or directory\n",
|
|
100
|
+
exit_code=1,
|
|
101
|
+
)
|
|
102
|
+
content = "".join(content_parts)
|
|
103
|
+
else:
|
|
104
|
+
content = ctx.stdin
|
|
105
|
+
|
|
106
|
+
if not content:
|
|
107
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
108
|
+
|
|
109
|
+
# Split content by separator
|
|
110
|
+
if regex_mode:
|
|
111
|
+
try:
|
|
112
|
+
pattern = re.compile(separator)
|
|
113
|
+
# Split and keep track of what separators were matched
|
|
114
|
+
records = pattern.split(content)
|
|
115
|
+
# Find all separator matches
|
|
116
|
+
separators = pattern.findall(content)
|
|
117
|
+
except re.error as e:
|
|
118
|
+
return ExecResult(
|
|
119
|
+
stdout="",
|
|
120
|
+
stderr=f"tac: invalid regex: {e}\n",
|
|
121
|
+
exit_code=1,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
# Literal separator split
|
|
125
|
+
records = content.split(separator)
|
|
126
|
+
# All separators are the same literal
|
|
127
|
+
separators = [separator] * (len(records) - 1) if len(records) > 1 else []
|
|
128
|
+
|
|
129
|
+
# Handle trailing empty record (from trailing separator)
|
|
130
|
+
trailing_empty = records and records[-1] == ""
|
|
131
|
+
if trailing_empty:
|
|
132
|
+
records = records[:-1]
|
|
133
|
+
|
|
134
|
+
# Reverse the records
|
|
135
|
+
records.reverse()
|
|
136
|
+
|
|
137
|
+
# Reconstruct output with separators
|
|
138
|
+
if not before:
|
|
139
|
+
# Normal mode: separator follows each record (except last)
|
|
140
|
+
output_parts = []
|
|
141
|
+
for i, record in enumerate(records):
|
|
142
|
+
output_parts.append(record)
|
|
143
|
+
if i < len(records) - 1:
|
|
144
|
+
output_parts.append(separators[0] if separators else separator)
|
|
145
|
+
# Add trailing separator if original had one
|
|
146
|
+
if trailing_empty:
|
|
147
|
+
output_parts.append(separators[0] if separators else separator)
|
|
148
|
+
output = "".join(output_parts)
|
|
149
|
+
else:
|
|
150
|
+
# -b mode: separator precedes each record (except first)
|
|
151
|
+
output_parts = []
|
|
152
|
+
for i, record in enumerate(records):
|
|
153
|
+
if i > 0:
|
|
154
|
+
output_parts.append(separators[0] if separators else separator)
|
|
155
|
+
output_parts.append(record)
|
|
156
|
+
output = "".join(output_parts)
|
|
157
|
+
|
|
158
|
+
return ExecResult(stdout=output, stderr="", exit_code=0)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tail command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: tail [OPTION]... [FILE]...
|
|
4
|
+
|
|
5
|
+
Print the last 10 lines of each FILE to standard output.
|
|
6
|
+
With more than one FILE, precede each with a header giving the file name.
|
|
7
|
+
With no FILE, or when FILE is -, read standard input.
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
-n, --lines=NUM output the last NUM lines, instead of the last 10
|
|
11
|
+
-c, --bytes=NUM output the last NUM bytes
|
|
12
|
+
-q, --quiet never output headers giving file names
|
|
13
|
+
-v, --verbose always output headers giving file names
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ...types import CommandContext, ExecResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TailCommand:
|
|
20
|
+
"""The tail command."""
|
|
21
|
+
|
|
22
|
+
name = "tail"
|
|
23
|
+
|
|
24
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
25
|
+
"""Execute the tail command."""
|
|
26
|
+
num_lines = 10
|
|
27
|
+
num_bytes = None
|
|
28
|
+
quiet = False
|
|
29
|
+
verbose = False
|
|
30
|
+
from_start = False # +NUM means from line NUM
|
|
31
|
+
files: list[str] = []
|
|
32
|
+
|
|
33
|
+
# Parse arguments
|
|
34
|
+
i = 0
|
|
35
|
+
while i < len(args):
|
|
36
|
+
arg = args[i]
|
|
37
|
+
if arg == "--":
|
|
38
|
+
files.extend(args[i + 1:])
|
|
39
|
+
break
|
|
40
|
+
elif arg.startswith("--lines="):
|
|
41
|
+
val = arg[8:]
|
|
42
|
+
if val.startswith("+"):
|
|
43
|
+
from_start = True
|
|
44
|
+
val = val[1:]
|
|
45
|
+
try:
|
|
46
|
+
num_lines = int(val)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return ExecResult(
|
|
49
|
+
stdout="",
|
|
50
|
+
stderr=f"tail: invalid number of lines: '{arg[8:]}'\n",
|
|
51
|
+
exit_code=1,
|
|
52
|
+
)
|
|
53
|
+
elif arg.startswith("--bytes="):
|
|
54
|
+
try:
|
|
55
|
+
num_bytes = int(arg[8:])
|
|
56
|
+
except ValueError:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"tail: invalid number of bytes: '{arg[8:]}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
elif arg == "--quiet" or arg == "-q":
|
|
63
|
+
quiet = True
|
|
64
|
+
elif arg == "--verbose" or arg == "-v":
|
|
65
|
+
verbose = True
|
|
66
|
+
elif arg.startswith("-n"):
|
|
67
|
+
if len(arg) > 2:
|
|
68
|
+
val = arg[2:]
|
|
69
|
+
else:
|
|
70
|
+
i += 1
|
|
71
|
+
if i >= len(args):
|
|
72
|
+
return ExecResult(
|
|
73
|
+
stdout="",
|
|
74
|
+
stderr="tail: option requires an argument -- 'n'\n",
|
|
75
|
+
exit_code=1,
|
|
76
|
+
)
|
|
77
|
+
val = args[i]
|
|
78
|
+
if val.startswith("+"):
|
|
79
|
+
from_start = True
|
|
80
|
+
val = val[1:]
|
|
81
|
+
try:
|
|
82
|
+
num_lines = int(val)
|
|
83
|
+
except ValueError:
|
|
84
|
+
return ExecResult(
|
|
85
|
+
stdout="",
|
|
86
|
+
stderr=f"tail: invalid number of lines: '{val}'\n",
|
|
87
|
+
exit_code=1,
|
|
88
|
+
)
|
|
89
|
+
elif arg.startswith("-c"):
|
|
90
|
+
if len(arg) > 2:
|
|
91
|
+
val = arg[2:]
|
|
92
|
+
else:
|
|
93
|
+
i += 1
|
|
94
|
+
if i >= len(args):
|
|
95
|
+
return ExecResult(
|
|
96
|
+
stdout="",
|
|
97
|
+
stderr="tail: option requires an argument -- 'c'\n",
|
|
98
|
+
exit_code=1,
|
|
99
|
+
)
|
|
100
|
+
val = args[i]
|
|
101
|
+
try:
|
|
102
|
+
num_bytes = int(val)
|
|
103
|
+
except ValueError:
|
|
104
|
+
return ExecResult(
|
|
105
|
+
stdout="",
|
|
106
|
+
stderr=f"tail: invalid number of bytes: '{val}'\n",
|
|
107
|
+
exit_code=1,
|
|
108
|
+
)
|
|
109
|
+
elif arg.startswith("+") and len(arg) > 1:
|
|
110
|
+
# +NUM means from line NUM
|
|
111
|
+
try:
|
|
112
|
+
num_lines = int(arg[1:])
|
|
113
|
+
from_start = True
|
|
114
|
+
except ValueError:
|
|
115
|
+
files.append(arg)
|
|
116
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
117
|
+
# Check for -NUM shorthand
|
|
118
|
+
try:
|
|
119
|
+
num_lines = int(arg[1:])
|
|
120
|
+
except ValueError:
|
|
121
|
+
return ExecResult(
|
|
122
|
+
stdout="",
|
|
123
|
+
stderr=f"tail: invalid option -- '{arg[1]}'\n",
|
|
124
|
+
exit_code=1,
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
files.append(arg)
|
|
128
|
+
i += 1
|
|
129
|
+
|
|
130
|
+
# Default to stdin
|
|
131
|
+
if not files:
|
|
132
|
+
files = ["-"]
|
|
133
|
+
|
|
134
|
+
stdout = ""
|
|
135
|
+
stderr = ""
|
|
136
|
+
exit_code = 0
|
|
137
|
+
show_headers = (len(files) > 1 and not quiet) or verbose
|
|
138
|
+
|
|
139
|
+
for file_idx, file in enumerate(files):
|
|
140
|
+
try:
|
|
141
|
+
if file == "-":
|
|
142
|
+
content = ctx.stdin
|
|
143
|
+
else:
|
|
144
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
145
|
+
content = await ctx.fs.read_file(path)
|
|
146
|
+
|
|
147
|
+
if show_headers:
|
|
148
|
+
if file_idx > 0:
|
|
149
|
+
stdout += "\n"
|
|
150
|
+
stdout += f"==> {file} <==\n"
|
|
151
|
+
|
|
152
|
+
if num_bytes is not None:
|
|
153
|
+
if from_start:
|
|
154
|
+
stdout += content[num_bytes - 1:]
|
|
155
|
+
else:
|
|
156
|
+
stdout += content[-num_bytes:] if num_bytes > 0 else ""
|
|
157
|
+
else:
|
|
158
|
+
lines = content.split("\n")
|
|
159
|
+
# Handle the case where content ends with newline
|
|
160
|
+
if lines and lines[-1] == "":
|
|
161
|
+
lines = lines[:-1]
|
|
162
|
+
|
|
163
|
+
if from_start:
|
|
164
|
+
# +NUM means starting from line NUM (1-indexed)
|
|
165
|
+
selected = lines[num_lines - 1:]
|
|
166
|
+
else:
|
|
167
|
+
selected = lines[-num_lines:] if num_lines > 0 else []
|
|
168
|
+
|
|
169
|
+
stdout += "\n".join(selected)
|
|
170
|
+
if selected:
|
|
171
|
+
stdout += "\n"
|
|
172
|
+
|
|
173
|
+
except FileNotFoundError:
|
|
174
|
+
stderr += f"tail: cannot open '{file}' for reading: No such file or directory\n"
|
|
175
|
+
exit_code = 1
|
|
176
|
+
except IsADirectoryError:
|
|
177
|
+
stderr += f"tail: error reading '{file}': Is a directory\n"
|
|
178
|
+
exit_code = 1
|
|
179
|
+
|
|
180
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|