borescope 0.1.0.dev0__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.
- borescope/__init__.py +10 -0
- borescope/__main__.py +10 -0
- borescope/cli.py +144 -0
- borescope/discovery.py +256 -0
- borescope/errors.py +26 -0
- borescope/juju.py +89 -0
- borescope/shell/__init__.py +8 -0
- borescope/shell/commands/__init__.py +13 -0
- borescope/shell/commands/_args.py +54 -0
- borescope/shell/commands/base.py +81 -0
- borescope/shell/commands/basic.py +117 -0
- borescope/shell/commands/execcmd.py +36 -0
- borescope/shell/commands/filesystem.py +494 -0
- borescope/shell/commands/pebble.py +388 -0
- borescope/shell/completion.py +77 -0
- borescope/shell/context.py +33 -0
- borescope/shell/history.py +29 -0
- borescope/shell/parser.py +91 -0
- borescope/shell/pathutils.py +21 -0
- borescope/shell/repl.py +133 -0
- borescope/shell/theme.py +35 -0
- borescope/snapshot.py +103 -0
- borescope/transport/__init__.py +162 -0
- borescope/transport/cli_transport.py +63 -0
- borescope/transport/relay.py +77 -0
- borescope/transport/runner.py +149 -0
- borescope/transport/socket_transport.py +29 -0
- borescope-0.1.0.dev0.dist-info/METADATA +100 -0
- borescope-0.1.0.dev0.dist-info/RECORD +32 -0
- borescope-0.1.0.dev0.dist-info/WHEEL +4 -0
- borescope-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- borescope-0.1.0.dev0.dist-info/licenses/LICENSE +203 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Filesystem commands implemented over the Pebble files API.
|
|
2
|
+
|
|
3
|
+
These exist as built-ins (rather than ``exec``) because they need either
|
|
4
|
+
shell-side state (paths relative to ``cwd``) or the Pebble files API
|
|
5
|
+
(``pull``/``push``/``list_files``/``make_dir``/``remove_path``) to work against a
|
|
6
|
+
rock with no shell or coreutils.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import fnmatch
|
|
12
|
+
import posixpath
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
from .. import pathutils
|
|
19
|
+
from ._args import parse_args
|
|
20
|
+
from .base import Command, Result
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ...transport import Transport
|
|
24
|
+
from ..context import ShellContext
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --------------------------------------------------------------------------- #
|
|
28
|
+
# Helpers
|
|
29
|
+
# --------------------------------------------------------------------------- #
|
|
30
|
+
def _read_bytes(transport: Transport, path: str) -> bytes:
|
|
31
|
+
with transport.pull(path, encoding=None) as handle:
|
|
32
|
+
data = handle.read()
|
|
33
|
+
return data if isinstance(data, bytes) else data.encode("utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_text(transport: Transport, path: str) -> str:
|
|
37
|
+
data = _read_bytes(transport, path)
|
|
38
|
+
return data.decode("utf-8", errors="replace")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_dir(info: Any) -> bool:
|
|
42
|
+
return getattr(getattr(info, "type", None), "name", "") == "DIRECTORY"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _int(value: str | None, default: int) -> int:
|
|
46
|
+
try:
|
|
47
|
+
return int(value) if value is not None else default
|
|
48
|
+
except ValueError:
|
|
49
|
+
return default
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _mode_str(perm: int | None) -> str:
|
|
53
|
+
if perm is None:
|
|
54
|
+
return "---------"
|
|
55
|
+
out = ""
|
|
56
|
+
for shift in (6, 3, 0):
|
|
57
|
+
bits = (perm >> shift) & 7
|
|
58
|
+
out += "r" if bits & 4 else "-"
|
|
59
|
+
out += "w" if bits & 2 else "-"
|
|
60
|
+
out += "x" if bits & 1 else "-"
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _type_char(info: Any) -> str:
|
|
65
|
+
name = getattr(getattr(info, "type", None), "name", "")
|
|
66
|
+
return {"DIRECTORY": "d", "SYMLINK": "l"}.get(name, "-")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _long_format(info: Any) -> str:
|
|
70
|
+
size = getattr(info, "size", None) or 0
|
|
71
|
+
mtime = getattr(info, "last_modified", None)
|
|
72
|
+
when = mtime.strftime("%Y-%m-%d %H:%M") if mtime else ""
|
|
73
|
+
perms = _mode_str(getattr(info, "permissions", None))
|
|
74
|
+
return f"{_type_char(info)}{perms} {str(size).rjust(8)} {when:>16} {info.name}"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _resolve(ctx: ShellContext, path: str) -> str:
|
|
78
|
+
return pathutils.resolve(ctx.cwd, path, home=ctx.home)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _input_text(ctx: ShellContext, paths: list[str], stdin: str | None) -> str:
|
|
82
|
+
if paths:
|
|
83
|
+
return _read_text(ctx.transport, _resolve(ctx, paths[0]))
|
|
84
|
+
return stdin or ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _dest_path(ctx: ShellContext, dst: str, src: str) -> str:
|
|
88
|
+
"""If *dst* is an existing directory, place *src*'s basename inside it."""
|
|
89
|
+
try:
|
|
90
|
+
infos = ctx.transport.list_files(dst, itself=True)
|
|
91
|
+
except Exception: # noqa: BLE001
|
|
92
|
+
return dst
|
|
93
|
+
if infos and _is_dir(infos[0]):
|
|
94
|
+
return posixpath.join(dst, posixpath.basename(src))
|
|
95
|
+
return dst
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --------------------------------------------------------------------------- #
|
|
99
|
+
# Commands
|
|
100
|
+
# --------------------------------------------------------------------------- #
|
|
101
|
+
class Ls(Command):
|
|
102
|
+
name = "ls"
|
|
103
|
+
summary = "List directory contents"
|
|
104
|
+
usage = "ls [-l] [-a] [path...]"
|
|
105
|
+
|
|
106
|
+
def run(
|
|
107
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
108
|
+
) -> Result:
|
|
109
|
+
flags, _, paths = parse_args(args)
|
|
110
|
+
long, show_all = "l" in flags, "a" in flags
|
|
111
|
+
paths = paths or [ctx.cwd]
|
|
112
|
+
blocks: list[str] = []
|
|
113
|
+
errors: list[str] = []
|
|
114
|
+
for path in paths:
|
|
115
|
+
try:
|
|
116
|
+
infos = ctx.transport.list_files(_resolve(ctx, path))
|
|
117
|
+
except Exception as exc: # noqa: BLE001
|
|
118
|
+
errors.append(f"ls: {path}: {exc}")
|
|
119
|
+
continue
|
|
120
|
+
entries = sorted(infos, key=lambda i: i.name)
|
|
121
|
+
if not show_all:
|
|
122
|
+
entries = [i for i in entries if not i.name.startswith(".")]
|
|
123
|
+
rendered = "\n".join(_long_format(i) if long else i.name for i in entries)
|
|
124
|
+
blocks.append(f"{path}:\n{rendered}" if len(paths) > 1 else rendered)
|
|
125
|
+
return Result(
|
|
126
|
+
output="\n\n".join(b for b in blocks if b),
|
|
127
|
+
error="\n".join(errors),
|
|
128
|
+
code=1 if errors else 0,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Cat(Command):
|
|
133
|
+
name = "cat"
|
|
134
|
+
summary = "Concatenate and print files"
|
|
135
|
+
usage = "cat [file...]"
|
|
136
|
+
|
|
137
|
+
def run(
|
|
138
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
139
|
+
) -> Result:
|
|
140
|
+
_, _, paths = parse_args(args)
|
|
141
|
+
if not paths:
|
|
142
|
+
return Result.ok(stdin or "")
|
|
143
|
+
chunks: list[str] = []
|
|
144
|
+
errors: list[str] = []
|
|
145
|
+
for path in paths:
|
|
146
|
+
try:
|
|
147
|
+
chunks.append(_read_text(ctx.transport, _resolve(ctx, path)))
|
|
148
|
+
except Exception as exc: # noqa: BLE001
|
|
149
|
+
errors.append(f"cat: {path}: {exc}")
|
|
150
|
+
return Result(
|
|
151
|
+
output="".join(chunks), error="\n".join(errors), code=1 if errors else 0
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Head(Command):
|
|
156
|
+
name = "head"
|
|
157
|
+
summary = "Print the first lines of input"
|
|
158
|
+
usage = "head [-n N] [file]"
|
|
159
|
+
|
|
160
|
+
def run(
|
|
161
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
162
|
+
) -> Result:
|
|
163
|
+
_, values, paths = parse_args(args, valued=("n",))
|
|
164
|
+
count = _int(values.get("n"), 10)
|
|
165
|
+
try:
|
|
166
|
+
text = _input_text(ctx, paths, stdin)
|
|
167
|
+
except Exception as exc: # noqa: BLE001
|
|
168
|
+
return Result.fail(f"head: {paths[0]}: {exc}")
|
|
169
|
+
return Result.ok("\n".join(text.splitlines()[:count]))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Tail(Command):
|
|
173
|
+
name = "tail"
|
|
174
|
+
summary = "Print the last lines of input (-f to follow)"
|
|
175
|
+
usage = "tail [-n N] [-f] [file]"
|
|
176
|
+
|
|
177
|
+
def run(
|
|
178
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
179
|
+
) -> Result:
|
|
180
|
+
flags, values, paths = parse_args(args, valued=("n",))
|
|
181
|
+
count = _int(values.get("n"), 10)
|
|
182
|
+
follow = "f" in flags
|
|
183
|
+
|
|
184
|
+
if not paths:
|
|
185
|
+
if follow:
|
|
186
|
+
return Result.fail("tail: -f requires a file")
|
|
187
|
+
return Result.ok("\n".join((stdin or "").splitlines()[-count:]))
|
|
188
|
+
|
|
189
|
+
path = _resolve(ctx, paths[0])
|
|
190
|
+
try:
|
|
191
|
+
text = _read_text(ctx.transport, path)
|
|
192
|
+
except Exception as exc: # noqa: BLE001
|
|
193
|
+
return Result.fail(f"tail: {paths[0]}: {exc}")
|
|
194
|
+
|
|
195
|
+
if not follow:
|
|
196
|
+
return Result.ok("\n".join(text.splitlines()[-count:]))
|
|
197
|
+
return self._follow(ctx, path, text, count)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _follow(ctx: ShellContext, path: str, text: str, count: int) -> Result:
|
|
201
|
+
initial = "\n".join(text.splitlines()[-count:])
|
|
202
|
+
if initial:
|
|
203
|
+
sys.stdout.write(initial + "\n")
|
|
204
|
+
sys.stdout.flush()
|
|
205
|
+
seen = len(text)
|
|
206
|
+
try:
|
|
207
|
+
while True:
|
|
208
|
+
time.sleep(1.0)
|
|
209
|
+
try:
|
|
210
|
+
current = _read_text(ctx.transport, path)
|
|
211
|
+
except Exception: # noqa: BLE001
|
|
212
|
+
continue
|
|
213
|
+
if len(current) > seen:
|
|
214
|
+
sys.stdout.write(current[seen:])
|
|
215
|
+
sys.stdout.flush()
|
|
216
|
+
seen = len(current)
|
|
217
|
+
except KeyboardInterrupt:
|
|
218
|
+
sys.stdout.write("\n")
|
|
219
|
+
return Result()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Find(Command):
|
|
223
|
+
name = "find"
|
|
224
|
+
summary = "Walk the tree, filtering by name/type"
|
|
225
|
+
usage = "find [path] [-name PATTERN] [-type f|d]"
|
|
226
|
+
|
|
227
|
+
def run(
|
|
228
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
229
|
+
) -> Result:
|
|
230
|
+
# find uses single-dash long options (-name, -type), so it parses its own
|
|
231
|
+
# args rather than going through the short-flag splitter.
|
|
232
|
+
start_arg: str | None = None
|
|
233
|
+
name_pat: str | None = None
|
|
234
|
+
type_filter: str | None = None
|
|
235
|
+
i = 0
|
|
236
|
+
while i < len(args):
|
|
237
|
+
arg = args[i]
|
|
238
|
+
if arg in ("-name", "-iname"):
|
|
239
|
+
i += 1
|
|
240
|
+
name_pat = args[i] if i < len(args) else None
|
|
241
|
+
elif arg == "-type":
|
|
242
|
+
i += 1
|
|
243
|
+
type_filter = args[i] if i < len(args) else None
|
|
244
|
+
elif not arg.startswith("-") and start_arg is None:
|
|
245
|
+
start_arg = arg
|
|
246
|
+
i += 1
|
|
247
|
+
start = _resolve(ctx, start_arg) if start_arg else ctx.cwd
|
|
248
|
+
results: list[str] = []
|
|
249
|
+
errors: list[str] = []
|
|
250
|
+
|
|
251
|
+
def walk(path: str) -> None:
|
|
252
|
+
try:
|
|
253
|
+
infos = ctx.transport.list_files(path)
|
|
254
|
+
except Exception as exc: # noqa: BLE001
|
|
255
|
+
errors.append(f"find: {path}: {exc}")
|
|
256
|
+
return
|
|
257
|
+
for info in sorted(infos, key=lambda i: i.name):
|
|
258
|
+
full = posixpath.join(path, info.name)
|
|
259
|
+
is_dir = _is_dir(info)
|
|
260
|
+
if self._matches(info.name, is_dir, name_pat, type_filter):
|
|
261
|
+
results.append(full)
|
|
262
|
+
if is_dir:
|
|
263
|
+
walk(full)
|
|
264
|
+
|
|
265
|
+
walk(start)
|
|
266
|
+
return Result(
|
|
267
|
+
output="\n".join(results),
|
|
268
|
+
error="\n".join(errors),
|
|
269
|
+
code=1 if errors and not results else 0,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _matches(
|
|
274
|
+
name: str, is_dir: bool, name_pat: str | None, type_filter: str | None
|
|
275
|
+
) -> bool:
|
|
276
|
+
if name_pat is not None and not fnmatch.fnmatch(name, name_pat):
|
|
277
|
+
return False
|
|
278
|
+
if type_filter == "d" and not is_dir:
|
|
279
|
+
return False
|
|
280
|
+
if type_filter == "f" and is_dir:
|
|
281
|
+
return False
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class Stat(Command):
|
|
286
|
+
name = "stat"
|
|
287
|
+
summary = "Show file metadata"
|
|
288
|
+
usage = "stat <path...>"
|
|
289
|
+
|
|
290
|
+
def run(
|
|
291
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
292
|
+
) -> Result:
|
|
293
|
+
_, _, paths = parse_args(args)
|
|
294
|
+
if not paths:
|
|
295
|
+
return Result.fail("stat: usage: stat <path...>")
|
|
296
|
+
blocks: list[str] = []
|
|
297
|
+
errors: list[str] = []
|
|
298
|
+
for path in paths:
|
|
299
|
+
resolved = _resolve(ctx, path)
|
|
300
|
+
try:
|
|
301
|
+
info = ctx.transport.list_files(resolved, itself=True)[0]
|
|
302
|
+
except Exception as exc: # noqa: BLE001
|
|
303
|
+
errors.append(f"stat: {path}: {exc}")
|
|
304
|
+
continue
|
|
305
|
+
blocks.append(self._format(info, resolved))
|
|
306
|
+
return Result(
|
|
307
|
+
output="\n".join(blocks), error="\n".join(errors), code=1 if errors else 0
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def _format(info: Any, path: str) -> str:
|
|
312
|
+
perms = getattr(info, "permissions", None)
|
|
313
|
+
mode = f"{perms:o}" if perms is not None else "?"
|
|
314
|
+
size = getattr(info, "size", None)
|
|
315
|
+
owner = f"{getattr(info, 'user', '?')}:{getattr(info, 'group', '?')}"
|
|
316
|
+
mtime = getattr(info, "last_modified", None)
|
|
317
|
+
return (
|
|
318
|
+
f" File: {path}\n"
|
|
319
|
+
f" Type: {_type_char(info)} Mode: {mode} Owner: {owner}\n"
|
|
320
|
+
f" Size: {size if size is not None else '?'} "
|
|
321
|
+
f"Modified: {mtime.isoformat() if mtime else '?'}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class Grep(Command):
|
|
326
|
+
name = "grep"
|
|
327
|
+
summary = "Search input for a pattern"
|
|
328
|
+
usage = "grep [-i] [-v] [-n] [-c] PATTERN [file...]"
|
|
329
|
+
|
|
330
|
+
def run(
|
|
331
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
332
|
+
) -> Result:
|
|
333
|
+
flags, _, positionals = parse_args(args)
|
|
334
|
+
if not positionals:
|
|
335
|
+
return Result.fail(
|
|
336
|
+
"grep: usage: grep [-i] [-v] [-n] [-c] PATTERN [file...]"
|
|
337
|
+
)
|
|
338
|
+
pattern, files = positionals[0], positionals[1:]
|
|
339
|
+
try:
|
|
340
|
+
regex = re.compile(pattern, re.IGNORECASE if "i" in flags else 0)
|
|
341
|
+
except re.error as exc:
|
|
342
|
+
return Result.fail(f"grep: invalid pattern: {exc}")
|
|
343
|
+
|
|
344
|
+
invert, show_num, count_only = "v" in flags, "n" in flags, "c" in flags
|
|
345
|
+
sources: list[tuple[str | None, str]] = []
|
|
346
|
+
errors: list[str] = []
|
|
347
|
+
if files:
|
|
348
|
+
for path in files:
|
|
349
|
+
try:
|
|
350
|
+
sources.append(
|
|
351
|
+
(path, _read_text(ctx.transport, _resolve(ctx, path)))
|
|
352
|
+
)
|
|
353
|
+
except Exception as exc: # noqa: BLE001
|
|
354
|
+
errors.append(f"grep: {path}: {exc}")
|
|
355
|
+
else:
|
|
356
|
+
sources.append((None, stdin or ""))
|
|
357
|
+
|
|
358
|
+
multi = len(files) > 1
|
|
359
|
+
out: list[str] = []
|
|
360
|
+
matched = False
|
|
361
|
+
for label, text in sources:
|
|
362
|
+
count = 0
|
|
363
|
+
for num, line in enumerate(text.splitlines(), 1):
|
|
364
|
+
hit = bool(regex.search(line)) != invert
|
|
365
|
+
if not hit:
|
|
366
|
+
continue
|
|
367
|
+
matched = True
|
|
368
|
+
count += 1
|
|
369
|
+
if not count_only:
|
|
370
|
+
prefix = (f"{label}:" if multi else "") + (
|
|
371
|
+
f"{num}:" if show_num else ""
|
|
372
|
+
)
|
|
373
|
+
out.append(prefix + line)
|
|
374
|
+
if count_only:
|
|
375
|
+
out.append((f"{label}:" if multi else "") + str(count))
|
|
376
|
+
return Result(
|
|
377
|
+
output="\n".join(out),
|
|
378
|
+
error="\n".join(errors),
|
|
379
|
+
code=0 if matched else 1,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class Cp(Command):
|
|
384
|
+
name = "cp"
|
|
385
|
+
summary = "Copy a file"
|
|
386
|
+
usage = "cp <src> <dst>"
|
|
387
|
+
|
|
388
|
+
def run(
|
|
389
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
390
|
+
) -> Result:
|
|
391
|
+
_, _, paths = parse_args(args)
|
|
392
|
+
if len(paths) != 2:
|
|
393
|
+
return Result.fail("cp: usage: cp <src> <dst>")
|
|
394
|
+
src, dst = _resolve(ctx, paths[0]), _resolve(ctx, paths[1])
|
|
395
|
+
try:
|
|
396
|
+
data = _read_bytes(ctx.transport, src)
|
|
397
|
+
except Exception as exc: # noqa: BLE001
|
|
398
|
+
return Result.fail(f"cp: {paths[0]}: {exc}")
|
|
399
|
+
try:
|
|
400
|
+
ctx.transport.push(_dest_path(ctx, dst, src), data)
|
|
401
|
+
except Exception as exc: # noqa: BLE001
|
|
402
|
+
return Result.fail(f"cp: {paths[1]}: {exc}")
|
|
403
|
+
return Result()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class Mv(Command):
|
|
407
|
+
name = "mv"
|
|
408
|
+
summary = "Move or rename a file"
|
|
409
|
+
usage = "mv <src> <dst>"
|
|
410
|
+
|
|
411
|
+
def run(
|
|
412
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
413
|
+
) -> Result:
|
|
414
|
+
_, _, paths = parse_args(args)
|
|
415
|
+
if len(paths) != 2:
|
|
416
|
+
return Result.fail("mv: usage: mv <src> <dst>")
|
|
417
|
+
src, dst = _resolve(ctx, paths[0]), _resolve(ctx, paths[1])
|
|
418
|
+
try:
|
|
419
|
+
data = _read_bytes(ctx.transport, src)
|
|
420
|
+
ctx.transport.push(_dest_path(ctx, dst, src), data)
|
|
421
|
+
ctx.transport.remove_path(src)
|
|
422
|
+
except Exception as exc: # noqa: BLE001
|
|
423
|
+
return Result.fail(f"mv: {exc}")
|
|
424
|
+
return Result()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class Rm(Command):
|
|
428
|
+
name = "rm"
|
|
429
|
+
summary = "Remove files or directories"
|
|
430
|
+
usage = "rm [-r] [-f] <path...>"
|
|
431
|
+
|
|
432
|
+
def run(
|
|
433
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
434
|
+
) -> Result:
|
|
435
|
+
flags, _, paths = parse_args(args)
|
|
436
|
+
if not paths:
|
|
437
|
+
return Result.fail("rm: usage: rm [-r] [-f] <path...>")
|
|
438
|
+
recursive = "r" in flags or "R" in flags
|
|
439
|
+
force = "f" in flags
|
|
440
|
+
errors: list[str] = []
|
|
441
|
+
for path in paths:
|
|
442
|
+
try:
|
|
443
|
+
ctx.transport.remove_path(_resolve(ctx, path), recursive=recursive)
|
|
444
|
+
except Exception as exc: # noqa: BLE001
|
|
445
|
+
if not force:
|
|
446
|
+
errors.append(f"rm: {path}: {exc}")
|
|
447
|
+
return Result(error="\n".join(errors), code=1 if errors else 0)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class Mkdir(Command):
|
|
451
|
+
name = "mkdir"
|
|
452
|
+
summary = "Create directories"
|
|
453
|
+
usage = "mkdir [-p] <path...>"
|
|
454
|
+
|
|
455
|
+
def run(
|
|
456
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
457
|
+
) -> Result:
|
|
458
|
+
flags, _, paths = parse_args(args)
|
|
459
|
+
if not paths:
|
|
460
|
+
return Result.fail("mkdir: usage: mkdir [-p] <path...>")
|
|
461
|
+
parents = "p" in flags
|
|
462
|
+
errors: list[str] = []
|
|
463
|
+
for path in paths:
|
|
464
|
+
try:
|
|
465
|
+
ctx.transport.make_dir(_resolve(ctx, path), make_parents=parents)
|
|
466
|
+
except Exception as exc: # noqa: BLE001
|
|
467
|
+
errors.append(f"mkdir: {path}: {exc}")
|
|
468
|
+
return Result(error="\n".join(errors), code=1 if errors else 0)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
class Touch(Command):
|
|
472
|
+
name = "touch"
|
|
473
|
+
summary = "Create an empty file if it does not exist"
|
|
474
|
+
usage = "touch <path...>"
|
|
475
|
+
|
|
476
|
+
def run(
|
|
477
|
+
self, ctx: ShellContext, args: list[str], stdin: str | None = None
|
|
478
|
+
) -> Result:
|
|
479
|
+
_, _, paths = parse_args(args)
|
|
480
|
+
if not paths:
|
|
481
|
+
return Result.fail("touch: usage: touch <path...>")
|
|
482
|
+
errors: list[str] = []
|
|
483
|
+
for path in paths:
|
|
484
|
+
resolved = _resolve(ctx, path)
|
|
485
|
+
try:
|
|
486
|
+
ctx.transport.list_files(resolved, itself=True)
|
|
487
|
+
continue # already exists; Pebble can't bump mtime, so leave it
|
|
488
|
+
except Exception: # noqa: BLE001
|
|
489
|
+
pass
|
|
490
|
+
try:
|
|
491
|
+
ctx.transport.push(resolved, "")
|
|
492
|
+
except Exception as exc: # noqa: BLE001
|
|
493
|
+
errors.append(f"touch: {path}: {exc}")
|
|
494
|
+
return Result(error="\n".join(errors), code=1 if errors else 0)
|