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,387 @@
|
|
|
1
|
+
"""Conditional Expression Evaluation.
|
|
2
|
+
|
|
3
|
+
Handles:
|
|
4
|
+
- [[ ... ]] conditional commands
|
|
5
|
+
- File tests (-f, -d, -e, etc.)
|
|
6
|
+
- String tests (-z, -n, =, !=)
|
|
7
|
+
- Numeric comparisons (-eq, -ne, -lt, etc.)
|
|
8
|
+
- Pattern matching (==, =~)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import fnmatch
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING, Union
|
|
14
|
+
|
|
15
|
+
from ..ast.types import (
|
|
16
|
+
CondBinaryNode,
|
|
17
|
+
CondUnaryNode,
|
|
18
|
+
CondNotNode,
|
|
19
|
+
CondAndNode,
|
|
20
|
+
CondOrNode,
|
|
21
|
+
CondGroupNode,
|
|
22
|
+
CondWordNode,
|
|
23
|
+
ConditionalExpressionNode,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .types import InterpreterContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# File test operators
|
|
31
|
+
FILE_TEST_OPS = {"-f", "-d", "-e", "-s", "-r", "-w", "-x", "-h", "-L", "-p", "-S", "-b", "-c", "-t", "-O", "-G", "-u", "-g", "-k", "-N"}
|
|
32
|
+
|
|
33
|
+
# Binary file test operators
|
|
34
|
+
BINARY_FILE_TEST_OPS = {"-nt", "-ot", "-ef"}
|
|
35
|
+
|
|
36
|
+
# String test operators
|
|
37
|
+
STRING_TEST_OPS = {"-z", "-n"}
|
|
38
|
+
|
|
39
|
+
# String comparison operators
|
|
40
|
+
STRING_COMPARE_OPS = {"=", "==", "!="}
|
|
41
|
+
|
|
42
|
+
# Numeric comparison operators
|
|
43
|
+
NUMERIC_OPS = {"-eq", "-ne", "-lt", "-le", "-gt", "-ge"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def evaluate_conditional(
|
|
47
|
+
ctx: "InterpreterContext",
|
|
48
|
+
expr: ConditionalExpressionNode,
|
|
49
|
+
) -> bool:
|
|
50
|
+
"""Evaluate a conditional expression from [[ ... ]]."""
|
|
51
|
+
from .expansion import expand_word_async
|
|
52
|
+
|
|
53
|
+
if isinstance(expr, CondBinaryNode):
|
|
54
|
+
left = await expand_word_async(ctx, expr.left) if expr.left else ""
|
|
55
|
+
right = await expand_word_async(ctx, expr.right) if expr.right else ""
|
|
56
|
+
|
|
57
|
+
# Check if RHS is fully quoted (should be treated literally, not as pattern)
|
|
58
|
+
is_rhs_quoted = False
|
|
59
|
+
if expr.right and expr.right.parts:
|
|
60
|
+
is_rhs_quoted = all(
|
|
61
|
+
p.type in ("SingleQuoted", "DoubleQuoted", "Escaped")
|
|
62
|
+
for p in expr.right.parts
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
op = expr.operator
|
|
66
|
+
|
|
67
|
+
# String comparisons (with pattern matching support in [[ ]])
|
|
68
|
+
if op in STRING_COMPARE_OPS:
|
|
69
|
+
return compare_strings(op, left, right, allow_pattern=not is_rhs_quoted)
|
|
70
|
+
|
|
71
|
+
# Numeric comparisons
|
|
72
|
+
if op in NUMERIC_OPS:
|
|
73
|
+
return compare_numeric(op, parse_numeric(left), parse_numeric(right))
|
|
74
|
+
|
|
75
|
+
# Binary file tests
|
|
76
|
+
if op in BINARY_FILE_TEST_OPS:
|
|
77
|
+
return await evaluate_binary_file_test(ctx, op, left, right)
|
|
78
|
+
|
|
79
|
+
# Regex matching
|
|
80
|
+
if op == "=~":
|
|
81
|
+
try:
|
|
82
|
+
match = re.search(right, left)
|
|
83
|
+
if match:
|
|
84
|
+
# Set BASH_REMATCH
|
|
85
|
+
ctx.state.env["BASH_REMATCH_0"] = match.group(0)
|
|
86
|
+
for i, group in enumerate(match.groups(), 1):
|
|
87
|
+
ctx.state.env[f"BASH_REMATCH_{i}"] = group if group else ""
|
|
88
|
+
ctx.state.env["BASH_REMATCH__length"] = str(len(match.groups()) + 1)
|
|
89
|
+
return match is not None
|
|
90
|
+
except re.error:
|
|
91
|
+
# Invalid regex is a syntax error (exit code 2)
|
|
92
|
+
raise ValueError("syntax error in regular expression")
|
|
93
|
+
|
|
94
|
+
# Lexicographic comparison
|
|
95
|
+
if op == "<":
|
|
96
|
+
return left < right
|
|
97
|
+
if op == ">":
|
|
98
|
+
return left > right
|
|
99
|
+
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
elif isinstance(expr, CondUnaryNode):
|
|
103
|
+
operand = await expand_word_async(ctx, expr.operand) if expr.operand else ""
|
|
104
|
+
op = expr.operator
|
|
105
|
+
|
|
106
|
+
# File test operators
|
|
107
|
+
if op in FILE_TEST_OPS:
|
|
108
|
+
return await evaluate_file_test(ctx, op, operand)
|
|
109
|
+
|
|
110
|
+
# String tests
|
|
111
|
+
if op == "-z":
|
|
112
|
+
return operand == ""
|
|
113
|
+
if op == "-n":
|
|
114
|
+
return operand != ""
|
|
115
|
+
|
|
116
|
+
# Variable test
|
|
117
|
+
if op == "-v":
|
|
118
|
+
return evaluate_variable_test(ctx, operand)
|
|
119
|
+
|
|
120
|
+
# Shell option test
|
|
121
|
+
if op == "-o":
|
|
122
|
+
return evaluate_shell_option(ctx, operand)
|
|
123
|
+
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
elif isinstance(expr, CondNotNode):
|
|
127
|
+
return not await evaluate_conditional(ctx, expr.operand)
|
|
128
|
+
|
|
129
|
+
elif isinstance(expr, CondAndNode):
|
|
130
|
+
left = await evaluate_conditional(ctx, expr.left)
|
|
131
|
+
if not left:
|
|
132
|
+
return False
|
|
133
|
+
return await evaluate_conditional(ctx, expr.right)
|
|
134
|
+
|
|
135
|
+
elif isinstance(expr, CondOrNode):
|
|
136
|
+
left = await evaluate_conditional(ctx, expr.left)
|
|
137
|
+
if left:
|
|
138
|
+
return True
|
|
139
|
+
return await evaluate_conditional(ctx, expr.right)
|
|
140
|
+
|
|
141
|
+
elif isinstance(expr, CondGroupNode):
|
|
142
|
+
return await evaluate_conditional(ctx, expr.expression)
|
|
143
|
+
|
|
144
|
+
elif isinstance(expr, CondWordNode):
|
|
145
|
+
value = await expand_word_async(ctx, expr.word) if expr.word else ""
|
|
146
|
+
return value != ""
|
|
147
|
+
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def compare_strings(op: str, left: str, right: str, allow_pattern: bool = False) -> bool:
|
|
152
|
+
"""Compare strings, optionally with pattern matching."""
|
|
153
|
+
if op == "=" or op == "==":
|
|
154
|
+
if allow_pattern:
|
|
155
|
+
return match_pattern(left, right)
|
|
156
|
+
return left == right
|
|
157
|
+
if op == "!=":
|
|
158
|
+
if allow_pattern:
|
|
159
|
+
return not match_pattern(left, right)
|
|
160
|
+
return left != right
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def match_pattern(value: str, pattern: str) -> bool:
|
|
165
|
+
"""Match a value against a glob-style pattern.
|
|
166
|
+
|
|
167
|
+
Converts glob pattern to regex for matching.
|
|
168
|
+
"""
|
|
169
|
+
# Use fnmatch for glob-style matching
|
|
170
|
+
return fnmatch.fnmatch(value, pattern)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def compare_numeric(op: str, left: int, right: int) -> bool:
|
|
174
|
+
"""Compare two numbers."""
|
|
175
|
+
if op == "-eq":
|
|
176
|
+
return left == right
|
|
177
|
+
if op == "-ne":
|
|
178
|
+
return left != right
|
|
179
|
+
if op == "-lt":
|
|
180
|
+
return left < right
|
|
181
|
+
if op == "-le":
|
|
182
|
+
return left <= right
|
|
183
|
+
if op == "-gt":
|
|
184
|
+
return left > right
|
|
185
|
+
if op == "-ge":
|
|
186
|
+
return left >= right
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def parse_numeric(value: str) -> int:
|
|
191
|
+
"""Parse a bash numeric value.
|
|
192
|
+
|
|
193
|
+
Supports:
|
|
194
|
+
- Decimal: 42, -42
|
|
195
|
+
- Octal: 0777, -0123
|
|
196
|
+
- Hex: 0xff, 0xFF, -0xff
|
|
197
|
+
- Base-N: 64#a, 2#1010
|
|
198
|
+
- Strings are coerced to 0
|
|
199
|
+
"""
|
|
200
|
+
value = value.strip()
|
|
201
|
+
if not value:
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
# Handle negative numbers
|
|
205
|
+
negative = False
|
|
206
|
+
if value.startswith("-"):
|
|
207
|
+
negative = True
|
|
208
|
+
value = value[1:]
|
|
209
|
+
elif value.startswith("+"):
|
|
210
|
+
value = value[1:]
|
|
211
|
+
|
|
212
|
+
result = 0
|
|
213
|
+
|
|
214
|
+
# Base-N syntax: base#value
|
|
215
|
+
base_match = re.match(r'^(\d+)#([a-zA-Z0-9@_]+)$', value)
|
|
216
|
+
if base_match:
|
|
217
|
+
base = int(base_match.group(1))
|
|
218
|
+
if 2 <= base <= 64:
|
|
219
|
+
result = parse_base_n(base_match.group(2), base)
|
|
220
|
+
else:
|
|
221
|
+
result = 0
|
|
222
|
+
# Hex: 0x or 0X
|
|
223
|
+
elif re.match(r'^0[xX][0-9a-fA-F]+$', value):
|
|
224
|
+
result = int(value, 16)
|
|
225
|
+
# Octal: starts with 0 followed by octal digits
|
|
226
|
+
elif re.match(r'^0[0-7]+$', value):
|
|
227
|
+
result = int(value, 8)
|
|
228
|
+
# Decimal
|
|
229
|
+
else:
|
|
230
|
+
try:
|
|
231
|
+
result = int(value)
|
|
232
|
+
except ValueError:
|
|
233
|
+
result = 0
|
|
234
|
+
|
|
235
|
+
return -result if negative else result
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_base_n(digits: str, base: int) -> int:
|
|
239
|
+
"""Parse a number in base N (2-64).
|
|
240
|
+
|
|
241
|
+
Digit values: 0-9=0-9, a-z=10-35, A-Z=36-61, @=62, _=63
|
|
242
|
+
"""
|
|
243
|
+
result = 0
|
|
244
|
+
for char in digits:
|
|
245
|
+
if '0' <= char <= '9':
|
|
246
|
+
digit_value = ord(char) - ord('0')
|
|
247
|
+
elif 'a' <= char <= 'z':
|
|
248
|
+
digit_value = ord(char) - ord('a') + 10
|
|
249
|
+
elif 'A' <= char <= 'Z':
|
|
250
|
+
digit_value = ord(char) - ord('A') + 36
|
|
251
|
+
elif char == '@':
|
|
252
|
+
digit_value = 62
|
|
253
|
+
elif char == '_':
|
|
254
|
+
digit_value = 63
|
|
255
|
+
else:
|
|
256
|
+
return 0
|
|
257
|
+
|
|
258
|
+
if digit_value >= base:
|
|
259
|
+
return 0
|
|
260
|
+
|
|
261
|
+
result = result * base + digit_value
|
|
262
|
+
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
async def evaluate_file_test(ctx: "InterpreterContext", op: str, path: str) -> bool:
|
|
267
|
+
"""Evaluate a file test operator."""
|
|
268
|
+
full_path = ctx.fs.resolve_path(ctx.state.cwd, path)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
exists = await ctx.fs.exists(full_path)
|
|
272
|
+
|
|
273
|
+
if op == "-e":
|
|
274
|
+
return exists
|
|
275
|
+
|
|
276
|
+
if not exists:
|
|
277
|
+
return False
|
|
278
|
+
|
|
279
|
+
if op == "-d":
|
|
280
|
+
return await ctx.fs.is_directory(full_path)
|
|
281
|
+
|
|
282
|
+
if op == "-f":
|
|
283
|
+
return not await ctx.fs.is_directory(full_path)
|
|
284
|
+
|
|
285
|
+
if op == "-s":
|
|
286
|
+
content = await ctx.fs.read_file(full_path)
|
|
287
|
+
return len(content) > 0
|
|
288
|
+
|
|
289
|
+
# For r/w/x, we assume true if file exists (VFS doesn't track permissions)
|
|
290
|
+
if op in ("-r", "-w", "-x"):
|
|
291
|
+
return exists
|
|
292
|
+
|
|
293
|
+
# Symlink tests
|
|
294
|
+
if op in ("-h", "-L"):
|
|
295
|
+
# VFS doesn't track symlinks, assume false
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# Special file tests (pipe, socket, block, char)
|
|
299
|
+
if op in ("-p", "-S", "-b", "-c"):
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
# Terminal test
|
|
303
|
+
if op == "-t":
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
# Owner tests
|
|
307
|
+
if op in ("-O", "-G"):
|
|
308
|
+
return exists
|
|
309
|
+
|
|
310
|
+
# Permission bit tests
|
|
311
|
+
if op in ("-u", "-g", "-k"):
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
# File modified since last read
|
|
315
|
+
if op == "-N":
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
return False
|
|
319
|
+
except Exception:
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def evaluate_binary_file_test(
|
|
324
|
+
ctx: "InterpreterContext", op: str, left: str, right: str
|
|
325
|
+
) -> bool:
|
|
326
|
+
"""Evaluate binary file test operators (-nt, -ot, -ef)."""
|
|
327
|
+
# These require file modification times which VFS may not track
|
|
328
|
+
# For now, return False
|
|
329
|
+
left_path = ctx.fs.resolve_path(ctx.state.cwd, left)
|
|
330
|
+
right_path = ctx.fs.resolve_path(ctx.state.cwd, right)
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
left_exists = await ctx.fs.exists(left_path)
|
|
334
|
+
right_exists = await ctx.fs.exists(right_path)
|
|
335
|
+
|
|
336
|
+
if op == "-nt": # newer than
|
|
337
|
+
# If left exists and right doesn't, left is newer
|
|
338
|
+
if left_exists and not right_exists:
|
|
339
|
+
return True
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
if op == "-ot": # older than
|
|
343
|
+
# If right exists and left doesn't, left is older
|
|
344
|
+
if right_exists and not left_exists:
|
|
345
|
+
return True
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
if op == "-ef": # same file
|
|
349
|
+
# Check if both point to same file (VFS: check paths)
|
|
350
|
+
return left_path == right_path and left_exists
|
|
351
|
+
|
|
352
|
+
return False
|
|
353
|
+
except Exception:
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def evaluate_variable_test(ctx: "InterpreterContext", name: str) -> bool:
|
|
358
|
+
"""Test if a variable is set (-v)."""
|
|
359
|
+
# Handle array element syntax: arr[idx]
|
|
360
|
+
if "[" in name and name.endswith("]"):
|
|
361
|
+
base = name[:name.index("[")]
|
|
362
|
+
idx = name[name.index("[") + 1:-1]
|
|
363
|
+
# Check if array element exists
|
|
364
|
+
key = f"{base}_{idx}"
|
|
365
|
+
return key in ctx.state.env
|
|
366
|
+
|
|
367
|
+
return name in ctx.state.env
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def evaluate_shell_option(ctx: "InterpreterContext", option: str) -> bool:
|
|
371
|
+
"""Test if a shell option is enabled (-o)."""
|
|
372
|
+
option_map = {
|
|
373
|
+
"errexit": lambda: ctx.state.options.errexit,
|
|
374
|
+
"nounset": lambda: ctx.state.options.nounset,
|
|
375
|
+
"pipefail": lambda: ctx.state.options.pipefail,
|
|
376
|
+
"xtrace": lambda: ctx.state.options.xtrace,
|
|
377
|
+
"verbose": lambda: ctx.state.options.verbose,
|
|
378
|
+
"e": lambda: ctx.state.options.errexit,
|
|
379
|
+
"u": lambda: ctx.state.options.nounset,
|
|
380
|
+
"x": lambda: ctx.state.options.xtrace,
|
|
381
|
+
"v": lambda: ctx.state.options.verbose,
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
getter = option_map.get(option)
|
|
385
|
+
if getter:
|
|
386
|
+
return getter()
|
|
387
|
+
return False
|