fss-cli 0.11.0__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.
- fss_cli/__init__.py +24 -0
- fss_cli/_accounts.py +29 -0
- fss_cli/_app.py +663 -0
- fss_cli/_cat.py +391 -0
- fss_cli/_command.py +368 -0
- fss_cli/_concurrent.py +181 -0
- fss_cli/_content.py +94 -0
- fss_cli/_copy_file.py +65 -0
- fss_cli/_cp.py +740 -0
- fss_cli/_diagnostics.py +12 -0
- fss_cli/_du.py +90 -0
- fss_cli/_find.py +109 -0
- fss_cli/_head_tail.py +128 -0
- fss_cli/_info.py +179 -0
- fss_cli/_listing.py +289 -0
- fss_cli/_logging.py +72 -0
- fss_cli/_ls.py +435 -0
- fss_cli/_manifest.py +264 -0
- fss_cli/_metadata.py +70 -0
- fss_cli/_mkdir.py +97 -0
- fss_cli/_mv.py +146 -0
- fss_cli/_path.py +62 -0
- fss_cli/_post_copy.py +191 -0
- fss_cli/_progress.py +132 -0
- fss_cli/_recursive_cp.py +908 -0
- fss_cli/_recursive_rm.py +335 -0
- fss_cli/_results.py +115 -0
- fss_cli/_rm.py +149 -0
- fss_cli/_rmdir.py +148 -0
- fss_cli/_size.py +95 -0
- fss_cli/_sources.py +178 -0
- fss_cli/_stat.py +196 -0
- fss_cli/_test.py +59 -0
- fss_cli/_tree.py +208 -0
- fss_cli/_unlink.py +90 -0
- fss_cli/_walk.py +141 -0
- fss_cli-0.11.0.dist-info/METADATA +144 -0
- fss_cli-0.11.0.dist-info/RECORD +40 -0
- fss_cli-0.11.0.dist-info/WHEEL +4 -0
- fss_cli-0.11.0.dist-info/licenses/LICENSE +661 -0
fss_cli/_app.py
ADDED
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""Embedded Typer application for mapped async fsspec sources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable, Coroutine, Mapping, Sequence
|
|
7
|
+
from contextlib import AbstractAsyncContextManager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import Annotated, Any, Literal, TypeAlias, TypedDict
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from fsspec import AbstractFileSystem
|
|
14
|
+
|
|
15
|
+
from ._cat import _run_cat, _StdinOperand
|
|
16
|
+
from ._command import (
|
|
17
|
+
_MappedOperand,
|
|
18
|
+
_parse_mapped_operand,
|
|
19
|
+
_usage_error,
|
|
20
|
+
)
|
|
21
|
+
from ._cp import _DEFAULT_METADATA_WAIT_SECONDS, _cp_plan, _run_cp
|
|
22
|
+
from ._diagnostics import _render_diagnostic_prefix, _render_diagnostic_value
|
|
23
|
+
from ._du import _DuRequest, _run_du
|
|
24
|
+
from ._find import _FindRequest, _run_find
|
|
25
|
+
from ._head_tail import _read_head, _read_tail, _run_byte_range
|
|
26
|
+
from ._info import _run_info
|
|
27
|
+
from ._ls import _LsRequest, _run_ls
|
|
28
|
+
from ._mkdir import _MkdirRequest, _run_mkdir
|
|
29
|
+
from ._mv import _plan_mv, _run_mv
|
|
30
|
+
from ._path import _has_dot_segment, _has_final_dot_segment, _is_root
|
|
31
|
+
from ._rm import _RmRequest, _run_rm
|
|
32
|
+
from ._rmdir import _run_rmdir
|
|
33
|
+
from ._size import _run_size
|
|
34
|
+
from ._stat import _run_stat
|
|
35
|
+
from ._test import _run_test
|
|
36
|
+
from ._tree import _run_tree, _TreeRequest
|
|
37
|
+
from ._unlink import _run_unlink
|
|
38
|
+
|
|
39
|
+
AsyncFilesystemSource: TypeAlias = Callable[[], AbstractAsyncContextManager[AbstractFileSystem]]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RecursionCapabilities(TypedDict, total=False):
|
|
43
|
+
"""Application policy for recursive core commands."""
|
|
44
|
+
|
|
45
|
+
copy: bool
|
|
46
|
+
remove: bool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AppCapabilities(TypedDict, total=False):
|
|
50
|
+
"""Application-level core command policy."""
|
|
51
|
+
|
|
52
|
+
recursion: RecursionCapabilities
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
CommandCallback: TypeAlias = Callable[..., None]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class CommandContext:
|
|
60
|
+
"""Immutable application context available to command callbacks."""
|
|
61
|
+
|
|
62
|
+
sources: Mapping[str, AsyncFilesystemSource]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class _Capabilities:
|
|
67
|
+
recursive_copy: bool = True
|
|
68
|
+
recursive_remove: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Shared parameter annotations. Typer reads the metavar, option spellings, and
|
|
72
|
+
# help text from these aliases, so every command that takes the same operand or
|
|
73
|
+
# option shape stays spelled and documented identically.
|
|
74
|
+
_Operand: TypeAlias = Annotated[str, typer.Argument(metavar="name:/path")]
|
|
75
|
+
_Operands: TypeAlias = Annotated[list[str], typer.Argument(metavar="name:/path")]
|
|
76
|
+
_OptionalOperands: TypeAlias = Annotated[
|
|
77
|
+
list[str] | None,
|
|
78
|
+
typer.Argument(metavar="name:/path"),
|
|
79
|
+
]
|
|
80
|
+
_CatOperands: TypeAlias = Annotated[
|
|
81
|
+
list[str] | None,
|
|
82
|
+
typer.Argument(metavar="name:/path|-"),
|
|
83
|
+
]
|
|
84
|
+
_CpOperands: TypeAlias = Annotated[
|
|
85
|
+
list[str],
|
|
86
|
+
typer.Argument(metavar="SOURCE... DESTINATION"),
|
|
87
|
+
]
|
|
88
|
+
_MvOperands: TypeAlias = Annotated[
|
|
89
|
+
list[str],
|
|
90
|
+
typer.Argument(
|
|
91
|
+
metavar="name:/path",
|
|
92
|
+
help="One or more source files followed by one destination.",
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
_ByteCount: TypeAlias = Annotated[
|
|
96
|
+
int,
|
|
97
|
+
typer.Option("-c", metavar="N", min=0, help="Number of bytes to display."),
|
|
98
|
+
]
|
|
99
|
+
_MaxDepth: TypeAlias = Annotated[
|
|
100
|
+
int | None,
|
|
101
|
+
typer.Option(
|
|
102
|
+
"--maxdepth",
|
|
103
|
+
metavar="N",
|
|
104
|
+
min=0,
|
|
105
|
+
help="Descend at most N levels below the operand.",
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
_FindType: TypeAlias = Annotated[
|
|
109
|
+
Literal["f", "d"],
|
|
110
|
+
typer.Option("--type", metavar="f|d", help="Report files (f) or directories (d)."),
|
|
111
|
+
]
|
|
112
|
+
_AlmostAll: TypeAlias = Annotated[
|
|
113
|
+
bool,
|
|
114
|
+
typer.Option("-A", help="Include entries whose name begins with a dot."),
|
|
115
|
+
]
|
|
116
|
+
_LongListing: TypeAlias = Annotated[
|
|
117
|
+
bool,
|
|
118
|
+
typer.Option("-l", help="Use the long listing format."),
|
|
119
|
+
]
|
|
120
|
+
_HumanSizes: TypeAlias = Annotated[
|
|
121
|
+
bool,
|
|
122
|
+
typer.Option("-h", help="Print sizes in human-readable units."),
|
|
123
|
+
]
|
|
124
|
+
_Summarize: TypeAlias = Annotated[
|
|
125
|
+
bool,
|
|
126
|
+
typer.Option("-s", help="Print only the total for the operand."),
|
|
127
|
+
]
|
|
128
|
+
_CreateParents: TypeAlias = Annotated[
|
|
129
|
+
bool,
|
|
130
|
+
typer.Option("-p", help="Create missing parent directories."),
|
|
131
|
+
]
|
|
132
|
+
_CopyRecursive: TypeAlias = Annotated[
|
|
133
|
+
bool,
|
|
134
|
+
typer.Option("-R", "-r", help="Copy one directory and its descendants."),
|
|
135
|
+
]
|
|
136
|
+
_MetadataWait: TypeAlias = Annotated[
|
|
137
|
+
int,
|
|
138
|
+
typer.Option(
|
|
139
|
+
"--metadata-wait",
|
|
140
|
+
metavar="SECONDS",
|
|
141
|
+
min=0,
|
|
142
|
+
help=(
|
|
143
|
+
"Seconds to poll after each successful file copy while destination info "
|
|
144
|
+
"still shows no size, or the size from before this copy."
|
|
145
|
+
),
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
_TestExists: TypeAlias = Annotated[
|
|
149
|
+
bool,
|
|
150
|
+
typer.Option("-e", help="Succeed when the operand exists."),
|
|
151
|
+
]
|
|
152
|
+
_TestDirectory: TypeAlias = Annotated[
|
|
153
|
+
bool,
|
|
154
|
+
typer.Option("-d", help="Succeed when the operand is a directory."),
|
|
155
|
+
]
|
|
156
|
+
_TestFile: TypeAlias = Annotated[
|
|
157
|
+
bool,
|
|
158
|
+
typer.Option("-f", help="Succeed when the operand is a regular file."),
|
|
159
|
+
]
|
|
160
|
+
# The `rm` help text is compatibility surface: with recursion disabled the
|
|
161
|
+
# rendered help must contain no `-R` or `-r` spelling at all.
|
|
162
|
+
_RmDirectory: TypeAlias = Annotated[
|
|
163
|
+
bool,
|
|
164
|
+
typer.Option("-d", help="Remove empty directories."),
|
|
165
|
+
]
|
|
166
|
+
_RmForce: TypeAlias = Annotated[
|
|
167
|
+
bool,
|
|
168
|
+
typer.Option("-f", help="Ignore missing operands."),
|
|
169
|
+
]
|
|
170
|
+
_RmVerbose: TypeAlias = Annotated[
|
|
171
|
+
int,
|
|
172
|
+
typer.Option("-v", count=True, help="Print removed operands."),
|
|
173
|
+
]
|
|
174
|
+
_RmRecursive: TypeAlias = Annotated[
|
|
175
|
+
bool,
|
|
176
|
+
typer.Option("-R", "-r", help="Remove directory trees with guarded traversal."),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _snapshot_capabilities(capabilities: AppCapabilities | None) -> _Capabilities:
|
|
181
|
+
if capabilities is None:
|
|
182
|
+
return _Capabilities()
|
|
183
|
+
if not isinstance(capabilities, Mapping):
|
|
184
|
+
msg = "capabilities must be a mapping"
|
|
185
|
+
raise TypeError(msg)
|
|
186
|
+
for name in capabilities:
|
|
187
|
+
if name != "recursion":
|
|
188
|
+
msg = f"capabilities.{name}: unknown capability"
|
|
189
|
+
raise ValueError(msg)
|
|
190
|
+
|
|
191
|
+
recursion = capabilities.get("recursion", {})
|
|
192
|
+
if not isinstance(recursion, Mapping):
|
|
193
|
+
msg = "capabilities.recursion must be a mapping"
|
|
194
|
+
raise TypeError(msg)
|
|
195
|
+
for name in recursion:
|
|
196
|
+
if name not in {"copy", "remove"}:
|
|
197
|
+
msg = f"capabilities.recursion.{name}: unknown capability"
|
|
198
|
+
raise ValueError(msg)
|
|
199
|
+
for name in ("copy", "remove"):
|
|
200
|
+
if name in recursion and type(recursion[name]) is not bool:
|
|
201
|
+
msg = f"capabilities.recursion.{name} must be a bool"
|
|
202
|
+
raise TypeError(msg)
|
|
203
|
+
return _Capabilities(
|
|
204
|
+
recursive_copy=recursion.get("copy", True),
|
|
205
|
+
recursive_remove=recursion.get("remove", False),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _validate_source_name(name: object) -> None:
|
|
210
|
+
if not isinstance(name, str):
|
|
211
|
+
msg = "async filesystem source names must be strings"
|
|
212
|
+
raise TypeError(msg)
|
|
213
|
+
if not name or any(character in name for character in (":", "\0", "\n")):
|
|
214
|
+
msg = "async filesystem source names must be non-empty and contain no colon, NUL, or newline"
|
|
215
|
+
raise ValueError(msg)
|
|
216
|
+
if name.startswith("-"):
|
|
217
|
+
msg = "async filesystem source names must not start with '-'"
|
|
218
|
+
raise ValueError(msg)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _ensure_no_active_event_loop(command: str) -> None:
|
|
222
|
+
try:
|
|
223
|
+
asyncio.get_running_loop()
|
|
224
|
+
except RuntimeError:
|
|
225
|
+
return
|
|
226
|
+
prefix = _render_diagnostic_prefix(command)
|
|
227
|
+
typer.echo(
|
|
228
|
+
f"{prefix} cannot run from an active event loop",
|
|
229
|
+
err=True,
|
|
230
|
+
color=True,
|
|
231
|
+
)
|
|
232
|
+
raise typer.Exit(1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _run_async_command(
|
|
236
|
+
command: str,
|
|
237
|
+
operation: Callable[[], Coroutine[Any, Any, None]],
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Run one preflighted command coroutine from a synchronous callback."""
|
|
240
|
+
_ensure_no_active_event_loop(command)
|
|
241
|
+
asyncio.run(operation())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class App:
|
|
245
|
+
"""Embedded core commands plus explicitly selected extensions."""
|
|
246
|
+
|
|
247
|
+
typer_app: typer.Typer
|
|
248
|
+
|
|
249
|
+
def __init__(
|
|
250
|
+
self,
|
|
251
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
252
|
+
*,
|
|
253
|
+
capabilities: AppCapabilities | None = None,
|
|
254
|
+
extensions: Sequence[CommandCallback] = (),
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Snapshot sources and register the requested command surface."""
|
|
257
|
+
self._sources = MappingProxyType(dict(sources))
|
|
258
|
+
if not self._sources:
|
|
259
|
+
msg = "at least one async filesystem source is required"
|
|
260
|
+
raise ValueError(msg)
|
|
261
|
+
for name in self._sources:
|
|
262
|
+
_validate_source_name(name)
|
|
263
|
+
self._capabilities = _snapshot_capabilities(capabilities)
|
|
264
|
+
|
|
265
|
+
self.typer_app = typer.Typer(add_completion=False)
|
|
266
|
+
self._register_commands()
|
|
267
|
+
for extension in extensions:
|
|
268
|
+
self.typer_app.command()(extension)
|
|
269
|
+
|
|
270
|
+
# Operand preflight shared by the command callbacks.
|
|
271
|
+
|
|
272
|
+
def _mapped(self, command: str, spelling: str) -> _MappedOperand:
|
|
273
|
+
"""Parse one ``name:/path`` operand against the configured sources."""
|
|
274
|
+
return _parse_mapped_operand(command, spelling, self._sources)
|
|
275
|
+
|
|
276
|
+
def _mapped_all(
|
|
277
|
+
self,
|
|
278
|
+
command: str,
|
|
279
|
+
spellings: Sequence[str],
|
|
280
|
+
) -> tuple[_MappedOperand, ...]:
|
|
281
|
+
"""Parse every ``name:/path`` operand in invocation order."""
|
|
282
|
+
return tuple(self._mapped(command, spelling) for spelling in spellings)
|
|
283
|
+
|
|
284
|
+
def _destructive_operand(self, command: str, spelling: str) -> _MappedOperand:
|
|
285
|
+
"""Parse one operand and reject roots and final dot segments."""
|
|
286
|
+
operand = self._mapped(command, spelling)
|
|
287
|
+
if _is_root(operand.path) or _has_final_dot_segment(operand.path):
|
|
288
|
+
rendered = _render_diagnostic_value(spelling)
|
|
289
|
+
_usage_error(command, f"{rendered}: rejected path")
|
|
290
|
+
return operand
|
|
291
|
+
|
|
292
|
+
# Command registration.
|
|
293
|
+
|
|
294
|
+
def _register_commands(self) -> None:
|
|
295
|
+
@self.typer_app.callback()
|
|
296
|
+
def root(ctx: typer.Context) -> None:
|
|
297
|
+
ctx.obj = CommandContext(self._sources)
|
|
298
|
+
|
|
299
|
+
self._register_query_commands()
|
|
300
|
+
self._register_listing_commands()
|
|
301
|
+
self._register_read_commands()
|
|
302
|
+
self._register_transfer_commands()
|
|
303
|
+
self._register_mutation_commands()
|
|
304
|
+
|
|
305
|
+
def _register_query_commands(self) -> None:
|
|
306
|
+
"""Register the metadata and predicate commands."""
|
|
307
|
+
|
|
308
|
+
@self.typer_app.command()
|
|
309
|
+
def info(operand: _Operand) -> None:
|
|
310
|
+
"""Display normalized file information."""
|
|
311
|
+
mapped = self._mapped("info", operand)
|
|
312
|
+
_run_async_command(
|
|
313
|
+
"info",
|
|
314
|
+
lambda: _run_info("info", mapped, self._sources),
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
@self.typer_app.command()
|
|
318
|
+
def size(operands: _Operands) -> None:
|
|
319
|
+
"""Display exact file sizes."""
|
|
320
|
+
mapped = self._mapped_all("size", operands)
|
|
321
|
+
_run_async_command(
|
|
322
|
+
"size",
|
|
323
|
+
lambda: _run_size("size", mapped, self._sources),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
@self.typer_app.command()
|
|
327
|
+
def stat(operands: _Operands) -> None:
|
|
328
|
+
"""Display file status."""
|
|
329
|
+
mapped = self._mapped_all("stat", operands)
|
|
330
|
+
_run_async_command(
|
|
331
|
+
"stat",
|
|
332
|
+
lambda: _run_stat("stat", mapped, self._sources),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@self.typer_app.command()
|
|
336
|
+
def test(
|
|
337
|
+
operand: _Operand,
|
|
338
|
+
*,
|
|
339
|
+
exists: _TestExists = False,
|
|
340
|
+
directory: _TestDirectory = False,
|
|
341
|
+
file: _TestFile = False,
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Evaluate a file predicate."""
|
|
344
|
+
mapped = self._mapped("test", operand)
|
|
345
|
+
selected: list[Literal["e", "d", "f"]] = [
|
|
346
|
+
predicate
|
|
347
|
+
for predicate, enabled in (
|
|
348
|
+
("e", exists),
|
|
349
|
+
("d", directory),
|
|
350
|
+
("f", file),
|
|
351
|
+
)
|
|
352
|
+
if enabled
|
|
353
|
+
]
|
|
354
|
+
if len(selected) != 1:
|
|
355
|
+
_usage_error("test", "exactly one predicate selector is required")
|
|
356
|
+
_run_async_command(
|
|
357
|
+
"test",
|
|
358
|
+
lambda: _run_test("test", selected[0], mapped, self._sources),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
def _register_listing_commands(self) -> None:
|
|
362
|
+
"""Register the directory listing and traversal commands."""
|
|
363
|
+
|
|
364
|
+
@self.typer_app.command()
|
|
365
|
+
def ls(
|
|
366
|
+
operands: _Operands,
|
|
367
|
+
*,
|
|
368
|
+
include_almost_all: _AlmostAll = False,
|
|
369
|
+
long_listing: _LongListing = False,
|
|
370
|
+
human_readable: _HumanSizes = False,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""List directory contents."""
|
|
373
|
+
self._listing(
|
|
374
|
+
operands,
|
|
375
|
+
include_almost_all=include_almost_all,
|
|
376
|
+
long_listing=long_listing,
|
|
377
|
+
human_readable=human_readable,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@self.typer_app.command()
|
|
381
|
+
def du(
|
|
382
|
+
operand: _Operand,
|
|
383
|
+
*,
|
|
384
|
+
summarize: _Summarize = False,
|
|
385
|
+
human_readable: _HumanSizes = False,
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Estimate file space usage."""
|
|
388
|
+
mapped = self._mapped("du", operand)
|
|
389
|
+
_run_async_command(
|
|
390
|
+
"du",
|
|
391
|
+
lambda: _run_du(
|
|
392
|
+
"du",
|
|
393
|
+
_DuRequest(
|
|
394
|
+
summarize=summarize,
|
|
395
|
+
human_readable=human_readable,
|
|
396
|
+
operand=mapped,
|
|
397
|
+
),
|
|
398
|
+
self._sources,
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
@self.typer_app.command()
|
|
403
|
+
def find(
|
|
404
|
+
operand: _Operand,
|
|
405
|
+
maxdepth: _MaxDepth = None,
|
|
406
|
+
kind: _FindType = "f",
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Find files recursively."""
|
|
409
|
+
mapped = self._mapped("find", operand)
|
|
410
|
+
_run_async_command(
|
|
411
|
+
"find",
|
|
412
|
+
lambda: _run_find(
|
|
413
|
+
"find",
|
|
414
|
+
_FindRequest(maxdepth=maxdepth, kind=kind, operand=mapped),
|
|
415
|
+
self._sources,
|
|
416
|
+
),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
@self.typer_app.command()
|
|
420
|
+
def tree(operand: _Operand, maxdepth: _MaxDepth = None) -> None:
|
|
421
|
+
"""Display a recursive directory tree."""
|
|
422
|
+
mapped = self._mapped("tree", operand)
|
|
423
|
+
_run_async_command(
|
|
424
|
+
"tree",
|
|
425
|
+
lambda: _run_tree(
|
|
426
|
+
"tree",
|
|
427
|
+
_TreeRequest(maxdepth=maxdepth, operand=mapped),
|
|
428
|
+
self._sources,
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _listing(
|
|
433
|
+
self,
|
|
434
|
+
operands: Sequence[str],
|
|
435
|
+
*,
|
|
436
|
+
include_almost_all: bool,
|
|
437
|
+
long_listing: bool,
|
|
438
|
+
human_readable: bool,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Preflight and run one ``ls`` invocation."""
|
|
441
|
+
mapped = self._mapped_all("ls", operands)
|
|
442
|
+
if human_readable and not long_listing:
|
|
443
|
+
_usage_error("ls", "-h: requires long listing")
|
|
444
|
+
_run_async_command(
|
|
445
|
+
"ls",
|
|
446
|
+
lambda: _run_ls(
|
|
447
|
+
"ls",
|
|
448
|
+
_LsRequest(
|
|
449
|
+
include_almost_all=include_almost_all,
|
|
450
|
+
long_listing=long_listing,
|
|
451
|
+
human_readable=human_readable,
|
|
452
|
+
operands=mapped,
|
|
453
|
+
),
|
|
454
|
+
self._sources,
|
|
455
|
+
),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def _register_read_commands(self) -> None:
|
|
459
|
+
"""Register the byte-reading commands."""
|
|
460
|
+
|
|
461
|
+
@self.typer_app.command()
|
|
462
|
+
def head(operand: _Operand, count: _ByteCount) -> None:
|
|
463
|
+
"""Display leading bytes."""
|
|
464
|
+
mapped = self._mapped("head", operand)
|
|
465
|
+
_run_async_command(
|
|
466
|
+
"head",
|
|
467
|
+
lambda: _run_byte_range(
|
|
468
|
+
"head",
|
|
469
|
+
count,
|
|
470
|
+
mapped,
|
|
471
|
+
self._sources,
|
|
472
|
+
_read_head,
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
@self.typer_app.command()
|
|
477
|
+
def tail(operand: _Operand, count: _ByteCount) -> None:
|
|
478
|
+
"""Display trailing bytes."""
|
|
479
|
+
mapped = self._mapped("tail", operand)
|
|
480
|
+
_run_async_command(
|
|
481
|
+
"tail",
|
|
482
|
+
lambda: _run_byte_range(
|
|
483
|
+
"tail",
|
|
484
|
+
count,
|
|
485
|
+
mapped,
|
|
486
|
+
self._sources,
|
|
487
|
+
_read_tail,
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
@self.typer_app.command()
|
|
492
|
+
def cat(operands: _CatOperands = None) -> None:
|
|
493
|
+
"""Concatenate files to standard output."""
|
|
494
|
+
parsed = tuple(
|
|
495
|
+
_StdinOperand() if operand == "-" else _parse_mapped_operand("cat", operand, self._sources)
|
|
496
|
+
for operand in operands or ("-",)
|
|
497
|
+
)
|
|
498
|
+
_run_async_command(
|
|
499
|
+
"cat",
|
|
500
|
+
lambda: _run_cat("cat", parsed, self._sources),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def _register_transfer_commands(self) -> None:
|
|
504
|
+
"""Register ``cp`` in its capability-selected form, plus ``mv``."""
|
|
505
|
+
if self._capabilities.recursive_copy:
|
|
506
|
+
|
|
507
|
+
@self.typer_app.command()
|
|
508
|
+
def cp(
|
|
509
|
+
operands: _CpOperands,
|
|
510
|
+
*,
|
|
511
|
+
recursive: _CopyRecursive = False,
|
|
512
|
+
metadata_wait: _MetadataWait = _DEFAULT_METADATA_WAIT_SECONDS,
|
|
513
|
+
) -> None:
|
|
514
|
+
"""Copy files or one directory."""
|
|
515
|
+
self._copy(operands, recursive=recursive, metadata_wait=metadata_wait)
|
|
516
|
+
|
|
517
|
+
else:
|
|
518
|
+
|
|
519
|
+
@self.typer_app.command()
|
|
520
|
+
def cp(
|
|
521
|
+
operands: _CpOperands,
|
|
522
|
+
*,
|
|
523
|
+
metadata_wait: _MetadataWait = _DEFAULT_METADATA_WAIT_SECONDS,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Copy one or more files."""
|
|
526
|
+
self._copy(operands, recursive=False, metadata_wait=metadata_wait)
|
|
527
|
+
|
|
528
|
+
@self.typer_app.command()
|
|
529
|
+
def mv(operands: _MvOperands) -> None:
|
|
530
|
+
"""Move or rename files on one mapped filesystem."""
|
|
531
|
+
if len(operands) < 2: # noqa: PLR2004 - command arity.
|
|
532
|
+
message = "requires at least one source and one destination"
|
|
533
|
+
raise typer.BadParameter(message, param_hint="name:/path")
|
|
534
|
+
plan = _plan_mv("mv", self._mapped_all("mv", operands))
|
|
535
|
+
_run_async_command(
|
|
536
|
+
"mv",
|
|
537
|
+
lambda: _run_mv("mv", plan, self._sources),
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def _copy(self, operands: Sequence[str], *, recursive: bool, metadata_wait: int) -> None:
|
|
541
|
+
"""Preflight and run one shared ``cp`` invocation."""
|
|
542
|
+
plan = _cp_plan(
|
|
543
|
+
"cp",
|
|
544
|
+
tuple(operands),
|
|
545
|
+
self._sources,
|
|
546
|
+
recursive=recursive,
|
|
547
|
+
metadata_wait=metadata_wait,
|
|
548
|
+
)
|
|
549
|
+
_run_async_command("cp", lambda: _run_cp("cp", plan, self._sources))
|
|
550
|
+
|
|
551
|
+
def _register_mutation_commands(self) -> None:
|
|
552
|
+
"""Register the namespace creation and removal commands."""
|
|
553
|
+
|
|
554
|
+
@self.typer_app.command()
|
|
555
|
+
def mkdir(operands: _Operands, *, parents: _CreateParents = False) -> None:
|
|
556
|
+
"""Create directories."""
|
|
557
|
+
mapped = self._mapped_all("mkdir", operands)
|
|
558
|
+
_run_async_command(
|
|
559
|
+
"mkdir",
|
|
560
|
+
lambda: _run_mkdir(
|
|
561
|
+
"mkdir",
|
|
562
|
+
_MkdirRequest(create_parents=parents, operands=mapped),
|
|
563
|
+
self._sources,
|
|
564
|
+
),
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
@self.typer_app.command()
|
|
568
|
+
def rmdir(operands: _Operands) -> None:
|
|
569
|
+
"""Remove empty directories."""
|
|
570
|
+
mapped = tuple(self._destructive_operand("rmdir", operand) for operand in operands)
|
|
571
|
+
_run_async_command(
|
|
572
|
+
"rmdir",
|
|
573
|
+
lambda: _run_rmdir("rmdir", mapped, self._sources),
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@self.typer_app.command()
|
|
577
|
+
def unlink(operand: _Operand) -> None:
|
|
578
|
+
"""Remove a single file."""
|
|
579
|
+
mapped = self._destructive_operand("unlink", operand)
|
|
580
|
+
_run_async_command(
|
|
581
|
+
"unlink",
|
|
582
|
+
lambda: _run_unlink("unlink", mapped, self._sources),
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
if self._capabilities.recursive_remove:
|
|
586
|
+
|
|
587
|
+
@self.typer_app.command(name="rm")
|
|
588
|
+
def recursive_rm(
|
|
589
|
+
operands: _OptionalOperands = None,
|
|
590
|
+
*,
|
|
591
|
+
directory: _RmDirectory = False,
|
|
592
|
+
force: _RmForce = False,
|
|
593
|
+
verbose: _RmVerbose = 0,
|
|
594
|
+
recursive: _RmRecursive = False,
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Remove files or directories with guarded -R or -r."""
|
|
597
|
+
self._remove(
|
|
598
|
+
operands,
|
|
599
|
+
directory=directory,
|
|
600
|
+
force=force,
|
|
601
|
+
verbose=verbose,
|
|
602
|
+
recursive=recursive,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
else:
|
|
606
|
+
|
|
607
|
+
@self.typer_app.command(name="rm")
|
|
608
|
+
def rm(
|
|
609
|
+
operands: _OptionalOperands = None,
|
|
610
|
+
*,
|
|
611
|
+
directory: _RmDirectory = False,
|
|
612
|
+
force: _RmForce = False,
|
|
613
|
+
verbose: _RmVerbose = 0,
|
|
614
|
+
) -> None:
|
|
615
|
+
"""Remove files; -d removes empty directories."""
|
|
616
|
+
self._remove(
|
|
617
|
+
operands,
|
|
618
|
+
directory=directory,
|
|
619
|
+
force=force,
|
|
620
|
+
verbose=verbose,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
def _remove(
|
|
624
|
+
self,
|
|
625
|
+
operands: Sequence[str] | None,
|
|
626
|
+
*,
|
|
627
|
+
directory: bool,
|
|
628
|
+
force: bool,
|
|
629
|
+
verbose: int,
|
|
630
|
+
recursive: bool = False,
|
|
631
|
+
) -> None:
|
|
632
|
+
"""Preflight option combinations and paths, then run one ``rm``."""
|
|
633
|
+
if verbose > 1:
|
|
634
|
+
_usage_error("rm", "-v: may be supplied once")
|
|
635
|
+
if directory and (force or verbose or recursive):
|
|
636
|
+
_usage_error("rm", "-d: cannot combine with other options")
|
|
637
|
+
if not recursive and force and verbose:
|
|
638
|
+
_usage_error("rm", "-f: cannot combine with -v")
|
|
639
|
+
spellings = list(operands or [])
|
|
640
|
+
if not spellings and not force:
|
|
641
|
+
_usage_error("rm", "missing mapped filesystem operand")
|
|
642
|
+
mapped = self._mapped_all("rm", spellings)
|
|
643
|
+
for spelling, operand in zip(spellings, mapped, strict=True):
|
|
644
|
+
rejected = _is_root(operand.path) or (
|
|
645
|
+
_has_dot_segment(operand.path) if recursive else _has_final_dot_segment(operand.path)
|
|
646
|
+
)
|
|
647
|
+
if rejected:
|
|
648
|
+
rendered = _render_diagnostic_value(spelling)
|
|
649
|
+
_usage_error("rm", f"{rendered}: rejected path")
|
|
650
|
+
_run_async_command(
|
|
651
|
+
"rm",
|
|
652
|
+
lambda: _run_rm(
|
|
653
|
+
"rm",
|
|
654
|
+
_RmRequest(
|
|
655
|
+
force=force,
|
|
656
|
+
directory=directory,
|
|
657
|
+
recursive=recursive,
|
|
658
|
+
verbose=bool(verbose),
|
|
659
|
+
operands=mapped,
|
|
660
|
+
),
|
|
661
|
+
self._sources,
|
|
662
|
+
),
|
|
663
|
+
)
|