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,299 @@
|
|
|
1
|
+
"""Expand and unexpand command implementations."""
|
|
2
|
+
|
|
3
|
+
from ...types import CommandContext, ExecResult
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExpandCommand:
|
|
7
|
+
"""The expand command - convert tabs to spaces."""
|
|
8
|
+
|
|
9
|
+
name = "expand"
|
|
10
|
+
|
|
11
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
12
|
+
"""Execute the expand command."""
|
|
13
|
+
tab_width = 8
|
|
14
|
+
tab_stops: list[int] = []
|
|
15
|
+
initial_only = False
|
|
16
|
+
files: list[str] = []
|
|
17
|
+
|
|
18
|
+
i = 0
|
|
19
|
+
while i < len(args):
|
|
20
|
+
arg = args[i]
|
|
21
|
+
if arg == "--help":
|
|
22
|
+
return ExecResult(
|
|
23
|
+
stdout="Usage: expand [OPTION]... [FILE]...\nConvert tabs to spaces.\n",
|
|
24
|
+
stderr="",
|
|
25
|
+
exit_code=0,
|
|
26
|
+
)
|
|
27
|
+
elif arg in ("-i", "--initial"):
|
|
28
|
+
initial_only = True
|
|
29
|
+
elif arg == "-t" and i + 1 < len(args):
|
|
30
|
+
i += 1
|
|
31
|
+
tab_stops = self._parse_tab_stops(args[i])
|
|
32
|
+
if tab_stops:
|
|
33
|
+
tab_width = tab_stops[0]
|
|
34
|
+
elif arg.startswith("-t"):
|
|
35
|
+
tab_stops = self._parse_tab_stops(arg[2:])
|
|
36
|
+
if tab_stops:
|
|
37
|
+
tab_width = tab_stops[0]
|
|
38
|
+
elif arg.startswith("--tabs="):
|
|
39
|
+
tab_stops = self._parse_tab_stops(arg[7:])
|
|
40
|
+
if tab_stops:
|
|
41
|
+
tab_width = tab_stops[0]
|
|
42
|
+
elif arg == "--":
|
|
43
|
+
files.extend(args[i + 1:])
|
|
44
|
+
break
|
|
45
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
46
|
+
# Could be -N for tab width
|
|
47
|
+
try:
|
|
48
|
+
tab_width = int(arg[1:])
|
|
49
|
+
except ValueError:
|
|
50
|
+
return ExecResult(
|
|
51
|
+
stdout="",
|
|
52
|
+
stderr=f"expand: invalid option -- '{arg[1]}'\n",
|
|
53
|
+
exit_code=1,
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
files.append(arg)
|
|
57
|
+
i += 1
|
|
58
|
+
|
|
59
|
+
if tab_width <= 0:
|
|
60
|
+
return ExecResult(
|
|
61
|
+
stdout="",
|
|
62
|
+
stderr="expand: tab size must be greater than 0\n",
|
|
63
|
+
exit_code=1,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Read from stdin if no files
|
|
67
|
+
if not files:
|
|
68
|
+
content = ctx.stdin
|
|
69
|
+
result = self._expand_content(content, tab_width, tab_stops, initial_only)
|
|
70
|
+
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
71
|
+
|
|
72
|
+
stdout_parts = []
|
|
73
|
+
stderr = ""
|
|
74
|
+
exit_code = 0
|
|
75
|
+
|
|
76
|
+
for file in files:
|
|
77
|
+
try:
|
|
78
|
+
if file == "-":
|
|
79
|
+
content = ctx.stdin
|
|
80
|
+
else:
|
|
81
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
82
|
+
content = await ctx.fs.read_file(path)
|
|
83
|
+
|
|
84
|
+
result = self._expand_content(content, tab_width, tab_stops, initial_only)
|
|
85
|
+
stdout_parts.append(result)
|
|
86
|
+
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
stderr += f"expand: {file}: No such file or directory\n"
|
|
89
|
+
exit_code = 1
|
|
90
|
+
|
|
91
|
+
return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
|
|
92
|
+
|
|
93
|
+
def _parse_tab_stops(self, s: str) -> list[int]:
|
|
94
|
+
"""Parse tab stop specification."""
|
|
95
|
+
if not s:
|
|
96
|
+
return []
|
|
97
|
+
try:
|
|
98
|
+
if "," in s:
|
|
99
|
+
return [int(x) for x in s.split(",") if x]
|
|
100
|
+
return [int(s)]
|
|
101
|
+
except ValueError:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
def _expand_content(
|
|
105
|
+
self, content: str, tab_width: int, tab_stops: list[int], initial_only: bool
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Expand tabs in content."""
|
|
108
|
+
lines = content.split("\n")
|
|
109
|
+
result_lines = []
|
|
110
|
+
|
|
111
|
+
for line in lines:
|
|
112
|
+
result_lines.append(self._expand_line(line, tab_width, tab_stops, initial_only))
|
|
113
|
+
|
|
114
|
+
return "\n".join(result_lines)
|
|
115
|
+
|
|
116
|
+
def _expand_line(
|
|
117
|
+
self, line: str, tab_width: int, tab_stops: list[int], initial_only: bool
|
|
118
|
+
) -> str:
|
|
119
|
+
"""Expand tabs in a single line."""
|
|
120
|
+
result = []
|
|
121
|
+
column = 0
|
|
122
|
+
in_initial = True
|
|
123
|
+
|
|
124
|
+
for char in line:
|
|
125
|
+
if char == "\t":
|
|
126
|
+
if initial_only and not in_initial:
|
|
127
|
+
result.append(char)
|
|
128
|
+
column += 1
|
|
129
|
+
else:
|
|
130
|
+
# Calculate spaces needed to reach next tab stop
|
|
131
|
+
if tab_stops and len(tab_stops) > 1:
|
|
132
|
+
# Find next tab stop
|
|
133
|
+
next_stop = None
|
|
134
|
+
for stop in tab_stops:
|
|
135
|
+
if stop > column:
|
|
136
|
+
next_stop = stop
|
|
137
|
+
break
|
|
138
|
+
if next_stop is None:
|
|
139
|
+
# Use last interval
|
|
140
|
+
interval = tab_stops[-1] - tab_stops[-2] if len(tab_stops) > 1 else tab_width
|
|
141
|
+
next_stop = column + interval - (column - tab_stops[-1]) % interval
|
|
142
|
+
spaces = next_stop - column
|
|
143
|
+
else:
|
|
144
|
+
spaces = tab_width - (column % tab_width)
|
|
145
|
+
result.append(" " * spaces)
|
|
146
|
+
column += spaces
|
|
147
|
+
else:
|
|
148
|
+
if char != " ":
|
|
149
|
+
in_initial = False
|
|
150
|
+
result.append(char)
|
|
151
|
+
column += 1
|
|
152
|
+
|
|
153
|
+
return "".join(result)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class UnexpandCommand:
|
|
157
|
+
"""The unexpand command - convert spaces to tabs."""
|
|
158
|
+
|
|
159
|
+
name = "unexpand"
|
|
160
|
+
|
|
161
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
162
|
+
"""Execute the unexpand command."""
|
|
163
|
+
tab_width = 8
|
|
164
|
+
all_spaces = False
|
|
165
|
+
files: list[str] = []
|
|
166
|
+
|
|
167
|
+
i = 0
|
|
168
|
+
while i < len(args):
|
|
169
|
+
arg = args[i]
|
|
170
|
+
if arg == "--help":
|
|
171
|
+
return ExecResult(
|
|
172
|
+
stdout="Usage: unexpand [OPTION]... [FILE]...\nConvert spaces to tabs.\n",
|
|
173
|
+
stderr="",
|
|
174
|
+
exit_code=0,
|
|
175
|
+
)
|
|
176
|
+
elif arg in ("-a", "--all"):
|
|
177
|
+
all_spaces = True
|
|
178
|
+
elif arg == "-t" and i + 1 < len(args):
|
|
179
|
+
i += 1
|
|
180
|
+
try:
|
|
181
|
+
tab_width = int(args[i])
|
|
182
|
+
except ValueError:
|
|
183
|
+
return ExecResult(
|
|
184
|
+
stdout="",
|
|
185
|
+
stderr=f"unexpand: invalid tab size: '{args[i]}'\n",
|
|
186
|
+
exit_code=1,
|
|
187
|
+
)
|
|
188
|
+
elif arg.startswith("-t"):
|
|
189
|
+
try:
|
|
190
|
+
tab_width = int(arg[2:])
|
|
191
|
+
except ValueError:
|
|
192
|
+
return ExecResult(
|
|
193
|
+
stdout="",
|
|
194
|
+
stderr=f"unexpand: invalid tab size: '{arg[2:]}'\n",
|
|
195
|
+
exit_code=1,
|
|
196
|
+
)
|
|
197
|
+
elif arg.startswith("--tabs="):
|
|
198
|
+
try:
|
|
199
|
+
tab_width = int(arg[7:])
|
|
200
|
+
except ValueError:
|
|
201
|
+
return ExecResult(
|
|
202
|
+
stdout="",
|
|
203
|
+
stderr=f"unexpand: invalid tab size: '{arg[7:]}'\n",
|
|
204
|
+
exit_code=1,
|
|
205
|
+
)
|
|
206
|
+
elif arg == "--":
|
|
207
|
+
files.extend(args[i + 1:])
|
|
208
|
+
break
|
|
209
|
+
elif arg.startswith("-") and len(arg) > 1:
|
|
210
|
+
return ExecResult(
|
|
211
|
+
stdout="",
|
|
212
|
+
stderr=f"unexpand: invalid option -- '{arg[1]}'\n",
|
|
213
|
+
exit_code=1,
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
files.append(arg)
|
|
217
|
+
i += 1
|
|
218
|
+
|
|
219
|
+
if tab_width <= 0:
|
|
220
|
+
return ExecResult(
|
|
221
|
+
stdout="",
|
|
222
|
+
stderr="unexpand: tab size must be greater than 0\n",
|
|
223
|
+
exit_code=1,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Read from stdin if no files
|
|
227
|
+
if not files:
|
|
228
|
+
content = ctx.stdin
|
|
229
|
+
result = self._unexpand_content(content, tab_width, all_spaces)
|
|
230
|
+
return ExecResult(stdout=result, stderr="", exit_code=0)
|
|
231
|
+
|
|
232
|
+
stdout_parts = []
|
|
233
|
+
stderr = ""
|
|
234
|
+
exit_code = 0
|
|
235
|
+
|
|
236
|
+
for file in files:
|
|
237
|
+
try:
|
|
238
|
+
if file == "-":
|
|
239
|
+
content = ctx.stdin
|
|
240
|
+
else:
|
|
241
|
+
path = ctx.fs.resolve_path(ctx.cwd, file)
|
|
242
|
+
content = await ctx.fs.read_file(path)
|
|
243
|
+
|
|
244
|
+
result = self._unexpand_content(content, tab_width, all_spaces)
|
|
245
|
+
stdout_parts.append(result)
|
|
246
|
+
|
|
247
|
+
except FileNotFoundError:
|
|
248
|
+
stderr += f"unexpand: {file}: No such file or directory\n"
|
|
249
|
+
exit_code = 1
|
|
250
|
+
|
|
251
|
+
return ExecResult(stdout="".join(stdout_parts), stderr=stderr, exit_code=exit_code)
|
|
252
|
+
|
|
253
|
+
def _unexpand_content(self, content: str, tab_width: int, all_spaces: bool) -> str:
|
|
254
|
+
"""Unexpand spaces in content."""
|
|
255
|
+
lines = content.split("\n")
|
|
256
|
+
result_lines = []
|
|
257
|
+
|
|
258
|
+
for line in lines:
|
|
259
|
+
result_lines.append(self._unexpand_line(line, tab_width, all_spaces))
|
|
260
|
+
|
|
261
|
+
return "\n".join(result_lines)
|
|
262
|
+
|
|
263
|
+
def _unexpand_line(self, line: str, tab_width: int, all_spaces: bool) -> str:
|
|
264
|
+
"""Unexpand spaces in a single line."""
|
|
265
|
+
if not line:
|
|
266
|
+
return line
|
|
267
|
+
|
|
268
|
+
result = []
|
|
269
|
+
space_count = 0
|
|
270
|
+
column = 0
|
|
271
|
+
in_leading = True
|
|
272
|
+
|
|
273
|
+
for char in line:
|
|
274
|
+
if char == " ":
|
|
275
|
+
space_count += 1
|
|
276
|
+
column += 1
|
|
277
|
+
|
|
278
|
+
# Check if we've reached a tab stop
|
|
279
|
+
if column % tab_width == 0:
|
|
280
|
+
if in_leading or all_spaces:
|
|
281
|
+
result.append("\t")
|
|
282
|
+
else:
|
|
283
|
+
result.append(" " * space_count)
|
|
284
|
+
space_count = 0
|
|
285
|
+
else:
|
|
286
|
+
# Flush pending spaces
|
|
287
|
+
if space_count > 0:
|
|
288
|
+
result.append(" " * space_count)
|
|
289
|
+
space_count = 0
|
|
290
|
+
result.append(char)
|
|
291
|
+
column += 1
|
|
292
|
+
if char != " ":
|
|
293
|
+
in_leading = False
|
|
294
|
+
|
|
295
|
+
# Flush any remaining spaces
|
|
296
|
+
if space_count > 0:
|
|
297
|
+
result.append(" " * space_count)
|
|
298
|
+
|
|
299
|
+
return "".join(result)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Expr command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: expr EXPRESSION
|
|
4
|
+
|
|
5
|
+
Print the value of EXPRESSION to standard output.
|
|
6
|
+
|
|
7
|
+
EXPRESSION may be:
|
|
8
|
+
ARG1 | ARG2 ARG1 if it is neither null nor 0, otherwise ARG2
|
|
9
|
+
ARG1 & ARG2 ARG1 if neither argument is null or 0, otherwise 0
|
|
10
|
+
ARG1 < ARG2 ARG1 is less than ARG2
|
|
11
|
+
ARG1 <= ARG2 ARG1 is less than or equal to ARG2
|
|
12
|
+
ARG1 = ARG2 ARG1 is equal to ARG2
|
|
13
|
+
ARG1 != ARG2 ARG1 is not equal to ARG2
|
|
14
|
+
ARG1 >= ARG2 ARG1 is greater than or equal to ARG2
|
|
15
|
+
ARG1 > ARG2 ARG1 is greater than ARG2
|
|
16
|
+
ARG1 + ARG2 arithmetic sum of ARG1 and ARG2
|
|
17
|
+
ARG1 - ARG2 arithmetic difference of ARG1 and ARG2
|
|
18
|
+
ARG1 * ARG2 arithmetic product of ARG1 and ARG2
|
|
19
|
+
ARG1 / ARG2 arithmetic quotient of ARG1 divided by ARG2
|
|
20
|
+
ARG1 % ARG2 arithmetic remainder of ARG1 divided by ARG2
|
|
21
|
+
STRING : REGEXP anchored pattern match of REGEXP in STRING
|
|
22
|
+
match STRING REGEXP same as STRING : REGEXP
|
|
23
|
+
substr STRING POS LENGTH substring of STRING, POS counted from 1
|
|
24
|
+
index STRING CHARS index in STRING where any CHARS is found, or 0
|
|
25
|
+
length STRING length of STRING
|
|
26
|
+
+ TOKEN interpret TOKEN as a string
|
|
27
|
+
( EXPRESSION ) value of EXPRESSION
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from ...types import CommandContext, ExecResult
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ExprCommand:
|
|
35
|
+
"""The expr command."""
|
|
36
|
+
|
|
37
|
+
name = "expr"
|
|
38
|
+
|
|
39
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
40
|
+
"""Execute the expr command."""
|
|
41
|
+
if not args:
|
|
42
|
+
return ExecResult(
|
|
43
|
+
stdout="",
|
|
44
|
+
stderr="expr: missing operand\n",
|
|
45
|
+
exit_code=2,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
result, _ = self._evaluate(args, 0)
|
|
50
|
+
# Exit code is 1 if result is empty or 0
|
|
51
|
+
exit_code = 1 if result == "" or result == "0" else 0
|
|
52
|
+
return ExecResult(stdout=str(result) + "\n", stderr="", exit_code=exit_code)
|
|
53
|
+
except ValueError as e:
|
|
54
|
+
return ExecResult(stdout="", stderr=f"expr: {e}\n", exit_code=2)
|
|
55
|
+
except ZeroDivisionError:
|
|
56
|
+
return ExecResult(stdout="", stderr="expr: division by zero\n", exit_code=2)
|
|
57
|
+
|
|
58
|
+
def _evaluate(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
59
|
+
"""Evaluate expression starting at position."""
|
|
60
|
+
return self._parse_or(args, pos)
|
|
61
|
+
|
|
62
|
+
def _parse_or(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
63
|
+
"""Parse OR expression: ARG1 | ARG2."""
|
|
64
|
+
left, pos = self._parse_and(args, pos)
|
|
65
|
+
|
|
66
|
+
while pos < len(args) and args[pos] == "|":
|
|
67
|
+
pos += 1
|
|
68
|
+
right, pos = self._parse_and(args, pos)
|
|
69
|
+
if left != "" and left != "0":
|
|
70
|
+
pass # keep left
|
|
71
|
+
else:
|
|
72
|
+
left = right
|
|
73
|
+
|
|
74
|
+
return left, pos
|
|
75
|
+
|
|
76
|
+
def _parse_and(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
77
|
+
"""Parse AND expression: ARG1 & ARG2."""
|
|
78
|
+
left, pos = self._parse_comparison(args, pos)
|
|
79
|
+
|
|
80
|
+
while pos < len(args) and args[pos] == "&":
|
|
81
|
+
pos += 1
|
|
82
|
+
right, pos = self._parse_comparison(args, pos)
|
|
83
|
+
if (left == "" or left == "0") or (right == "" or right == "0"):
|
|
84
|
+
left = "0"
|
|
85
|
+
# else keep left
|
|
86
|
+
|
|
87
|
+
return left, pos
|
|
88
|
+
|
|
89
|
+
def _parse_comparison(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
90
|
+
"""Parse comparison: <, <=, =, !=, >=, >."""
|
|
91
|
+
left, pos = self._parse_additive(args, pos)
|
|
92
|
+
|
|
93
|
+
if pos < len(args) and args[pos] in ("<", "<=", "=", "!=", ">=", ">"):
|
|
94
|
+
op = args[pos]
|
|
95
|
+
pos += 1
|
|
96
|
+
right, pos = self._parse_additive(args, pos)
|
|
97
|
+
|
|
98
|
+
# Try numeric comparison first
|
|
99
|
+
try:
|
|
100
|
+
l_num = int(left)
|
|
101
|
+
r_num = int(right)
|
|
102
|
+
if op == "<":
|
|
103
|
+
result = l_num < r_num
|
|
104
|
+
elif op == "<=":
|
|
105
|
+
result = l_num <= r_num
|
|
106
|
+
elif op == "=":
|
|
107
|
+
result = l_num == r_num
|
|
108
|
+
elif op == "!=":
|
|
109
|
+
result = l_num != r_num
|
|
110
|
+
elif op == ">=":
|
|
111
|
+
result = l_num >= r_num
|
|
112
|
+
elif op == ">":
|
|
113
|
+
result = l_num > r_num
|
|
114
|
+
except ValueError:
|
|
115
|
+
# String comparison
|
|
116
|
+
if op == "<":
|
|
117
|
+
result = left < right
|
|
118
|
+
elif op == "<=":
|
|
119
|
+
result = left <= right
|
|
120
|
+
elif op == "=":
|
|
121
|
+
result = left == right
|
|
122
|
+
elif op == "!=":
|
|
123
|
+
result = left != right
|
|
124
|
+
elif op == ">=":
|
|
125
|
+
result = left >= right
|
|
126
|
+
elif op == ">":
|
|
127
|
+
result = left > right
|
|
128
|
+
|
|
129
|
+
left = "1" if result else "0"
|
|
130
|
+
|
|
131
|
+
return left, pos
|
|
132
|
+
|
|
133
|
+
def _parse_additive(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
134
|
+
"""Parse additive: + and -."""
|
|
135
|
+
left, pos = self._parse_multiplicative(args, pos)
|
|
136
|
+
|
|
137
|
+
while pos < len(args) and args[pos] in ("+", "-"):
|
|
138
|
+
op = args[pos]
|
|
139
|
+
pos += 1
|
|
140
|
+
right, pos = self._parse_multiplicative(args, pos)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
l_num = int(left)
|
|
144
|
+
r_num = int(right)
|
|
145
|
+
if op == "+":
|
|
146
|
+
left = str(l_num + r_num)
|
|
147
|
+
else:
|
|
148
|
+
left = str(l_num - r_num)
|
|
149
|
+
except ValueError:
|
|
150
|
+
raise ValueError("non-integer argument")
|
|
151
|
+
|
|
152
|
+
return left, pos
|
|
153
|
+
|
|
154
|
+
def _parse_multiplicative(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
155
|
+
"""Parse multiplicative: *, /, %."""
|
|
156
|
+
left, pos = self._parse_match(args, pos)
|
|
157
|
+
|
|
158
|
+
while pos < len(args) and args[pos] in ("*", "/", "%"):
|
|
159
|
+
op = args[pos]
|
|
160
|
+
pos += 1
|
|
161
|
+
right, pos = self._parse_match(args, pos)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
l_num = int(left)
|
|
165
|
+
r_num = int(right)
|
|
166
|
+
if op == "*":
|
|
167
|
+
left = str(l_num * r_num)
|
|
168
|
+
elif op == "/":
|
|
169
|
+
left = str(l_num // r_num)
|
|
170
|
+
else:
|
|
171
|
+
left = str(l_num % r_num)
|
|
172
|
+
except ValueError:
|
|
173
|
+
raise ValueError("non-integer argument")
|
|
174
|
+
|
|
175
|
+
return left, pos
|
|
176
|
+
|
|
177
|
+
def _parse_match(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
178
|
+
"""Parse match expression: STRING : REGEXP."""
|
|
179
|
+
left, pos = self._parse_primary(args, pos)
|
|
180
|
+
|
|
181
|
+
if pos < len(args) and args[pos] == ":":
|
|
182
|
+
pos += 1
|
|
183
|
+
right, pos = self._parse_primary(args, pos)
|
|
184
|
+
# Anchored match at start
|
|
185
|
+
pattern = "^(" + right + ")"
|
|
186
|
+
try:
|
|
187
|
+
match = re.match(pattern, left)
|
|
188
|
+
if match:
|
|
189
|
+
if match.groups():
|
|
190
|
+
left = match.group(1)
|
|
191
|
+
else:
|
|
192
|
+
left = str(len(match.group(0)))
|
|
193
|
+
else:
|
|
194
|
+
left = ""
|
|
195
|
+
except re.error as e:
|
|
196
|
+
raise ValueError(f"invalid pattern: {e}")
|
|
197
|
+
|
|
198
|
+
return left, pos
|
|
199
|
+
|
|
200
|
+
def _parse_primary(self, args: list[str], pos: int) -> tuple[str, int]:
|
|
201
|
+
"""Parse primary expression."""
|
|
202
|
+
if pos >= len(args):
|
|
203
|
+
raise ValueError("missing operand")
|
|
204
|
+
|
|
205
|
+
token = args[pos]
|
|
206
|
+
|
|
207
|
+
# Parentheses
|
|
208
|
+
if token == "(":
|
|
209
|
+
pos += 1
|
|
210
|
+
result, pos = self._evaluate(args, pos)
|
|
211
|
+
if pos >= len(args) or args[pos] != ")":
|
|
212
|
+
raise ValueError("unmatched '('")
|
|
213
|
+
pos += 1
|
|
214
|
+
return result, pos
|
|
215
|
+
|
|
216
|
+
# Built-in functions
|
|
217
|
+
if token == "match" and pos + 2 < len(args):
|
|
218
|
+
pos += 1
|
|
219
|
+
string = args[pos]
|
|
220
|
+
pos += 1
|
|
221
|
+
pattern = args[pos]
|
|
222
|
+
pos += 1
|
|
223
|
+
# Anchored match
|
|
224
|
+
try:
|
|
225
|
+
match = re.match("^(" + pattern + ")", string)
|
|
226
|
+
if match:
|
|
227
|
+
if match.groups():
|
|
228
|
+
return match.group(1), pos
|
|
229
|
+
return str(len(match.group(0))), pos
|
|
230
|
+
return "", pos
|
|
231
|
+
except re.error as e:
|
|
232
|
+
raise ValueError(f"invalid pattern: {e}")
|
|
233
|
+
|
|
234
|
+
if token == "substr" and pos + 3 < len(args):
|
|
235
|
+
pos += 1
|
|
236
|
+
string = args[pos]
|
|
237
|
+
pos += 1
|
|
238
|
+
try:
|
|
239
|
+
start = int(args[pos]) - 1 # 1-indexed
|
|
240
|
+
pos += 1
|
|
241
|
+
length = int(args[pos])
|
|
242
|
+
pos += 1
|
|
243
|
+
if start < 0:
|
|
244
|
+
start = 0
|
|
245
|
+
return string[start:start + length], pos
|
|
246
|
+
except ValueError:
|
|
247
|
+
raise ValueError("non-integer argument")
|
|
248
|
+
|
|
249
|
+
if token == "index" and pos + 2 < len(args):
|
|
250
|
+
pos += 1
|
|
251
|
+
string = args[pos]
|
|
252
|
+
pos += 1
|
|
253
|
+
chars = args[pos]
|
|
254
|
+
pos += 1
|
|
255
|
+
for i, c in enumerate(string):
|
|
256
|
+
if c in chars:
|
|
257
|
+
return str(i + 1), pos # 1-indexed
|
|
258
|
+
return "0", pos
|
|
259
|
+
|
|
260
|
+
if token == "length" and pos + 1 < len(args):
|
|
261
|
+
pos += 1
|
|
262
|
+
string = args[pos]
|
|
263
|
+
pos += 1
|
|
264
|
+
return str(len(string)), pos
|
|
265
|
+
|
|
266
|
+
# Quote next token as string
|
|
267
|
+
if token == "+":
|
|
268
|
+
pos += 1
|
|
269
|
+
if pos >= len(args):
|
|
270
|
+
raise ValueError("missing operand after '+'")
|
|
271
|
+
return args[pos], pos + 1
|
|
272
|
+
|
|
273
|
+
return token, pos + 1
|