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.
Files changed (193) hide show
  1. just_bash/__init__.py +55 -0
  2. just_bash/ast/__init__.py +213 -0
  3. just_bash/ast/factory.py +320 -0
  4. just_bash/ast/types.py +953 -0
  5. just_bash/bash.py +220 -0
  6. just_bash/commands/__init__.py +23 -0
  7. just_bash/commands/argv/__init__.py +5 -0
  8. just_bash/commands/argv/argv.py +21 -0
  9. just_bash/commands/awk/__init__.py +5 -0
  10. just_bash/commands/awk/awk.py +1168 -0
  11. just_bash/commands/base64/__init__.py +5 -0
  12. just_bash/commands/base64/base64.py +138 -0
  13. just_bash/commands/basename/__init__.py +5 -0
  14. just_bash/commands/basename/basename.py +72 -0
  15. just_bash/commands/bash/__init__.py +5 -0
  16. just_bash/commands/bash/bash.py +188 -0
  17. just_bash/commands/cat/__init__.py +5 -0
  18. just_bash/commands/cat/cat.py +173 -0
  19. just_bash/commands/checksum/__init__.py +5 -0
  20. just_bash/commands/checksum/checksum.py +179 -0
  21. just_bash/commands/chmod/__init__.py +5 -0
  22. just_bash/commands/chmod/chmod.py +216 -0
  23. just_bash/commands/column/__init__.py +5 -0
  24. just_bash/commands/column/column.py +180 -0
  25. just_bash/commands/comm/__init__.py +5 -0
  26. just_bash/commands/comm/comm.py +150 -0
  27. just_bash/commands/compression/__init__.py +5 -0
  28. just_bash/commands/compression/compression.py +298 -0
  29. just_bash/commands/cp/__init__.py +5 -0
  30. just_bash/commands/cp/cp.py +149 -0
  31. just_bash/commands/curl/__init__.py +5 -0
  32. just_bash/commands/curl/curl.py +801 -0
  33. just_bash/commands/cut/__init__.py +5 -0
  34. just_bash/commands/cut/cut.py +327 -0
  35. just_bash/commands/date/__init__.py +5 -0
  36. just_bash/commands/date/date.py +258 -0
  37. just_bash/commands/diff/__init__.py +5 -0
  38. just_bash/commands/diff/diff.py +118 -0
  39. just_bash/commands/dirname/__init__.py +5 -0
  40. just_bash/commands/dirname/dirname.py +56 -0
  41. just_bash/commands/du/__init__.py +5 -0
  42. just_bash/commands/du/du.py +150 -0
  43. just_bash/commands/echo/__init__.py +5 -0
  44. just_bash/commands/echo/echo.py +125 -0
  45. just_bash/commands/env/__init__.py +5 -0
  46. just_bash/commands/env/env.py +163 -0
  47. just_bash/commands/expand/__init__.py +5 -0
  48. just_bash/commands/expand/expand.py +299 -0
  49. just_bash/commands/expr/__init__.py +5 -0
  50. just_bash/commands/expr/expr.py +273 -0
  51. just_bash/commands/file/__init__.py +5 -0
  52. just_bash/commands/file/file.py +274 -0
  53. just_bash/commands/find/__init__.py +5 -0
  54. just_bash/commands/find/find.py +623 -0
  55. just_bash/commands/fold/__init__.py +5 -0
  56. just_bash/commands/fold/fold.py +160 -0
  57. just_bash/commands/grep/__init__.py +5 -0
  58. just_bash/commands/grep/grep.py +418 -0
  59. just_bash/commands/head/__init__.py +5 -0
  60. just_bash/commands/head/head.py +167 -0
  61. just_bash/commands/help/__init__.py +5 -0
  62. just_bash/commands/help/help.py +67 -0
  63. just_bash/commands/hostname/__init__.py +5 -0
  64. just_bash/commands/hostname/hostname.py +21 -0
  65. just_bash/commands/html_to_markdown/__init__.py +5 -0
  66. just_bash/commands/html_to_markdown/html_to_markdown.py +191 -0
  67. just_bash/commands/join/__init__.py +5 -0
  68. just_bash/commands/join/join.py +252 -0
  69. just_bash/commands/jq/__init__.py +5 -0
  70. just_bash/commands/jq/jq.py +280 -0
  71. just_bash/commands/ln/__init__.py +5 -0
  72. just_bash/commands/ln/ln.py +127 -0
  73. just_bash/commands/ls/__init__.py +5 -0
  74. just_bash/commands/ls/ls.py +280 -0
  75. just_bash/commands/mkdir/__init__.py +5 -0
  76. just_bash/commands/mkdir/mkdir.py +92 -0
  77. just_bash/commands/mv/__init__.py +5 -0
  78. just_bash/commands/mv/mv.py +142 -0
  79. just_bash/commands/nl/__init__.py +5 -0
  80. just_bash/commands/nl/nl.py +180 -0
  81. just_bash/commands/od/__init__.py +5 -0
  82. just_bash/commands/od/od.py +157 -0
  83. just_bash/commands/paste/__init__.py +5 -0
  84. just_bash/commands/paste/paste.py +100 -0
  85. just_bash/commands/printf/__init__.py +5 -0
  86. just_bash/commands/printf/printf.py +157 -0
  87. just_bash/commands/pwd/__init__.py +5 -0
  88. just_bash/commands/pwd/pwd.py +23 -0
  89. just_bash/commands/read/__init__.py +5 -0
  90. just_bash/commands/read/read.py +185 -0
  91. just_bash/commands/readlink/__init__.py +5 -0
  92. just_bash/commands/readlink/readlink.py +86 -0
  93. just_bash/commands/registry.py +844 -0
  94. just_bash/commands/rev/__init__.py +5 -0
  95. just_bash/commands/rev/rev.py +74 -0
  96. just_bash/commands/rg/__init__.py +5 -0
  97. just_bash/commands/rg/rg.py +1048 -0
  98. just_bash/commands/rm/__init__.py +5 -0
  99. just_bash/commands/rm/rm.py +106 -0
  100. just_bash/commands/search_engine/__init__.py +13 -0
  101. just_bash/commands/search_engine/matcher.py +170 -0
  102. just_bash/commands/search_engine/regex.py +159 -0
  103. just_bash/commands/sed/__init__.py +5 -0
  104. just_bash/commands/sed/sed.py +863 -0
  105. just_bash/commands/seq/__init__.py +5 -0
  106. just_bash/commands/seq/seq.py +190 -0
  107. just_bash/commands/shell/__init__.py +5 -0
  108. just_bash/commands/shell/shell.py +206 -0
  109. just_bash/commands/sleep/__init__.py +5 -0
  110. just_bash/commands/sleep/sleep.py +62 -0
  111. just_bash/commands/sort/__init__.py +5 -0
  112. just_bash/commands/sort/sort.py +411 -0
  113. just_bash/commands/split/__init__.py +5 -0
  114. just_bash/commands/split/split.py +237 -0
  115. just_bash/commands/sqlite3/__init__.py +5 -0
  116. just_bash/commands/sqlite3/sqlite3_cmd.py +505 -0
  117. just_bash/commands/stat/__init__.py +5 -0
  118. just_bash/commands/stat/stat.py +150 -0
  119. just_bash/commands/strings/__init__.py +5 -0
  120. just_bash/commands/strings/strings.py +150 -0
  121. just_bash/commands/tac/__init__.py +5 -0
  122. just_bash/commands/tac/tac.py +158 -0
  123. just_bash/commands/tail/__init__.py +5 -0
  124. just_bash/commands/tail/tail.py +180 -0
  125. just_bash/commands/tar/__init__.py +5 -0
  126. just_bash/commands/tar/tar.py +1067 -0
  127. just_bash/commands/tee/__init__.py +5 -0
  128. just_bash/commands/tee/tee.py +63 -0
  129. just_bash/commands/timeout/__init__.py +5 -0
  130. just_bash/commands/timeout/timeout.py +188 -0
  131. just_bash/commands/touch/__init__.py +5 -0
  132. just_bash/commands/touch/touch.py +91 -0
  133. just_bash/commands/tr/__init__.py +5 -0
  134. just_bash/commands/tr/tr.py +297 -0
  135. just_bash/commands/tree/__init__.py +5 -0
  136. just_bash/commands/tree/tree.py +139 -0
  137. just_bash/commands/true/__init__.py +5 -0
  138. just_bash/commands/true/true.py +32 -0
  139. just_bash/commands/uniq/__init__.py +5 -0
  140. just_bash/commands/uniq/uniq.py +323 -0
  141. just_bash/commands/wc/__init__.py +5 -0
  142. just_bash/commands/wc/wc.py +169 -0
  143. just_bash/commands/which/__init__.py +5 -0
  144. just_bash/commands/which/which.py +52 -0
  145. just_bash/commands/xan/__init__.py +5 -0
  146. just_bash/commands/xan/xan.py +1663 -0
  147. just_bash/commands/xargs/__init__.py +5 -0
  148. just_bash/commands/xargs/xargs.py +136 -0
  149. just_bash/commands/yq/__init__.py +5 -0
  150. just_bash/commands/yq/yq.py +848 -0
  151. just_bash/fs/__init__.py +29 -0
  152. just_bash/fs/in_memory_fs.py +621 -0
  153. just_bash/fs/mountable_fs.py +504 -0
  154. just_bash/fs/overlay_fs.py +894 -0
  155. just_bash/fs/read_write_fs.py +455 -0
  156. just_bash/interpreter/__init__.py +37 -0
  157. just_bash/interpreter/builtins/__init__.py +92 -0
  158. just_bash/interpreter/builtins/alias.py +154 -0
  159. just_bash/interpreter/builtins/cd.py +76 -0
  160. just_bash/interpreter/builtins/control.py +127 -0
  161. just_bash/interpreter/builtins/declare.py +336 -0
  162. just_bash/interpreter/builtins/export.py +56 -0
  163. just_bash/interpreter/builtins/let.py +44 -0
  164. just_bash/interpreter/builtins/local.py +57 -0
  165. just_bash/interpreter/builtins/mapfile.py +152 -0
  166. just_bash/interpreter/builtins/misc.py +378 -0
  167. just_bash/interpreter/builtins/readonly.py +80 -0
  168. just_bash/interpreter/builtins/set.py +234 -0
  169. just_bash/interpreter/builtins/shopt.py +201 -0
  170. just_bash/interpreter/builtins/source.py +136 -0
  171. just_bash/interpreter/builtins/test.py +290 -0
  172. just_bash/interpreter/builtins/unset.py +53 -0
  173. just_bash/interpreter/conditionals.py +387 -0
  174. just_bash/interpreter/control_flow.py +381 -0
  175. just_bash/interpreter/errors.py +116 -0
  176. just_bash/interpreter/expansion.py +1156 -0
  177. just_bash/interpreter/interpreter.py +813 -0
  178. just_bash/interpreter/types.py +134 -0
  179. just_bash/network/__init__.py +1 -0
  180. just_bash/parser/__init__.py +39 -0
  181. just_bash/parser/lexer.py +948 -0
  182. just_bash/parser/parser.py +2162 -0
  183. just_bash/py.typed +0 -0
  184. just_bash/query_engine/__init__.py +83 -0
  185. just_bash/query_engine/builtins/__init__.py +1283 -0
  186. just_bash/query_engine/evaluator.py +578 -0
  187. just_bash/query_engine/parser.py +525 -0
  188. just_bash/query_engine/tokenizer.py +329 -0
  189. just_bash/query_engine/types.py +373 -0
  190. just_bash/types.py +180 -0
  191. just_bash-0.1.5.dist-info/METADATA +410 -0
  192. just_bash-0.1.5.dist-info/RECORD +193 -0
  193. 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,5 @@
1
+ """True and false command implementations."""
2
+
3
+ from .true import TrueCommand, FalseCommand
4
+
5
+ __all__ = ["TrueCommand", "FalseCommand"]
@@ -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,5 @@
1
+ """Uniq command implementation."""
2
+
3
+ from .uniq import UniqCommand
4
+
5
+ __all__ = ["UniqCommand"]
@@ -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,5 @@
1
+ """Wc command implementation."""
2
+
3
+ from .wc import WcCommand
4
+
5
+ __all__ = ["WcCommand"]
@@ -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)
@@ -0,0 +1,5 @@
1
+ """Which command."""
2
+
3
+ from .which import WhichCommand
4
+
5
+ __all__ = ["WhichCommand"]