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,252 @@
|
|
|
1
|
+
"""Join command implementation."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class JoinCommand:
|
|
7
|
+
"""The join command - join lines of two files on a common field."""
|
|
8
|
+
|
|
9
|
+
name = "join"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the join command."""
|
|
13
|
+
field1 = 1
|
|
14
|
+
field2 = 1
|
|
15
|
+
separator = None
|
|
16
|
+
output_format = None
|
|
17
|
+
print_unpaired_1 = False
|
|
18
|
+
print_unpaired_2 = False
|
|
19
|
+
only_unpaired_1 = False
|
|
20
|
+
only_unpaired_2 = False
|
|
21
|
+
empty_replacement = None
|
|
22
|
+
ignore_case = False
|
|
23
|
+
files: list[str] = []
|
|
24
|
+
|
|
25
|
+
i = 0
|
|
26
|
+
while i < len(args):
|
|
27
|
+
arg = args[i]
|
|
28
|
+
if arg == "--help":
|
|
29
|
+
return ExecResult(
|
|
30
|
+
stdout="Usage: join [OPTION]... FILE1 FILE2\nJoin lines of two files on a common field.\n",
|
|
31
|
+
stderr="",
|
|
32
|
+
exit_code=0,
|
|
33
|
+
)
|
|
34
|
+
elif arg == "-1" and i + 1 < len(args):
|
|
35
|
+
i += 1
|
|
36
|
+
try:
|
|
37
|
+
field1 = int(args[i])
|
|
38
|
+
except ValueError:
|
|
39
|
+
return ExecResult(
|
|
40
|
+
stdout="",
|
|
41
|
+
stderr=f"join: invalid field number: '{args[i]}'\n",
|
|
42
|
+
exit_code=1,
|
|
43
|
+
)
|
|
44
|
+
elif arg == "-2" and i + 1 < len(args):
|
|
45
|
+
i += 1
|
|
46
|
+
try:
|
|
47
|
+
field2 = int(args[i])
|
|
48
|
+
except ValueError:
|
|
49
|
+
return ExecResult(
|
|
50
|
+
stdout="",
|
|
51
|
+
stderr=f"join: invalid field number: '{args[i]}'\n",
|
|
52
|
+
exit_code=1,
|
|
53
|
+
)
|
|
54
|
+
elif arg == "-t" and i + 1 < len(args):
|
|
55
|
+
i += 1
|
|
56
|
+
separator = args[i]
|
|
57
|
+
elif arg.startswith("-t"):
|
|
58
|
+
separator = arg[2:]
|
|
59
|
+
elif arg == "-a" and i + 1 < len(args):
|
|
60
|
+
i += 1
|
|
61
|
+
if args[i] == "1":
|
|
62
|
+
print_unpaired_1 = True
|
|
63
|
+
elif args[i] == "2":
|
|
64
|
+
print_unpaired_2 = True
|
|
65
|
+
elif arg == "-v" and i + 1 < len(args):
|
|
66
|
+
i += 1
|
|
67
|
+
if args[i] == "1":
|
|
68
|
+
only_unpaired_1 = True
|
|
69
|
+
elif args[i] == "2":
|
|
70
|
+
only_unpaired_2 = True
|
|
71
|
+
elif arg == "-e" and i + 1 < len(args):
|
|
72
|
+
i += 1
|
|
73
|
+
empty_replacement = args[i]
|
|
74
|
+
elif arg == "-o" and i + 1 < len(args):
|
|
75
|
+
i += 1
|
|
76
|
+
output_format = args[i]
|
|
77
|
+
elif arg in ("-i", "--ignore-case"):
|
|
78
|
+
ignore_case = True
|
|
79
|
+
elif arg == "--":
|
|
80
|
+
files.extend(args[i + 1:])
|
|
81
|
+
break
|
|
82
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
83
|
+
return ExecResult(
|
|
84
|
+
stdout="",
|
|
85
|
+
stderr=f"join: invalid option -- '{arg[1]}'\n",
|
|
86
|
+
exit_code=1,
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
files.append(arg)
|
|
90
|
+
i += 1
|
|
91
|
+
|
|
92
|
+
if len(files) < 2:
|
|
93
|
+
return ExecResult(
|
|
94
|
+
stdout="",
|
|
95
|
+
stderr="join: missing operand\n",
|
|
96
|
+
exit_code=1,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
file1, file2 = files[0], files[1]
|
|
100
|
+
|
|
101
|
+
# Read files
|
|
102
|
+
try:
|
|
103
|
+
if file1 == "-":
|
|
104
|
+
content1 = ctx.stdin
|
|
105
|
+
else:
|
|
106
|
+
path1 = ctx.fs.resolve_path(ctx.cwd, file1)
|
|
107
|
+
content1 = await ctx.fs.read_file(path1)
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
return ExecResult(
|
|
110
|
+
stdout="",
|
|
111
|
+
stderr=f"join: {file1}: No such file or directory\n",
|
|
112
|
+
exit_code=1,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if file2 == "-":
|
|
117
|
+
content2 = ctx.stdin
|
|
118
|
+
else:
|
|
119
|
+
path2 = ctx.fs.resolve_path(ctx.cwd, file2)
|
|
120
|
+
content2 = await ctx.fs.read_file(path2)
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
return ExecResult(
|
|
123
|
+
stdout="",
|
|
124
|
+
stderr=f"join: {file2}: No such file or directory\n",
|
|
125
|
+
exit_code=1,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
lines1 = content1.rstrip("\n").split("\n") if content1.strip() else []
|
|
129
|
+
lines2 = content2.rstrip("\n").split("\n") if content2.strip() else []
|
|
130
|
+
|
|
131
|
+
result = self._join(
|
|
132
|
+
lines1, lines2, field1, field2, separator, output_format,
|
|
133
|
+
print_unpaired_1, print_unpaired_2, only_unpaired_1, only_unpaired_2,
|
|
134
|
+
empty_replacement, ignore_case
|
|
135
|
+
)
|
|
136
|
+
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
137
|
+
|
|
138
|
+
def _join(
|
|
139
|
+
self,
|
|
140
|
+
lines1: list[str],
|
|
141
|
+
lines2: list[str],
|
|
142
|
+
field1: int,
|
|
143
|
+
field2: int,
|
|
144
|
+
separator: str | None,
|
|
145
|
+
output_format: str | None,
|
|
146
|
+
print_unpaired_1: bool,
|
|
147
|
+
print_unpaired_2: bool,
|
|
148
|
+
only_unpaired_1: bool,
|
|
149
|
+
only_unpaired_2: bool,
|
|
150
|
+
empty_replacement: str | None,
|
|
151
|
+
ignore_case: bool,
|
|
152
|
+
) -> str:
|
|
153
|
+
"""Join two lists of lines."""
|
|
154
|
+
sep = separator if separator else " "
|
|
155
|
+
|
|
156
|
+
# Parse lines into fields
|
|
157
|
+
def parse_line(line: str) -> list[str]:
|
|
158
|
+
if separator:
|
|
159
|
+
return line.split(separator)
|
|
160
|
+
return line.split()
|
|
161
|
+
|
|
162
|
+
def get_key(fields: list[str], field_num: int) -> str:
|
|
163
|
+
if field_num <= 0 or field_num > len(fields):
|
|
164
|
+
return ""
|
|
165
|
+
key = fields[field_num - 1]
|
|
166
|
+
return key.lower() if ignore_case else key
|
|
167
|
+
|
|
168
|
+
# Build index for file2
|
|
169
|
+
index2: dict[str, list[tuple[int, list[str]]]] = {}
|
|
170
|
+
for idx, line in enumerate(lines2):
|
|
171
|
+
fields = parse_line(line)
|
|
172
|
+
key = get_key(fields, field2)
|
|
173
|
+
if key not in index2:
|
|
174
|
+
index2[key] = []
|
|
175
|
+
index2[key].append((idx, fields))
|
|
176
|
+
|
|
177
|
+
result_lines = []
|
|
178
|
+
matched2 = set()
|
|
179
|
+
|
|
180
|
+
# Process file1
|
|
181
|
+
for line1 in lines1:
|
|
182
|
+
fields1 = parse_line(line1)
|
|
183
|
+
key1 = get_key(fields1, field1)
|
|
184
|
+
|
|
185
|
+
if key1 in index2:
|
|
186
|
+
for idx2, fields2 in index2[key1]:
|
|
187
|
+
matched2.add(idx2)
|
|
188
|
+
if not only_unpaired_1 and not only_unpaired_2:
|
|
189
|
+
output = self._format_output(
|
|
190
|
+
key1, fields1, fields2, field1, field2,
|
|
191
|
+
sep, output_format, empty_replacement
|
|
192
|
+
)
|
|
193
|
+
result_lines.append(output)
|
|
194
|
+
else:
|
|
195
|
+
if print_unpaired_1 or only_unpaired_1:
|
|
196
|
+
result_lines.append(sep.join(fields1))
|
|
197
|
+
|
|
198
|
+
# Print unmatched from file2
|
|
199
|
+
if print_unpaired_2 or only_unpaired_2:
|
|
200
|
+
for idx, line2 in enumerate(lines2):
|
|
201
|
+
if idx not in matched2:
|
|
202
|
+
fields2 = parse_line(line2)
|
|
203
|
+
result_lines.append(sep.join(fields2))
|
|
204
|
+
|
|
205
|
+
if result_lines:
|
|
206
|
+
return "\n".join(result_lines) + "\n"
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
def _format_output(
|
|
210
|
+
self,
|
|
211
|
+
key: str,
|
|
212
|
+
fields1: list[str],
|
|
213
|
+
fields2: list[str],
|
|
214
|
+
field1: int,
|
|
215
|
+
field2: int,
|
|
216
|
+
sep: str,
|
|
217
|
+
output_format: str | None,
|
|
218
|
+
empty_replacement: str | None,
|
|
219
|
+
) -> str:
|
|
220
|
+
"""Format the output line."""
|
|
221
|
+
if output_format:
|
|
222
|
+
# Parse output format like "1.1,2.2,1.3"
|
|
223
|
+
parts = []
|
|
224
|
+
for spec in output_format.split(","):
|
|
225
|
+
spec = spec.strip()
|
|
226
|
+
if spec == "0":
|
|
227
|
+
parts.append(key)
|
|
228
|
+
elif "." in spec:
|
|
229
|
+
file_num, field_num = spec.split(".")
|
|
230
|
+
file_num = int(file_num)
|
|
231
|
+
field_num = int(field_num)
|
|
232
|
+
if file_num == 1:
|
|
233
|
+
if field_num <= len(fields1):
|
|
234
|
+
parts.append(fields1[field_num - 1])
|
|
235
|
+
elif empty_replacement:
|
|
236
|
+
parts.append(empty_replacement)
|
|
237
|
+
elif file_num == 2:
|
|
238
|
+
if field_num <= len(fields2):
|
|
239
|
+
parts.append(fields2[field_num - 1])
|
|
240
|
+
elif empty_replacement:
|
|
241
|
+
parts.append(empty_replacement)
|
|
242
|
+
return sep.join(parts)
|
|
243
|
+
else:
|
|
244
|
+
# Default: key + other fields from both files
|
|
245
|
+
parts = [key]
|
|
246
|
+
for i, f in enumerate(fields1):
|
|
247
|
+
if i + 1 != field1:
|
|
248
|
+
parts.append(f)
|
|
249
|
+
for i, f in enumerate(fields2):
|
|
250
|
+
if i + 1 != field2:
|
|
251
|
+
parts.append(f)
|
|
252
|
+
return sep.join(parts)
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Jq command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: jq [OPTIONS] FILTER [FILE...]
|
|
4
|
+
|
|
5
|
+
JSON processor using jq-style expressions.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-r, --raw-output output strings without quotes
|
|
9
|
+
-c, --compact compact output (no pretty printing)
|
|
10
|
+
-s, --slurp read all inputs into an array
|
|
11
|
+
-e exit with 1 if last output is false or null
|
|
12
|
+
-n, --null-input don't read any input
|
|
13
|
+
-R, --raw-input read each line as a string
|
|
14
|
+
-j, --join-output no newlines between outputs
|
|
15
|
+
-S, --sort-keys sort object keys alphabetically
|
|
16
|
+
--tab use tabs for indentation
|
|
17
|
+
-a, --ascii-output escape non-ASCII characters
|
|
18
|
+
|
|
19
|
+
Filters:
|
|
20
|
+
. identity (output input unchanged)
|
|
21
|
+
.foo object field access
|
|
22
|
+
.foo.bar nested field access
|
|
23
|
+
.[N] array index access
|
|
24
|
+
.[] array/object iterator
|
|
25
|
+
.[N:M] array slice
|
|
26
|
+
| pipe (chain filters)
|
|
27
|
+
, output multiple values
|
|
28
|
+
select(expr) filter values
|
|
29
|
+
map(expr) apply expression to each element
|
|
30
|
+
keys get object keys
|
|
31
|
+
values get object values
|
|
32
|
+
length get length
|
|
33
|
+
type get type name
|
|
34
|
+
empty output nothing
|
|
35
|
+
add sum/concatenate
|
|
36
|
+
first, last first/last element
|
|
37
|
+
reverse reverse array
|
|
38
|
+
sort sort array
|
|
39
|
+
unique unique elements
|
|
40
|
+
flatten flatten nested arrays
|
|
41
|
+
group_by(expr) group by expression
|
|
42
|
+
min, max minimum/maximum
|
|
43
|
+
has(key) check if key exists
|
|
44
|
+
in(object) check if key is in object
|
|
45
|
+
contains(x) check if contains x
|
|
46
|
+
split(s) split string by s
|
|
47
|
+
join(s) join array by s
|
|
48
|
+
ascii_downcase lowercase
|
|
49
|
+
ascii_upcase uppercase
|
|
50
|
+
ltrimstr(s) remove prefix
|
|
51
|
+
rtrimstr(s) remove suffix
|
|
52
|
+
startswith(s) check prefix
|
|
53
|
+
endswith(s) check suffix
|
|
54
|
+
test(regex) regex match
|
|
55
|
+
@base64 encode to base64
|
|
56
|
+
@base64d decode from base64
|
|
57
|
+
@uri URI encode
|
|
58
|
+
@csv CSV format
|
|
59
|
+
@json JSON encode
|
|
60
|
+
@text convert to text
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
import json
|
|
64
|
+
from typing import Any
|
|
65
|
+
from ...types import CommandContext, ExecResult
|
|
66
|
+
from ...query_engine import parse, evaluate, EvalContext
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class JqCommand:
|
|
70
|
+
"""The jq command."""
|
|
71
|
+
|
|
72
|
+
name = "jq"
|
|
73
|
+
|
|
74
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
75
|
+
"""Execute the jq command."""
|
|
76
|
+
raw_output = False
|
|
77
|
+
compact = False
|
|
78
|
+
slurp = False
|
|
79
|
+
exit_on_false = False
|
|
80
|
+
null_input = False
|
|
81
|
+
raw_input = False
|
|
82
|
+
join_output = False
|
|
83
|
+
sort_keys = False
|
|
84
|
+
use_tabs = False
|
|
85
|
+
ascii_output = False
|
|
86
|
+
filter_str: str | None = None
|
|
87
|
+
files: list[str] = []
|
|
88
|
+
|
|
89
|
+
# Parse arguments
|
|
90
|
+
i = 0
|
|
91
|
+
while i < len(args):
|
|
92
|
+
arg = args[i]
|
|
93
|
+
if arg == "--":
|
|
94
|
+
files.extend(args[i + 1:])
|
|
95
|
+
break
|
|
96
|
+
elif arg in ("-r", "--raw-output"):
|
|
97
|
+
raw_output = True
|
|
98
|
+
elif arg in ("-c", "--compact"):
|
|
99
|
+
compact = True
|
|
100
|
+
elif arg in ("-s", "--slurp"):
|
|
101
|
+
slurp = True
|
|
102
|
+
elif arg == "-e":
|
|
103
|
+
exit_on_false = True
|
|
104
|
+
elif arg in ("-n", "--null-input"):
|
|
105
|
+
null_input = True
|
|
106
|
+
elif arg in ("-R", "--raw-input"):
|
|
107
|
+
raw_input = True
|
|
108
|
+
elif arg in ("-j", "--join-output"):
|
|
109
|
+
join_output = True
|
|
110
|
+
elif arg in ("-S", "--sort-keys"):
|
|
111
|
+
sort_keys = True
|
|
112
|
+
elif arg == "--tab":
|
|
113
|
+
use_tabs = True
|
|
114
|
+
elif arg in ("-a", "--ascii-output"):
|
|
115
|
+
ascii_output = True
|
|
116
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
117
|
+
# Combined flags
|
|
118
|
+
for c in arg[1:]:
|
|
119
|
+
if c == "r":
|
|
120
|
+
raw_output = True
|
|
121
|
+
elif c == "c":
|
|
122
|
+
compact = True
|
|
123
|
+
elif c == "s":
|
|
124
|
+
slurp = True
|
|
125
|
+
elif c == "e":
|
|
126
|
+
exit_on_false = True
|
|
127
|
+
elif c == "n":
|
|
128
|
+
null_input = True
|
|
129
|
+
elif c == "R":
|
|
130
|
+
raw_input = True
|
|
131
|
+
elif c == "j":
|
|
132
|
+
join_output = True
|
|
133
|
+
elif c == "S":
|
|
134
|
+
sort_keys = True
|
|
135
|
+
elif c == "a":
|
|
136
|
+
ascii_output = True
|
|
137
|
+
else:
|
|
138
|
+
return ExecResult(
|
|
139
|
+
stdout="",
|
|
140
|
+
stderr=f"jq: Unknown option: -{c}\n",
|
|
141
|
+
exit_code=2,
|
|
142
|
+
)
|
|
143
|
+
elif filter_str is None:
|
|
144
|
+
# First positional argument is the filter
|
|
145
|
+
filter_str = arg
|
|
146
|
+
else:
|
|
147
|
+
files.append(arg)
|
|
148
|
+
i += 1
|
|
149
|
+
|
|
150
|
+
# Default filter if none provided
|
|
151
|
+
if filter_str is None:
|
|
152
|
+
filter_str = "."
|
|
153
|
+
|
|
154
|
+
# Parse the filter using query engine
|
|
155
|
+
try:
|
|
156
|
+
ast = parse(filter_str)
|
|
157
|
+
except ValueError as e:
|
|
158
|
+
return ExecResult(
|
|
159
|
+
stdout="",
|
|
160
|
+
stderr=f"jq: {e}\n",
|
|
161
|
+
exit_code=2,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Get input
|
|
165
|
+
inputs: list[Any] = []
|
|
166
|
+
stderr = ""
|
|
167
|
+
|
|
168
|
+
if null_input:
|
|
169
|
+
inputs = [None]
|
|
170
|
+
elif not files:
|
|
171
|
+
files = ["-"]
|
|
172
|
+
|
|
173
|
+
for f in files:
|
|
174
|
+
try:
|
|
175
|
+
if f == "-":
|
|
176
|
+
content = ctx.stdin
|
|
177
|
+
else:
|
|
178
|
+
path = ctx.fs.resolve_path(ctx.cwd, f)
|
|
179
|
+
content = await ctx.fs.read_file(path)
|
|
180
|
+
|
|
181
|
+
if raw_input:
|
|
182
|
+
# Each line is a string
|
|
183
|
+
for line in content.split("\n"):
|
|
184
|
+
if line:
|
|
185
|
+
inputs.append(line)
|
|
186
|
+
else:
|
|
187
|
+
# Parse JSON
|
|
188
|
+
content = content.strip()
|
|
189
|
+
if content:
|
|
190
|
+
# Handle multiple JSON objects
|
|
191
|
+
decoder = json.JSONDecoder()
|
|
192
|
+
pos = 0
|
|
193
|
+
while pos < len(content):
|
|
194
|
+
# Skip whitespace
|
|
195
|
+
while pos < len(content) and content[pos] in " \t\n\r":
|
|
196
|
+
pos += 1
|
|
197
|
+
if pos >= len(content):
|
|
198
|
+
break
|
|
199
|
+
try:
|
|
200
|
+
obj, end = decoder.raw_decode(content, pos)
|
|
201
|
+
inputs.append(obj)
|
|
202
|
+
pos = end
|
|
203
|
+
except json.JSONDecodeError as e:
|
|
204
|
+
stderr += f"jq: parse error: {e}\n"
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
except FileNotFoundError:
|
|
208
|
+
stderr += f"jq: error: {f}: No such file or directory\n"
|
|
209
|
+
|
|
210
|
+
if stderr:
|
|
211
|
+
return ExecResult(stdout="", stderr=stderr, exit_code=2)
|
|
212
|
+
|
|
213
|
+
# Apply slurp
|
|
214
|
+
if slurp and not null_input:
|
|
215
|
+
inputs = [inputs]
|
|
216
|
+
|
|
217
|
+
# Create evaluation context
|
|
218
|
+
eval_ctx = EvalContext(env=dict(ctx.env))
|
|
219
|
+
|
|
220
|
+
# Apply filter using query engine
|
|
221
|
+
outputs: list[Any] = []
|
|
222
|
+
for inp in inputs:
|
|
223
|
+
try:
|
|
224
|
+
results = evaluate(inp, ast, eval_ctx)
|
|
225
|
+
outputs.extend(results)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
stderr += f"jq: error: {e}\n"
|
|
228
|
+
|
|
229
|
+
# Format output
|
|
230
|
+
output = ""
|
|
231
|
+
for val in outputs:
|
|
232
|
+
formatted = self._format_value(
|
|
233
|
+
val, raw_output, compact, sort_keys, use_tabs, ascii_output
|
|
234
|
+
)
|
|
235
|
+
if join_output:
|
|
236
|
+
output += formatted
|
|
237
|
+
else:
|
|
238
|
+
output += formatted + "\n"
|
|
239
|
+
|
|
240
|
+
# Determine exit code
|
|
241
|
+
exit_code = 0
|
|
242
|
+
if exit_on_false and outputs:
|
|
243
|
+
last = outputs[-1]
|
|
244
|
+
if last is None or last is False:
|
|
245
|
+
exit_code = 1
|
|
246
|
+
|
|
247
|
+
if stderr:
|
|
248
|
+
return ExecResult(stdout=output, stderr=stderr, exit_code=2)
|
|
249
|
+
|
|
250
|
+
return ExecResult(stdout=output, stderr="", exit_code=exit_code)
|
|
251
|
+
|
|
252
|
+
def _format_value(
|
|
253
|
+
self,
|
|
254
|
+
value: Any,
|
|
255
|
+
raw: bool,
|
|
256
|
+
compact: bool,
|
|
257
|
+
sort_keys: bool = False,
|
|
258
|
+
use_tabs: bool = False,
|
|
259
|
+
ascii_output: bool = False,
|
|
260
|
+
) -> str:
|
|
261
|
+
"""Format a value for output."""
|
|
262
|
+
if value is None:
|
|
263
|
+
return "null"
|
|
264
|
+
elif isinstance(value, bool):
|
|
265
|
+
return "true" if value else "false"
|
|
266
|
+
elif isinstance(value, str):
|
|
267
|
+
if raw:
|
|
268
|
+
return value
|
|
269
|
+
return json.dumps(value, ensure_ascii=ascii_output)
|
|
270
|
+
elif isinstance(value, (int, float)):
|
|
271
|
+
return json.dumps(value)
|
|
272
|
+
else:
|
|
273
|
+
indent_val = "\t" if use_tabs else (None if compact else 2)
|
|
274
|
+
return json.dumps(
|
|
275
|
+
value,
|
|
276
|
+
indent=indent_val,
|
|
277
|
+
separators=(",", ":") if compact else None,
|
|
278
|
+
sort_keys=sort_keys,
|
|
279
|
+
ensure_ascii=ascii_output,
|
|
280
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Ln command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: ln [OPTION]... TARGET LINK_NAME
|
|
4
|
+
|
|
5
|
+
Create a link to TARGET with the name LINK_NAME.
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
-s, --symbolic make symbolic links instead of hard links
|
|
9
|
+
-f, --force remove existing destination files
|
|
10
|
+
-v, --verbose print name of each linked file
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from ...types import CommandContext, ExecResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LnCommand:
|
|
17
|
+
"""The ln command."""
|
|
18
|
+
|
|
19
|
+
name = "ln"
|
|
20
|
+
|
|
21
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
22
|
+
"""Execute the ln command."""
|
|
23
|
+
symbolic = False
|
|
24
|
+
force = False
|
|
25
|
+
verbose = False
|
|
26
|
+
paths: list[str] = []
|
|
27
|
+
|
|
28
|
+
# Parse arguments
|
|
29
|
+
i = 0
|
|
30
|
+
while i < len(args):
|
|
31
|
+
arg = args[i]
|
|
32
|
+
if arg == "--":
|
|
33
|
+
paths.extend(args[i + 1:])
|
|
34
|
+
break
|
|
35
|
+
elif arg.startswith("--"):
|
|
36
|
+
if arg == "--symbolic":
|
|
37
|
+
symbolic = True
|
|
38
|
+
elif arg == "--force":
|
|
39
|
+
force = True
|
|
40
|
+
elif arg == "--verbose":
|
|
41
|
+
verbose = True
|
|
42
|
+
else:
|
|
43
|
+
return ExecResult(
|
|
44
|
+
stdout="",
|
|
45
|
+
stderr=f"ln: unrecognized option '{arg}'\n",
|
|
46
|
+
exit_code=1,
|
|
47
|
+
)
|
|
48
|
+
elif arg.startswith("-") and arg != "-":
|
|
49
|
+
for c in arg[1:]:
|
|
50
|
+
if c == "s":
|
|
51
|
+
symbolic = True
|
|
52
|
+
elif c == "f":
|
|
53
|
+
force = True
|
|
54
|
+
elif c == "v":
|
|
55
|
+
verbose = True
|
|
56
|
+
else:
|
|
57
|
+
return ExecResult(
|
|
58
|
+
stdout="",
|
|
59
|
+
stderr=f"ln: invalid option -- '{c}'\n",
|
|
60
|
+
exit_code=1,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
paths.append(arg)
|
|
64
|
+
i += 1
|
|
65
|
+
|
|
66
|
+
if len(paths) < 2:
|
|
67
|
+
if len(paths) == 0:
|
|
68
|
+
return ExecResult(
|
|
69
|
+
stdout="",
|
|
70
|
+
stderr="ln: missing file operand\n",
|
|
71
|
+
exit_code=1,
|
|
72
|
+
)
|
|
73
|
+
return ExecResult(
|
|
74
|
+
stdout="",
|
|
75
|
+
stderr=f"ln: missing destination file operand after '{paths[0]}'\n",
|
|
76
|
+
exit_code=1,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
target = paths[0]
|
|
80
|
+
link_name = paths[1]
|
|
81
|
+
|
|
82
|
+
target_path = ctx.fs.resolve_path(ctx.cwd, target)
|
|
83
|
+
link_path = ctx.fs.resolve_path(ctx.cwd, link_name)
|
|
84
|
+
|
|
85
|
+
stdout = ""
|
|
86
|
+
stderr = ""
|
|
87
|
+
exit_code = 0
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# For hard links, target must exist
|
|
91
|
+
if not symbolic:
|
|
92
|
+
try:
|
|
93
|
+
await ctx.fs.stat(target_path)
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
return ExecResult(
|
|
96
|
+
stdout="",
|
|
97
|
+
stderr=f"ln: failed to access '{target}': No such file or directory\n",
|
|
98
|
+
exit_code=1,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Remove existing link if force
|
|
102
|
+
if force:
|
|
103
|
+
try:
|
|
104
|
+
await ctx.fs.rm(link_path, recursive=False, force=True)
|
|
105
|
+
except (FileNotFoundError, IsADirectoryError):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Create the link
|
|
109
|
+
if symbolic:
|
|
110
|
+
await ctx.fs.symlink(target, link_path)
|
|
111
|
+
else:
|
|
112
|
+
await ctx.fs.link(target_path, link_path)
|
|
113
|
+
|
|
114
|
+
if verbose:
|
|
115
|
+
stdout += f"'{link_name}' -> '{target}'\n"
|
|
116
|
+
|
|
117
|
+
except FileExistsError:
|
|
118
|
+
stderr += f"ln: failed to create {'symbolic ' if symbolic else ''}link '{link_name}': File exists\n"
|
|
119
|
+
exit_code = 1
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
stderr += f"ln: failed to create {'symbolic ' if symbolic else ''}link '{link_name}': No such file or directory\n"
|
|
122
|
+
exit_code = 1
|
|
123
|
+
except OSError as e:
|
|
124
|
+
stderr += f"ln: failed to create link: {e}\n"
|
|
125
|
+
exit_code = 1
|
|
126
|
+
|
|
127
|
+
return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code)
|