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,623 @@
|
|
|
1
|
+
"""Find command implementation.
|
|
2
|
+
|
|
3
|
+
Usage: find [path...] [expression]
|
|
4
|
+
|
|
5
|
+
Search for files in a directory hierarchy.
|
|
6
|
+
|
|
7
|
+
Note: -user and -group predicates are not implemented as they are not
|
|
8
|
+
applicable in an in-memory virtual filesystem designed for sandboxed execution.
|
|
9
|
+
|
|
10
|
+
Options:
|
|
11
|
+
-name PATTERN match file name (shell glob)
|
|
12
|
+
-iname PATTERN like -name but case insensitive
|
|
13
|
+
-type TYPE file type (f=file, d=directory, l=symlink)
|
|
14
|
+
-size N[cwbkMG] file size (+ for greater, - for less)
|
|
15
|
+
-mtime N modification time in days (+ older, - newer)
|
|
16
|
+
-newer FILE newer than FILE
|
|
17
|
+
-path PATTERN match full path
|
|
18
|
+
-regex PATTERN match path with regex
|
|
19
|
+
-maxdepth N descend at most N levels
|
|
20
|
+
-mindepth N do not apply tests at levels less than N
|
|
21
|
+
-empty match empty files/directories
|
|
22
|
+
-perm MODE match permission bits
|
|
23
|
+
|
|
24
|
+
Actions:
|
|
25
|
+
-print print path (default)
|
|
26
|
+
-print0 print path with null terminator
|
|
27
|
+
-delete delete matched files
|
|
28
|
+
-exec CMD {} ; execute command
|
|
29
|
+
|
|
30
|
+
Operators:
|
|
31
|
+
-and, -a logical AND (implicit)
|
|
32
|
+
-or, -o logical OR
|
|
33
|
+
-not, ! logical NOT
|
|
34
|
+
( expr ) grouping
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import fnmatch
|
|
38
|
+
import re
|
|
39
|
+
import time
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
from typing import Any
|
|
42
|
+
from ...types import CommandContext, ExecResult
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Expression:
|
|
47
|
+
"""A find expression."""
|
|
48
|
+
|
|
49
|
+
type: str # "predicate", "and", "or", "not", "group"
|
|
50
|
+
predicate: str = ""
|
|
51
|
+
value: Any = None
|
|
52
|
+
left: "Expression | None" = None
|
|
53
|
+
right: "Expression | None" = None
|
|
54
|
+
inner: "Expression | None" = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FindCommand:
|
|
58
|
+
"""The find command."""
|
|
59
|
+
|
|
60
|
+
name = "find"
|
|
61
|
+
|
|
62
|
+
async def execute(self, args: list[str], ctx: CommandContext) -> ExecResult:
|
|
63
|
+
"""Execute the find command."""
|
|
64
|
+
paths: list[str] = []
|
|
65
|
+
i = 0
|
|
66
|
+
maxdepth = -1
|
|
67
|
+
mindepth = 0
|
|
68
|
+
|
|
69
|
+
# Parse paths (before first -option or !)
|
|
70
|
+
while i < len(args):
|
|
71
|
+
arg = args[i]
|
|
72
|
+
if arg.startswith("-") or arg == "!" or arg == "(":
|
|
73
|
+
break
|
|
74
|
+
paths.append(arg)
|
|
75
|
+
i += 1
|
|
76
|
+
|
|
77
|
+
# Default to current directory
|
|
78
|
+
if not paths:
|
|
79
|
+
paths = ["."]
|
|
80
|
+
|
|
81
|
+
# Parse depth options first
|
|
82
|
+
j = i
|
|
83
|
+
while j < len(args):
|
|
84
|
+
if args[j] == "-maxdepth" and j + 1 < len(args):
|
|
85
|
+
try:
|
|
86
|
+
maxdepth = int(args[j + 1])
|
|
87
|
+
except ValueError:
|
|
88
|
+
return ExecResult(
|
|
89
|
+
stdout="",
|
|
90
|
+
stderr=f"find: invalid argument '{args[j + 1]}' to '-maxdepth'\n",
|
|
91
|
+
exit_code=1,
|
|
92
|
+
)
|
|
93
|
+
elif args[j] == "-mindepth" and j + 1 < len(args):
|
|
94
|
+
try:
|
|
95
|
+
mindepth = int(args[j + 1])
|
|
96
|
+
except ValueError:
|
|
97
|
+
return ExecResult(
|
|
98
|
+
stdout="",
|
|
99
|
+
stderr=f"find: invalid argument '{args[j + 1]}' to '-mindepth'\n",
|
|
100
|
+
exit_code=1,
|
|
101
|
+
)
|
|
102
|
+
j += 1
|
|
103
|
+
|
|
104
|
+
# Parse expression
|
|
105
|
+
expr_args = args[i:]
|
|
106
|
+
# Remove depth options from expression parsing
|
|
107
|
+
filtered_args = []
|
|
108
|
+
j = 0
|
|
109
|
+
while j < len(expr_args):
|
|
110
|
+
if expr_args[j] in ("-maxdepth", "-mindepth"):
|
|
111
|
+
j += 2
|
|
112
|
+
else:
|
|
113
|
+
filtered_args.append(expr_args[j])
|
|
114
|
+
j += 1
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
expr, action = self._parse_expression(filtered_args)
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
return ExecResult(
|
|
120
|
+
stdout="",
|
|
121
|
+
stderr=f"find: {e}\n",
|
|
122
|
+
exit_code=1,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Default action is -print
|
|
126
|
+
if action is None:
|
|
127
|
+
action = ("print", None)
|
|
128
|
+
|
|
129
|
+
# Execute find
|
|
130
|
+
output = ""
|
|
131
|
+
stderr = ""
|
|
132
|
+
exit_code = 0
|
|
133
|
+
|
|
134
|
+
for path in paths:
|
|
135
|
+
try:
|
|
136
|
+
resolved = ctx.fs.resolve_path(ctx.cwd, path)
|
|
137
|
+
result, err = await self._find_recursive(
|
|
138
|
+
ctx, resolved, path, expr, action, 0, maxdepth, mindepth
|
|
139
|
+
)
|
|
140
|
+
output += result
|
|
141
|
+
stderr += err
|
|
142
|
+
except FileNotFoundError:
|
|
143
|
+
stderr += f"find: '{path}': No such file or directory\n"
|
|
144
|
+
exit_code = 1
|
|
145
|
+
|
|
146
|
+
if exit_code == 0 and stderr:
|
|
147
|
+
exit_code = 1
|
|
148
|
+
|
|
149
|
+
return ExecResult(stdout=output, stderr=stderr, exit_code=exit_code)
|
|
150
|
+
|
|
151
|
+
def _parse_expression(
|
|
152
|
+
self, args: list[str]
|
|
153
|
+
) -> tuple[Expression | None, tuple[str, Any] | None]:
|
|
154
|
+
"""Parse find expression from arguments."""
|
|
155
|
+
if not args:
|
|
156
|
+
return None, None
|
|
157
|
+
|
|
158
|
+
expr, pos, action = self._parse_or(args, 0)
|
|
159
|
+
return expr, action
|
|
160
|
+
|
|
161
|
+
def _parse_or(
|
|
162
|
+
self, args: list[str], pos: int
|
|
163
|
+
) -> tuple[Expression | None, int, tuple[str, Any] | None]:
|
|
164
|
+
"""Parse OR expression."""
|
|
165
|
+
left, pos, action = self._parse_and(args, pos)
|
|
166
|
+
|
|
167
|
+
while pos < len(args) and args[pos] in ("-or", "-o"):
|
|
168
|
+
pos += 1
|
|
169
|
+
right, pos, act2 = self._parse_and(args, pos)
|
|
170
|
+
if act2:
|
|
171
|
+
action = act2
|
|
172
|
+
left = Expression(type="or", left=left, right=right)
|
|
173
|
+
|
|
174
|
+
return left, pos, action
|
|
175
|
+
|
|
176
|
+
def _parse_and(
|
|
177
|
+
self, args: list[str], pos: int
|
|
178
|
+
) -> tuple[Expression | None, int, tuple[str, Any] | None]:
|
|
179
|
+
"""Parse AND expression."""
|
|
180
|
+
left, pos, action = self._parse_not(args, pos)
|
|
181
|
+
|
|
182
|
+
while pos < len(args) and (args[pos] in ("-and", "-a") or (
|
|
183
|
+
args[pos] not in ("-or", "-o", ")") and not args[pos].startswith("-print") and args[pos] != "-delete"
|
|
184
|
+
)):
|
|
185
|
+
if args[pos] in ("-and", "-a"):
|
|
186
|
+
pos += 1
|
|
187
|
+
right, pos, act2 = self._parse_not(args, pos)
|
|
188
|
+
if act2:
|
|
189
|
+
action = act2
|
|
190
|
+
if right:
|
|
191
|
+
left = Expression(type="and", left=left, right=right)
|
|
192
|
+
|
|
193
|
+
return left, pos, action
|
|
194
|
+
|
|
195
|
+
def _parse_not(
|
|
196
|
+
self, args: list[str], pos: int
|
|
197
|
+
) -> tuple[Expression | None, int, tuple[str, Any] | None]:
|
|
198
|
+
"""Parse NOT expression."""
|
|
199
|
+
if pos < len(args) and args[pos] in ("-not", "!"):
|
|
200
|
+
pos += 1
|
|
201
|
+
inner, pos, action = self._parse_primary(args, pos)
|
|
202
|
+
return Expression(type="not", inner=inner), pos, action
|
|
203
|
+
|
|
204
|
+
return self._parse_primary(args, pos)
|
|
205
|
+
|
|
206
|
+
def _parse_primary(
|
|
207
|
+
self, args: list[str], pos: int
|
|
208
|
+
) -> tuple[Expression | None, int, tuple[str, Any] | None]:
|
|
209
|
+
"""Parse primary expression."""
|
|
210
|
+
if pos >= len(args):
|
|
211
|
+
return None, pos, None
|
|
212
|
+
|
|
213
|
+
action = None
|
|
214
|
+
arg = args[pos]
|
|
215
|
+
|
|
216
|
+
# Grouping
|
|
217
|
+
if arg == "(":
|
|
218
|
+
pos += 1
|
|
219
|
+
expr, pos, action = self._parse_or(args, pos)
|
|
220
|
+
if pos < len(args) and args[pos] == ")":
|
|
221
|
+
pos += 1
|
|
222
|
+
return Expression(type="group", inner=expr), pos, action
|
|
223
|
+
|
|
224
|
+
# Actions
|
|
225
|
+
if arg == "-print":
|
|
226
|
+
pos += 1
|
|
227
|
+
return None, pos, ("print", None)
|
|
228
|
+
|
|
229
|
+
if arg == "-print0":
|
|
230
|
+
pos += 1
|
|
231
|
+
return None, pos, ("print0", None)
|
|
232
|
+
|
|
233
|
+
if arg == "-delete":
|
|
234
|
+
pos += 1
|
|
235
|
+
return None, pos, ("delete", None)
|
|
236
|
+
|
|
237
|
+
if arg == "-exec":
|
|
238
|
+
# Find the terminator
|
|
239
|
+
cmd_parts = []
|
|
240
|
+
pos += 1
|
|
241
|
+
while pos < len(args) and args[pos] not in (";", "+"):
|
|
242
|
+
cmd_parts.append(args[pos])
|
|
243
|
+
pos += 1
|
|
244
|
+
if pos < len(args):
|
|
245
|
+
pos += 1
|
|
246
|
+
return None, pos, ("exec", cmd_parts)
|
|
247
|
+
|
|
248
|
+
# Predicates
|
|
249
|
+
if arg in ("-name", "-iname"):
|
|
250
|
+
if pos + 1 >= len(args):
|
|
251
|
+
raise ValueError(f"missing argument to '{arg}'")
|
|
252
|
+
pattern = args[pos + 1]
|
|
253
|
+
return (
|
|
254
|
+
Expression(
|
|
255
|
+
type="predicate", predicate=arg[1:], value=pattern
|
|
256
|
+
),
|
|
257
|
+
pos + 2,
|
|
258
|
+
action,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if arg == "-type":
|
|
262
|
+
if pos + 1 >= len(args):
|
|
263
|
+
raise ValueError("missing argument to '-type'")
|
|
264
|
+
type_val = args[pos + 1]
|
|
265
|
+
return (
|
|
266
|
+
Expression(type="predicate", predicate="type", value=type_val),
|
|
267
|
+
pos + 2,
|
|
268
|
+
action,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if arg == "-size":
|
|
272
|
+
if pos + 1 >= len(args):
|
|
273
|
+
raise ValueError("missing argument to '-size'")
|
|
274
|
+
size_spec = args[pos + 1]
|
|
275
|
+
return (
|
|
276
|
+
Expression(type="predicate", predicate="size", value=size_spec),
|
|
277
|
+
pos + 2,
|
|
278
|
+
action,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if arg == "-mtime":
|
|
282
|
+
if pos + 1 >= len(args):
|
|
283
|
+
raise ValueError("missing argument to '-mtime'")
|
|
284
|
+
mtime_val = args[pos + 1]
|
|
285
|
+
return (
|
|
286
|
+
Expression(type="predicate", predicate="mtime", value=mtime_val),
|
|
287
|
+
pos + 2,
|
|
288
|
+
action,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if arg == "-newer":
|
|
292
|
+
if pos + 1 >= len(args):
|
|
293
|
+
raise ValueError("missing argument to '-newer'")
|
|
294
|
+
return (
|
|
295
|
+
Expression(type="predicate", predicate="newer", value=args[pos + 1]),
|
|
296
|
+
pos + 2,
|
|
297
|
+
action,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if arg in ("-path", "-ipath"):
|
|
301
|
+
if pos + 1 >= len(args):
|
|
302
|
+
raise ValueError(f"missing argument to '{arg}'")
|
|
303
|
+
return (
|
|
304
|
+
Expression(type="predicate", predicate=arg[1:], value=args[pos + 1]),
|
|
305
|
+
pos + 2,
|
|
306
|
+
action,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if arg in ("-regex", "-iregex"):
|
|
310
|
+
if pos + 1 >= len(args):
|
|
311
|
+
raise ValueError(f"missing argument to '{arg}'")
|
|
312
|
+
return (
|
|
313
|
+
Expression(type="predicate", predicate=arg[1:], value=args[pos + 1]),
|
|
314
|
+
pos + 2,
|
|
315
|
+
action,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if arg == "-empty":
|
|
319
|
+
return (
|
|
320
|
+
Expression(type="predicate", predicate="empty"),
|
|
321
|
+
pos + 1,
|
|
322
|
+
action,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if arg == "-perm":
|
|
326
|
+
if pos + 1 >= len(args):
|
|
327
|
+
raise ValueError("missing argument to '-perm'")
|
|
328
|
+
return (
|
|
329
|
+
Expression(type="predicate", predicate="perm", value=args[pos + 1]),
|
|
330
|
+
pos + 2,
|
|
331
|
+
action,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if arg.startswith("-"):
|
|
335
|
+
raise ValueError(f"unknown predicate '{arg}'")
|
|
336
|
+
|
|
337
|
+
# Skip unknown args
|
|
338
|
+
return None, pos + 1, action
|
|
339
|
+
|
|
340
|
+
async def _find_recursive(
|
|
341
|
+
self,
|
|
342
|
+
ctx: CommandContext,
|
|
343
|
+
abs_path: str,
|
|
344
|
+
display_path: str,
|
|
345
|
+
expr: Expression | None,
|
|
346
|
+
action: tuple[str, Any],
|
|
347
|
+
depth: int,
|
|
348
|
+
maxdepth: int,
|
|
349
|
+
mindepth: int,
|
|
350
|
+
) -> tuple[str, str]:
|
|
351
|
+
"""Recursively find files."""
|
|
352
|
+
output = ""
|
|
353
|
+
stderr = ""
|
|
354
|
+
|
|
355
|
+
if maxdepth >= 0 and depth > maxdepth:
|
|
356
|
+
return output, stderr
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
stat = await ctx.fs.stat(abs_path)
|
|
360
|
+
except FileNotFoundError:
|
|
361
|
+
return output, f"find: '{display_path}': No such file or directory\n"
|
|
362
|
+
|
|
363
|
+
# Check if expression matches (only at mindepth or deeper)
|
|
364
|
+
if depth >= mindepth:
|
|
365
|
+
matches = await self._evaluate(ctx, abs_path, display_path, stat, expr)
|
|
366
|
+
|
|
367
|
+
if matches:
|
|
368
|
+
act_name, act_val = action
|
|
369
|
+
|
|
370
|
+
if act_name == "print":
|
|
371
|
+
output += display_path + "\n"
|
|
372
|
+
elif act_name == "print0":
|
|
373
|
+
output += display_path + "\0"
|
|
374
|
+
elif act_name == "delete":
|
|
375
|
+
try:
|
|
376
|
+
await ctx.fs.rm(abs_path, recursive=stat.is_directory)
|
|
377
|
+
except Exception as e:
|
|
378
|
+
stderr += f"find: cannot delete '{display_path}': {e}\n"
|
|
379
|
+
elif act_name == "exec":
|
|
380
|
+
# Execute command with {} replaced by path
|
|
381
|
+
if act_val:
|
|
382
|
+
# Replace {} with the current path
|
|
383
|
+
cmd_parts = [p.replace("{}", abs_path) for p in act_val]
|
|
384
|
+
# Build command string
|
|
385
|
+
cmd_str = " ".join(cmd_parts)
|
|
386
|
+
# Execute via the context's exec function
|
|
387
|
+
try:
|
|
388
|
+
result = await ctx.exec(cmd_str, {"cwd": ctx.cwd})
|
|
389
|
+
output += result.stdout
|
|
390
|
+
stderr += result.stderr
|
|
391
|
+
except Exception as e:
|
|
392
|
+
stderr += f"find: -exec failed: {e}\n"
|
|
393
|
+
|
|
394
|
+
# Recurse into directories
|
|
395
|
+
if stat.is_directory and (maxdepth < 0 or depth < maxdepth):
|
|
396
|
+
try:
|
|
397
|
+
entries = await ctx.fs.readdir(abs_path)
|
|
398
|
+
for entry in sorted(entries):
|
|
399
|
+
child_abs = abs_path.rstrip("/") + "/" + entry
|
|
400
|
+
child_display = display_path.rstrip("/") + "/" + entry
|
|
401
|
+
child_out, child_err = await self._find_recursive(
|
|
402
|
+
ctx, child_abs, child_display, expr, action, depth + 1, maxdepth, mindepth
|
|
403
|
+
)
|
|
404
|
+
output += child_out
|
|
405
|
+
stderr += child_err
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
return output, stderr
|
|
410
|
+
|
|
411
|
+
async def _evaluate(
|
|
412
|
+
self,
|
|
413
|
+
ctx: CommandContext,
|
|
414
|
+
abs_path: str,
|
|
415
|
+
display_path: str,
|
|
416
|
+
stat: Any,
|
|
417
|
+
expr: Expression | None,
|
|
418
|
+
) -> bool:
|
|
419
|
+
"""Evaluate an expression against a file."""
|
|
420
|
+
if expr is None:
|
|
421
|
+
return True
|
|
422
|
+
|
|
423
|
+
if expr.type == "and":
|
|
424
|
+
left = await self._evaluate(ctx, abs_path, display_path, stat, expr.left)
|
|
425
|
+
if not left:
|
|
426
|
+
return False
|
|
427
|
+
return await self._evaluate(ctx, abs_path, display_path, stat, expr.right)
|
|
428
|
+
|
|
429
|
+
elif expr.type == "or":
|
|
430
|
+
left = await self._evaluate(ctx, abs_path, display_path, stat, expr.left)
|
|
431
|
+
if left:
|
|
432
|
+
return True
|
|
433
|
+
return await self._evaluate(ctx, abs_path, display_path, stat, expr.right)
|
|
434
|
+
|
|
435
|
+
elif expr.type == "not":
|
|
436
|
+
return not await self._evaluate(ctx, abs_path, display_path, stat, expr.inner)
|
|
437
|
+
|
|
438
|
+
elif expr.type == "group":
|
|
439
|
+
return await self._evaluate(ctx, abs_path, display_path, stat, expr.inner)
|
|
440
|
+
|
|
441
|
+
elif expr.type == "predicate":
|
|
442
|
+
return await self._evaluate_predicate(ctx, abs_path, display_path, stat, expr)
|
|
443
|
+
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
async def _evaluate_predicate(
|
|
447
|
+
self,
|
|
448
|
+
ctx: CommandContext,
|
|
449
|
+
abs_path: str,
|
|
450
|
+
display_path: str,
|
|
451
|
+
stat: dict,
|
|
452
|
+
expr: Expression,
|
|
453
|
+
) -> bool:
|
|
454
|
+
"""Evaluate a single predicate."""
|
|
455
|
+
pred = expr.predicate
|
|
456
|
+
value = expr.value
|
|
457
|
+
|
|
458
|
+
# Get basename for name matching
|
|
459
|
+
basename = abs_path.rsplit("/", 1)[-1]
|
|
460
|
+
|
|
461
|
+
if pred == "name":
|
|
462
|
+
return fnmatch.fnmatch(basename, value)
|
|
463
|
+
|
|
464
|
+
elif pred == "iname":
|
|
465
|
+
return fnmatch.fnmatch(basename.lower(), value.lower())
|
|
466
|
+
|
|
467
|
+
elif pred == "type":
|
|
468
|
+
if value == "f":
|
|
469
|
+
return stat.is_file
|
|
470
|
+
elif value == "d":
|
|
471
|
+
return stat.is_directory
|
|
472
|
+
elif value == "l":
|
|
473
|
+
return stat.is_symbolic_link
|
|
474
|
+
return False
|
|
475
|
+
|
|
476
|
+
elif pred == "size":
|
|
477
|
+
return self._match_size(stat.size, value)
|
|
478
|
+
|
|
479
|
+
elif pred == "mtime":
|
|
480
|
+
now = time.time()
|
|
481
|
+
days = (now - stat.mtime) / 86400
|
|
482
|
+
return self._match_numeric(days, value)
|
|
483
|
+
|
|
484
|
+
elif pred == "newer":
|
|
485
|
+
try:
|
|
486
|
+
ref_path = ctx.fs.resolve_path(ctx.cwd, value)
|
|
487
|
+
ref_stat = await ctx.fs.stat(ref_path)
|
|
488
|
+
return stat.mtime > ref_stat.mtime
|
|
489
|
+
except FileNotFoundError:
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
elif pred == "path":
|
|
493
|
+
return fnmatch.fnmatch(display_path, value)
|
|
494
|
+
|
|
495
|
+
elif pred == "ipath":
|
|
496
|
+
return fnmatch.fnmatch(display_path.lower(), value.lower())
|
|
497
|
+
|
|
498
|
+
elif pred == "regex":
|
|
499
|
+
try:
|
|
500
|
+
return bool(re.search(value, display_path))
|
|
501
|
+
except re.error:
|
|
502
|
+
return False
|
|
503
|
+
|
|
504
|
+
elif pred == "iregex":
|
|
505
|
+
try:
|
|
506
|
+
return bool(re.search(value, display_path, re.IGNORECASE))
|
|
507
|
+
except re.error:
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
elif pred == "empty":
|
|
511
|
+
if stat.is_directory:
|
|
512
|
+
try:
|
|
513
|
+
entries = await ctx.fs.readdir(abs_path)
|
|
514
|
+
return len(entries) == 0
|
|
515
|
+
except Exception:
|
|
516
|
+
return False
|
|
517
|
+
else:
|
|
518
|
+
return stat.size == 0
|
|
519
|
+
|
|
520
|
+
elif pred == "perm":
|
|
521
|
+
return self._match_perm(stat.mode, value)
|
|
522
|
+
|
|
523
|
+
return True
|
|
524
|
+
|
|
525
|
+
def _match_size(self, size: int, spec: str) -> bool:
|
|
526
|
+
"""Match file size against specification."""
|
|
527
|
+
if not spec:
|
|
528
|
+
return True
|
|
529
|
+
|
|
530
|
+
# Parse +/- prefix
|
|
531
|
+
compare = "eq"
|
|
532
|
+
if spec[0] == "+":
|
|
533
|
+
compare = "gt"
|
|
534
|
+
spec = spec[1:]
|
|
535
|
+
elif spec[0] == "-":
|
|
536
|
+
compare = "lt"
|
|
537
|
+
spec = spec[1:]
|
|
538
|
+
|
|
539
|
+
# Parse unit suffix
|
|
540
|
+
unit = 512 # default is 512-byte blocks
|
|
541
|
+
if spec and spec[-1] in "cwbkMG":
|
|
542
|
+
suffix = spec[-1]
|
|
543
|
+
spec = spec[:-1]
|
|
544
|
+
if suffix == "c":
|
|
545
|
+
unit = 1
|
|
546
|
+
elif suffix == "w":
|
|
547
|
+
unit = 2
|
|
548
|
+
elif suffix == "b":
|
|
549
|
+
unit = 512
|
|
550
|
+
elif suffix == "k":
|
|
551
|
+
unit = 1024
|
|
552
|
+
elif suffix == "M":
|
|
553
|
+
unit = 1024 * 1024
|
|
554
|
+
elif suffix == "G":
|
|
555
|
+
unit = 1024 * 1024 * 1024
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
n = int(spec)
|
|
559
|
+
except ValueError:
|
|
560
|
+
return True
|
|
561
|
+
|
|
562
|
+
target = n * unit
|
|
563
|
+
|
|
564
|
+
if compare == "eq":
|
|
565
|
+
return size == target
|
|
566
|
+
elif compare == "gt":
|
|
567
|
+
return size > target
|
|
568
|
+
elif compare == "lt":
|
|
569
|
+
return size < target
|
|
570
|
+
|
|
571
|
+
return True
|
|
572
|
+
|
|
573
|
+
def _match_numeric(self, actual: float, spec: str) -> bool:
|
|
574
|
+
"""Match numeric value against +N/-N/N specification."""
|
|
575
|
+
if not spec:
|
|
576
|
+
return True
|
|
577
|
+
|
|
578
|
+
compare = "eq"
|
|
579
|
+
if spec[0] == "+":
|
|
580
|
+
compare = "gt"
|
|
581
|
+
spec = spec[1:]
|
|
582
|
+
elif spec[0] == "-":
|
|
583
|
+
compare = "lt"
|
|
584
|
+
spec = spec[1:]
|
|
585
|
+
|
|
586
|
+
try:
|
|
587
|
+
n = int(spec)
|
|
588
|
+
except ValueError:
|
|
589
|
+
return True
|
|
590
|
+
|
|
591
|
+
if compare == "eq":
|
|
592
|
+
return int(actual) == n
|
|
593
|
+
elif compare == "gt":
|
|
594
|
+
return actual > n
|
|
595
|
+
elif compare == "lt":
|
|
596
|
+
return actual < n
|
|
597
|
+
|
|
598
|
+
return True
|
|
599
|
+
|
|
600
|
+
def _match_perm(self, mode: int, spec: str) -> bool:
|
|
601
|
+
"""Match permission mode."""
|
|
602
|
+
if not spec:
|
|
603
|
+
return True
|
|
604
|
+
|
|
605
|
+
# Handle exact match
|
|
606
|
+
exact = True
|
|
607
|
+
if spec.startswith("-"):
|
|
608
|
+
exact = False
|
|
609
|
+
spec = spec[1:]
|
|
610
|
+
elif spec.startswith("/"):
|
|
611
|
+
# Any bit match
|
|
612
|
+
exact = False
|
|
613
|
+
spec = spec[1:]
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
perm = int(spec, 8)
|
|
617
|
+
except ValueError:
|
|
618
|
+
return True
|
|
619
|
+
|
|
620
|
+
if exact:
|
|
621
|
+
return (mode & 0o7777) == perm
|
|
622
|
+
else:
|
|
623
|
+
return (mode & perm) == perm
|