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,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,5 @@
1
+ """Jq command implementation."""
2
+
3
+ from .jq import JqCommand
4
+
5
+ __all__ = ["JqCommand"]
@@ -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,5 @@
1
+ """Ln command implementation."""
2
+
3
+ from .ln import LnCommand
4
+
5
+ __all__ = ["LnCommand"]
@@ -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)
@@ -0,0 +1,5 @@
1
+ """Ls command implementation."""
2
+
3
+ from .ls import LsCommand
4
+
5
+ __all__ = ["LsCommand"]