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,139 @@
|
|
|
1
|
+
"""Tree command implementation."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TreeCommand:
|
|
7
|
+
"""The tree command - display directory tree."""
|
|
8
|
+
|
|
9
|
+
name = "tree"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the tree command."""
|
|
13
|
+
show_all = False
|
|
14
|
+
dirs_only = False
|
|
15
|
+
max_depth = None
|
|
16
|
+
show_full_path = False
|
|
17
|
+
paths: list[str] = []
|
|
18
|
+
|
|
19
|
+
i = 0
|
|
20
|
+
while i < len(args):
|
|
21
|
+
arg = args[i]
|
|
22
|
+
if arg == "-a":
|
|
23
|
+
show_all = True
|
|
24
|
+
elif arg == "-d":
|
|
25
|
+
dirs_only = True
|
|
26
|
+
elif arg == "-L" and i + 1 < len(args):
|
|
27
|
+
i += 1
|
|
28
|
+
try:
|
|
29
|
+
max_depth = int(args[i])
|
|
30
|
+
except ValueError:
|
|
31
|
+
return ExecResult(
|
|
32
|
+
stdout="",
|
|
33
|
+
stderr=f"tree: Invalid level: {args[i]}\n",
|
|
34
|
+
exit_code=1,
|
|
35
|
+
)
|
|
36
|
+
elif arg == "-f":
|
|
37
|
+
show_full_path = True
|
|
38
|
+
elif arg == "--help":
|
|
39
|
+
return ExecResult(
|
|
40
|
+
stdout="Usage: tree [OPTIONS] [directory...]\n",
|
|
41
|
+
stderr="",
|
|
42
|
+
exit_code=0,
|
|
43
|
+
)
|
|
44
|
+
elif arg.startswith("-"):
|
|
45
|
+
pass # Ignore unknown options
|
|
46
|
+
else:
|
|
47
|
+
paths.append(arg)
|
|
48
|
+
i += 1
|
|
49
|
+
|
|
50
|
+
if not paths:
|
|
51
|
+
paths = ["."]
|
|
52
|
+
|
|
53
|
+
output_lines = []
|
|
54
|
+
total_dirs = 0
|
|
55
|
+
total_files = 0
|
|
56
|
+
|
|
57
|
+
for path in paths:
|
|
58
|
+
try:
|
|
59
|
+
resolved = ctx.fs.resolve_path(ctx.cwd, path)
|
|
60
|
+
stat = await ctx.fs.stat(resolved)
|
|
61
|
+
|
|
62
|
+
if not stat.is_directory:
|
|
63
|
+
return ExecResult(
|
|
64
|
+
stdout="",
|
|
65
|
+
stderr=f"tree: {path}: Not a directory\n",
|
|
66
|
+
exit_code=1,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
output_lines.append(path)
|
|
70
|
+
dirs, files = await self._tree(
|
|
71
|
+
ctx, resolved, "", show_all, dirs_only,
|
|
72
|
+
max_depth, 0, show_full_path, output_lines
|
|
73
|
+
)
|
|
74
|
+
total_dirs += dirs
|
|
75
|
+
total_files += files
|
|
76
|
+
|
|
77
|
+
except FileNotFoundError:
|
|
78
|
+
return ExecResult(
|
|
79
|
+
stdout="",
|
|
80
|
+
stderr=f"tree: {path}: No such file or directory\n",
|
|
81
|
+
exit_code=1,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
output_lines.append("")
|
|
85
|
+
output_lines.append(f"{total_dirs} directories, {total_files} files")
|
|
86
|
+
|
|
87
|
+
return ExecResult(
|
|
88
|
+
stdout="\n".join(output_lines) + "\n",
|
|
89
|
+
stderr="",
|
|
90
|
+
exit_code=0,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def _tree(
|
|
94
|
+
self, ctx: CommandContext, path: str, prefix: str,
|
|
95
|
+
show_all: bool, dirs_only: bool, max_depth: int | None,
|
|
96
|
+
current_depth: int, show_full_path: bool, output_lines: list[str]
|
|
97
|
+
) -> tuple[int, int]:
|
|
98
|
+
"""Recursively build tree output."""
|
|
99
|
+
if max_depth is not None and current_depth >= max_depth:
|
|
100
|
+
return 0, 0
|
|
101
|
+
|
|
102
|
+
entries = await ctx.fs.readdir(path)
|
|
103
|
+
entries = sorted(entries)
|
|
104
|
+
|
|
105
|
+
if not show_all:
|
|
106
|
+
entries = [e for e in entries if not e.startswith(".")]
|
|
107
|
+
|
|
108
|
+
dirs = 0
|
|
109
|
+
files = 0
|
|
110
|
+
|
|
111
|
+
for idx, entry in enumerate(entries):
|
|
112
|
+
is_last = idx == len(entries) - 1
|
|
113
|
+
connector = "└── " if is_last else "├── "
|
|
114
|
+
entry_path = f"{path}/{entry}"
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
stat = await ctx.fs.stat(entry_path)
|
|
118
|
+
|
|
119
|
+
if dirs_only and not stat.is_directory:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
display_name = entry_path if show_full_path else entry
|
|
123
|
+
output_lines.append(f"{prefix}{connector}{display_name}")
|
|
124
|
+
|
|
125
|
+
if stat.is_directory:
|
|
126
|
+
dirs += 1
|
|
127
|
+
new_prefix = prefix + (" " if is_last else "│ ")
|
|
128
|
+
sub_dirs, sub_files = await self._tree(
|
|
129
|
+
ctx, entry_path, new_prefix, show_all, dirs_only,
|
|
130
|
+
max_depth, current_depth + 1, show_full_path, output_lines
|
|
131
|
+
)
|
|
132
|
+
dirs += sub_dirs
|
|
133
|
+
files += sub_files
|
|
134
|
+
else:
|
|
135
|
+
files += 1
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
return dirs, files
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""True and false command implementations.
|
|
2
|
+
|
|
3
|
+
Usage: true
|
|
4
|
+
false
|
|
5
|
+
|
|
6
|
+
true - do nothing, successfully
|
|
7
|
+
false - do nothing, unsuccessfully
|
|
8
|
+
|
|
9
|
+
Exit with a status code indicating success (true) or failure (false).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from ...types import Command, CommandContext, ExecResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TrueCommand:
|
|
16
|
+
"""The true command - always succeeds."""
|
|
17
|
+
|
|
18
|
+
name = "true"
|
|
19
|
+
|
|
20
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
21
|
+
"""Execute the true command."""
|
|
22
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FalseCommand:
|
|
26
|
+
"""The false command - always fails."""
|
|
27
|
+
|
|
28
|
+
name = "false"
|
|
29
|
+
|
|
30
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
31
|
+
"""Execute the false command."""
|
|
32
|
+
return ExecResult(stdout="", stderr="", exit_code=1)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Uniq command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: uniq [OPTION]... [INPUT [OUTPUT]]
|
|
4
|
+
|
|
5
|
+
Filter adjacent matching lines from INPUT (or standard input),
|
|
6
|
+
writing to OUTPUT (or standard output).
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-c, --count prefix lines by the number of occurrences
|
|
10
|
+
-d, --repeated only print duplicate lines, one for each group
|
|
11
|
+
-D print all duplicate lines
|
|
12
|
+
-i, --ignore-case ignore differences in case when comparing
|
|
13
|
+
-u, --unique only print unique lines
|
|
14
|
+
-s, --skip-chars=N avoid comparing the first N characters
|
|
15
|
+
-w, --check-chars=N compare no more than N characters in lines
|
|
16
|
+
-f, --skip-fields=N avoid comparing the first N fields
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ...types import CommandContext, ExecResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UniqCommand:
|
|
23
|
+
"""The uniq command."""
|
|
24
|
+
|
|
25
|
+
name = "uniq"
|
|
26
|
+
|
|
27
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
28
|
+
"""Execute the uniq command."""
|
|
29
|
+
count = False
|
|
30
|
+
repeated = False
|
|
31
|
+
all_repeated = False
|
|
32
|
+
ignore_case = False
|
|
33
|
+
unique = False
|
|
34
|
+
skip_chars = 0
|
|
35
|
+
check_chars = 0 # 0 means no limit
|
|
36
|
+
skip_fields = 0
|
|
37
|
+
files: list[str] = []
|
|
38
|
+
|
|
39
|
+
# Parse arguments
|
|
40
|
+
i = 0
|
|
41
|
+
while i < len(args):
|
|
42
|
+
arg = args[i]
|
|
43
|
+
if arg == "--":
|
|
44
|
+
files.extend(args[i + 1:])
|
|
45
|
+
break
|
|
46
|
+
elif arg.startswith("--"):
|
|
47
|
+
if arg == "--count":
|
|
48
|
+
count = True
|
|
49
|
+
elif arg == "--repeated":
|
|
50
|
+
repeated = True
|
|
51
|
+
elif arg == "--ignore-case":
|
|
52
|
+
ignore_case = True
|
|
53
|
+
elif arg == "--unique":
|
|
54
|
+
unique = True
|
|
55
|
+
elif arg.startswith("--skip-chars="):
|
|
56
|
+
try:
|
|
57
|
+
skip_chars = int(arg[13:])
|
|
58
|
+
except ValueError:
|
|
59
|
+
return ExecResult(
|
|
60
|
+
stdout="",
|
|
61
|
+
stderr=f"uniq: invalid number of bytes to skip: '{arg[13:]}'\n",
|
|
62
|
+
exit_code=1,
|
|
63
|
+
)
|
|
64
|
+
elif arg.startswith("--check-chars="):
|
|
65
|
+
try:
|
|
66
|
+
check_chars = int(arg[14:])
|
|
67
|
+
except ValueError:
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="",
|
|
70
|
+
stderr=f"uniq: invalid number of bytes to compare: '{arg[14:]}'\n",
|
|
71
|
+
exit_code=1,
|
|
72
|
+
)
|
|
73
|
+
elif arg.startswith("--skip-fields="):
|
|
74
|
+
try:
|
|
75
|
+
skip_fields = int(arg[14:])
|
|
76
|
+
except ValueError:
|
|
77
|
+
return ExecResult(
|
|
78
|
+
stdout="",
|
|
79
|
+
stderr=f"uniq: invalid number of fields to skip: '{arg[14:]}'\n",
|
|
80
|
+
exit_code=1,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
return ExecResult(
|
|
84
|
+
stdout="",
|
|
85
|
+
stderr=f"uniq: unrecognized option '{arg}'\n",
|
|
86
|
+
exit_code=1,
|
|
87
|
+
)
|
|
88
|
+
elif arg.startswith("-") and arg != "-":
|
|
89
|
+
j = 1
|
|
90
|
+
while j < len(arg):
|
|
91
|
+
c = arg[j]
|
|
92
|
+
if c == "c":
|
|
93
|
+
count = True
|
|
94
|
+
elif c == "d":
|
|
95
|
+
repeated = True
|
|
96
|
+
elif c == "D":
|
|
97
|
+
all_repeated = True
|
|
98
|
+
elif c == "i":
|
|
99
|
+
ignore_case = True
|
|
100
|
+
elif c == "u":
|
|
101
|
+
unique = True
|
|
102
|
+
elif c == "s":
|
|
103
|
+
# -s requires a value
|
|
104
|
+
if j + 1 < len(arg):
|
|
105
|
+
try:
|
|
106
|
+
skip_chars = int(arg[j + 1:])
|
|
107
|
+
except ValueError:
|
|
108
|
+
return ExecResult(
|
|
109
|
+
stdout="",
|
|
110
|
+
stderr=f"uniq: invalid number of bytes to skip\n",
|
|
111
|
+
exit_code=1,
|
|
112
|
+
)
|
|
113
|
+
break
|
|
114
|
+
elif i + 1 < len(args):
|
|
115
|
+
i += 1
|
|
116
|
+
try:
|
|
117
|
+
skip_chars = int(args[i])
|
|
118
|
+
except ValueError:
|
|
119
|
+
return ExecResult(
|
|
120
|
+
stdout="",
|
|
121
|
+
stderr=f"uniq: invalid number of bytes to skip: '{args[i]}'\n",
|
|
122
|
+
exit_code=1,
|
|
123
|
+
)
|
|
124
|
+
break
|
|
125
|
+
else:
|
|
126
|
+
return ExecResult(
|
|
127
|
+
stdout="",
|
|
128
|
+
stderr="uniq: option requires an argument -- 's'\n",
|
|
129
|
+
exit_code=1,
|
|
130
|
+
)
|
|
131
|
+
elif c == "w":
|
|
132
|
+
# -w requires a value
|
|
133
|
+
if j + 1 < len(arg):
|
|
134
|
+
try:
|
|
135
|
+
check_chars = int(arg[j + 1:])
|
|
136
|
+
except ValueError:
|
|
137
|
+
return ExecResult(
|
|
138
|
+
stdout="",
|
|
139
|
+
stderr=f"uniq: invalid number of bytes to compare\n",
|
|
140
|
+
exit_code=1,
|
|
141
|
+
)
|
|
142
|
+
break
|
|
143
|
+
elif i + 1 < len(args):
|
|
144
|
+
i += 1
|
|
145
|
+
try:
|
|
146
|
+
check_chars = int(args[i])
|
|
147
|
+
except ValueError:
|
|
148
|
+
return ExecResult(
|
|
149
|
+
stdout="",
|
|
150
|
+
stderr=f"uniq: invalid number of bytes to compare: '{args[i]}'\n",
|
|
151
|
+
exit_code=1,
|
|
152
|
+
)
|
|
153
|
+
break
|
|
154
|
+
else:
|
|
155
|
+
return ExecResult(
|
|
156
|
+
stdout="",
|
|
157
|
+
stderr="uniq: option requires an argument -- 'w'\n",
|
|
158
|
+
exit_code=1,
|
|
159
|
+
)
|
|
160
|
+
elif c == "f":
|
|
161
|
+
# -f requires a value
|
|
162
|
+
if j + 1 < len(arg):
|
|
163
|
+
try:
|
|
164
|
+
skip_fields = int(arg[j + 1:])
|
|
165
|
+
except ValueError:
|
|
166
|
+
return ExecResult(
|
|
167
|
+
stdout="",
|
|
168
|
+
stderr=f"uniq: invalid number of fields to skip\n",
|
|
169
|
+
exit_code=1,
|
|
170
|
+
)
|
|
171
|
+
break
|
|
172
|
+
elif i + 1 < len(args):
|
|
173
|
+
i += 1
|
|
174
|
+
try:
|
|
175
|
+
skip_fields = int(args[i])
|
|
176
|
+
except ValueError:
|
|
177
|
+
return ExecResult(
|
|
178
|
+
stdout="",
|
|
179
|
+
stderr=f"uniq: invalid number of fields to skip: '{args[i]}'\n",
|
|
180
|
+
exit_code=1,
|
|
181
|
+
)
|
|
182
|
+
break
|
|
183
|
+
else:
|
|
184
|
+
return ExecResult(
|
|
185
|
+
stdout="",
|
|
186
|
+
stderr="uniq: option requires an argument -- 'f'\n",
|
|
187
|
+
exit_code=1,
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
return ExecResult(
|
|
191
|
+
stdout="",
|
|
192
|
+
stderr=f"uniq: invalid option -- '{c}'\n",
|
|
193
|
+
exit_code=1,
|
|
194
|
+
)
|
|
195
|
+
j += 1
|
|
196
|
+
else:
|
|
197
|
+
files.append(arg)
|
|
198
|
+
i += 1
|
|
199
|
+
|
|
200
|
+
# Get input
|
|
201
|
+
if len(files) == 0:
|
|
202
|
+
content = ctx.stdin
|
|
203
|
+
elif files[0] == "-":
|
|
204
|
+
content = ctx.stdin
|
|
205
|
+
else:
|
|
206
|
+
try:
|
|
207
|
+
path = ctx.fs.resolve_path(ctx.cwd, files[0])
|
|
208
|
+
content = await ctx.fs.read_file(path)
|
|
209
|
+
except FileNotFoundError:
|
|
210
|
+
return ExecResult(
|
|
211
|
+
stdout="",
|
|
212
|
+
stderr=f"uniq: {files[0]}: No such file or directory\n",
|
|
213
|
+
exit_code=1,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# Process lines
|
|
217
|
+
lines = content.split("\n")
|
|
218
|
+
# Remove trailing empty line if present
|
|
219
|
+
if lines and lines[-1] == "":
|
|
220
|
+
lines = lines[:-1]
|
|
221
|
+
|
|
222
|
+
def get_compare_key(line: str) -> str:
|
|
223
|
+
"""Get the comparison key for a line."""
|
|
224
|
+
# Skip fields first
|
|
225
|
+
if skip_fields > 0:
|
|
226
|
+
parts = line.split()
|
|
227
|
+
line = " ".join(parts[skip_fields:]) if len(parts) > skip_fields else ""
|
|
228
|
+
|
|
229
|
+
# Skip characters
|
|
230
|
+
if skip_chars > 0:
|
|
231
|
+
line = line[skip_chars:]
|
|
232
|
+
|
|
233
|
+
# Limit check chars
|
|
234
|
+
if check_chars > 0:
|
|
235
|
+
line = line[:check_chars]
|
|
236
|
+
|
|
237
|
+
if ignore_case:
|
|
238
|
+
line = line.lower()
|
|
239
|
+
|
|
240
|
+
return line
|
|
241
|
+
|
|
242
|
+
# Group adjacent lines
|
|
243
|
+
groups: list[tuple[int, str]] = [] # (count, original_line)
|
|
244
|
+
prev_key = None
|
|
245
|
+
prev_line = None
|
|
246
|
+
count_val = 0
|
|
247
|
+
|
|
248
|
+
for line in lines:
|
|
249
|
+
key = get_compare_key(line)
|
|
250
|
+
if key == prev_key:
|
|
251
|
+
count_val += 1
|
|
252
|
+
if all_repeated:
|
|
253
|
+
groups.append((1, line))
|
|
254
|
+
else:
|
|
255
|
+
if prev_line is not None:
|
|
256
|
+
if not all_repeated:
|
|
257
|
+
groups.append((count_val, prev_line))
|
|
258
|
+
prev_key = key
|
|
259
|
+
prev_line = line
|
|
260
|
+
count_val = 1
|
|
261
|
+
if all_repeated:
|
|
262
|
+
# We'll add it later if it has duplicates
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
# Don't forget the last group
|
|
266
|
+
if prev_line is not None and not all_repeated:
|
|
267
|
+
groups.append((count_val, prev_line))
|
|
268
|
+
|
|
269
|
+
# For all_repeated (-D), we need to rebuild groups differently
|
|
270
|
+
if all_repeated:
|
|
271
|
+
groups = []
|
|
272
|
+
prev_key = None
|
|
273
|
+
current_group: list[str] = []
|
|
274
|
+
|
|
275
|
+
for line in lines:
|
|
276
|
+
key = get_compare_key(line)
|
|
277
|
+
if key == prev_key:
|
|
278
|
+
current_group.append(line)
|
|
279
|
+
else:
|
|
280
|
+
# Output previous group if it had duplicates
|
|
281
|
+
if len(current_group) > 1:
|
|
282
|
+
for l in current_group:
|
|
283
|
+
groups.append((1, l))
|
|
284
|
+
current_group = [line]
|
|
285
|
+
prev_key = key
|
|
286
|
+
|
|
287
|
+
# Last group
|
|
288
|
+
if len(current_group) > 1:
|
|
289
|
+
for l in current_group:
|
|
290
|
+
groups.append((1, l))
|
|
291
|
+
|
|
292
|
+
# Filter based on options
|
|
293
|
+
output_lines: list[str] = []
|
|
294
|
+
for cnt, line in groups:
|
|
295
|
+
if repeated and cnt < 2:
|
|
296
|
+
continue
|
|
297
|
+
if unique and cnt > 1:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if count:
|
|
301
|
+
output_lines.append(f"{cnt:7d} {line}")
|
|
302
|
+
else:
|
|
303
|
+
output_lines.append(line)
|
|
304
|
+
|
|
305
|
+
# Generate output
|
|
306
|
+
stdout = "\n".join(output_lines)
|
|
307
|
+
if output_lines:
|
|
308
|
+
stdout += "\n"
|
|
309
|
+
|
|
310
|
+
# Write to output file if specified
|
|
311
|
+
if len(files) > 1 and files[1] != "-":
|
|
312
|
+
try:
|
|
313
|
+
path = ctx.fs.resolve_path(ctx.cwd, files[1])
|
|
314
|
+
await ctx.fs.write_file(path, stdout)
|
|
315
|
+
return ExecResult(stdout="", stderr="", exit_code=0)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
return ExecResult(
|
|
318
|
+
stdout="",
|
|
319
|
+
stderr=f"uniq: {files[1]}: {e}\n",
|
|
320
|
+
exit_code=1,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return ExecResult(stdout=stdout, stderr="", exit_code=0)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Wc command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: wc [OPTION]... [FILE]...
|
|
4
|
+
|
|
5
|
+
Print newline, word, and byte counts for each FILE.
|
|
6
|
+
With no FILE, or when FILE is -, read standard input.
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-c, --bytes print the byte counts
|
|
10
|
+
-m, --chars print the character counts
|
|
11
|
+
-l, --lines print the newline counts
|
|
12
|
+
-w, --words print the word counts
|
|
13
|
+
-L, --max-line-length print the length of the longest line
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ...types import CommandContext, ExecResult
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class WcCommand:
|
|
20
|
+
"""The wc command."""
|
|
21
|
+
|
|
22
|
+
name = "wc"
|
|
23
|
+
|
|
24
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
25
|
+
"""Execute the wc command."""
|
|
26
|
+
show_lines = False
|
|
27
|
+
show_words = False
|
|
28
|
+
show_bytes = False
|
|
29
|
+
show_chars = False
|
|
30
|
+
show_max_line = False
|
|
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("--"):
|
|
41
|
+
if arg == "--lines":
|
|
42
|
+
show_lines = True
|
|
43
|
+
elif arg == "--words":
|
|
44
|
+
show_words = True
|
|
45
|
+
elif arg == "--bytes":
|
|
46
|
+
show_bytes = True
|
|
47
|
+
elif arg == "--chars":
|
|
48
|
+
show_chars = True
|
|
49
|
+
elif arg == "--max-line-length":
|
|
50
|
+
show_max_line = True
|
|
51
|
+
else:
|
|
52
|
+
return ExecResult(
|
|
53
|
+
stdout="",
|
|
54
|
+
stderr=f"wc: unrecognized option '{arg}'\n",
|
|
55
|
+
exit_code=1,
|
|
56
|
+
)
|
|
57
|
+
elif arg.startswith("-") and arg != "-":
|
|
58
|
+
for c in arg[1:]:
|
|
59
|
+
if c == 'l':
|
|
60
|
+
show_lines = True
|
|
61
|
+
elif c == 'w':
|
|
62
|
+
show_words = True
|
|
63
|
+
elif c == 'c':
|
|
64
|
+
show_bytes = True
|
|
65
|
+
elif c == 'm':
|
|
66
|
+
show_chars = True
|
|
67
|
+
elif c == 'L':
|
|
68
|
+
show_max_line = True
|
|
69
|
+
else:
|
|
70
|
+
return ExecResult(
|
|
71
|
+
stdout="",
|
|
72
|
+
stderr=f"wc: invalid option -- '{c}'\n",
|
|
73
|
+
exit_code=1,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
files.append(arg)
|
|
77
|
+
i += 1
|
|
78
|
+
|
|
79
|
+
# Default to all three counts if none specified
|
|
80
|
+
if not (show_lines or show_words or show_bytes or show_chars or show_max_line):
|
|
81
|
+
show_lines = True
|
|
82
|
+
show_words = True
|
|
83
|
+
show_bytes = True
|
|
84
|
+
|
|
85
|
+
# Default to stdin
|
|
86
|
+
if not files:
|
|
87
|
+
files = ["-"]
|
|
88
|
+
|
|
89
|
+
stdout = ""
|
|
90
|
+
stderr = ""
|
|
91
|
+
exit_code = 0
|
|
92
|
+
|
|
93
|
+
total_lines = 0
|
|
94
|
+
total_words = 0
|
|
95
|
+
total_bytes = 0
|
|
96
|
+
total_chars = 0
|
|
97
|
+
total_max_line = 0
|
|
98
|
+
|
|
99
|
+
for file in files:
|
|
100
|
+
try:
|
|
101
|
+
if file == "-":
|
|
102
|
+
content = ctx.stdin
|
|
103
|
+
else:
|
|
104
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
105
|
+
content = await ctx.fs.read_file(path)
|
|
106
|
+
|
|
107
|
+
# Count lines (number of newlines)
|
|
108
|
+
lines = content.count("\n")
|
|
109
|
+
# Count words
|
|
110
|
+
words = len(content.split())
|
|
111
|
+
# Count bytes
|
|
112
|
+
bytes_count = len(content.encode("utf-8"))
|
|
113
|
+
# Count chars
|
|
114
|
+
chars = len(content)
|
|
115
|
+
# Max line length
|
|
116
|
+
max_line = max((len(line) for line in content.split("\n")), default=0)
|
|
117
|
+
|
|
118
|
+
total_lines += lines
|
|
119
|
+
total_words += words
|
|
120
|
+
total_bytes += bytes_count
|
|
121
|
+
total_chars += chars
|
|
122
|
+
total_max_line = max(total_max_line, max_line)
|
|
123
|
+
|
|
124
|
+
# Build output
|
|
125
|
+
# Use minimal formatting when only one counter is shown
|
|
126
|
+
num_counters = sum([show_lines, show_words, show_bytes, show_chars, show_max_line])
|
|
127
|
+
use_padding = num_counters > 1 or len(files) > 1
|
|
128
|
+
|
|
129
|
+
parts = []
|
|
130
|
+
if show_lines:
|
|
131
|
+
parts.append(f"{lines:7d}" if use_padding else str(lines))
|
|
132
|
+
if show_words:
|
|
133
|
+
parts.append(f"{words:7d}" if use_padding else str(words))
|
|
134
|
+
if show_bytes:
|
|
135
|
+
parts.append(f"{bytes_count:7d}" if use_padding else str(bytes_count))
|
|
136
|
+
if show_chars:
|
|
137
|
+
parts.append(f"{chars:7d}" if use_padding else str(chars))
|
|
138
|
+
if show_max_line:
|
|
139
|
+
parts.append(f"{max_line:7d}" if use_padding else str(max_line))
|
|
140
|
+
|
|
141
|
+
# Don't show filename for stdin when it's the only file
|
|
142
|
+
if file == "-" and len(files) == 1:
|
|
143
|
+
stdout += " ".join(parts) + "\n"
|
|
144
|
+
else:
|
|
145
|
+
stdout += " ".join(parts) + f" {file}\n"
|
|
146
|
+
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
stderr += f"wc: {file}: No such file or directory\n"
|
|
149
|
+
exit_code = 1
|
|
150
|
+
except IsADirectoryError:
|
|
151
|
+
stderr += f"wc: {file}: Is a directory\n"
|
|
152
|
+
exit_code = 1
|
|
153
|
+
|
|
154
|
+
# Print total if multiple files
|
|
155
|
+
if len(files) > 1:
|
|
156
|
+
parts = []
|
|
157
|
+
if show_lines:
|
|
158
|
+
parts.append(f"{total_lines:7d}")
|
|
159
|
+
if show_words:
|
|
160
|
+
parts.append(f"{total_words:7d}")
|
|
161
|
+
if show_bytes:
|
|
162
|
+
parts.append(f"{total_bytes:7d}")
|
|
163
|
+
if show_chars:
|
|
164
|
+
parts.append(f"{total_chars:7d}")
|
|
165
|
+
if show_max_line:
|
|
166
|
+
parts.append(f"{total_max_line:7d}")
|
|
167
|
+
stdout += " ".join(parts) + " total\n"
|
|
168
|
+
|
|
169
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|