feat(cli): improve file discovery and completion

- Render remote files as an aligned table with JSON and TSV alternatives.
- Add Bash and Zsh completion for commands, options, paths, and remote files.
- Allow downloads by exact filename while rejecting ambiguous duplicate names.
- Cover table formatting, completion scripts, and selector resolution with tests.
This commit is contained in:
2026-07-27 17:30:43 +02:00
parent 9db36c2d5b
commit 77c76b16c4
4 changed files with 226 additions and 6 deletions
+44 -6
View File
@@ -9,8 +9,10 @@ from pathlib import Path
from .app import make_client
from .auth import login as auth_login
from .client import download, list_remote, upload
from .completion import script as completion_script
from .config import DEFAULT_CONFIG
from .sync import sync_once
from .table import format_table
DEFAULT_NODE = 'picoshare'
@@ -42,21 +44,44 @@ def _cmd_list(args) -> int:
return 1
finally:
client.close()
for item in files:
print(f"{item.get('file_id')}\t{item.get('file_name')}\t{item.get('file_size', '')}")
if args.format == 'json':
print(json.dumps(files, indent=2, ensure_ascii=False))
elif args.format == 'tsv':
for item in files:
print(f"{item.get('file_id', '')}\t{item.get('file_name', '')}\t{item.get('file_size', '')}")
else:
rows = [
[item.get('file_id'), item.get('file_name'), item.get('file_size', '')]
for item in files
]
print(format_table(['ID', 'Name', 'Size'], rows))
return 0
def _resolve_remote_file(files: list[dict], selector: str) -> dict:
id_matches = [item for item in files if item.get('file_id') == selector]
if id_matches:
return id_matches[0]
name_matches = [item for item in files if item.get('file_name') == selector]
if not name_matches:
raise FileNotFoundError(f'no remote file matches: {selector}')
if len(name_matches) > 1:
raise RuntimeError(f'multiple remote files named {selector!r}; use the file ID')
return name_matches[0]
def _cmd_download(args) -> int:
dest = Path(args.output).expanduser() if args.output else Path(args.file_id)
client = make_client(args)
try:
download(client, args.node, args.file_id, dest)
remote = _resolve_remote_file(list_remote(client, args.node), args.file)
file_id = str(remote['file_id'])
file_name = str(remote.get('file_name') or file_id)
dest = Path(args.output).expanduser() if args.output else Path(file_name)
download(client, args.node, file_id, dest)
except Exception as exc:
print(f'error: {exc}', file=sys.stderr)
return 1
finally:
client.close()
print(f'downloaded {args.file_id} -> {dest}')
print(f'downloaded {file_name} ({file_id}) -> {dest}')
return 0
def _cmd_sync(args) -> int:
@@ -74,6 +99,14 @@ def _cmd_watch(args) -> int:
return code
time.sleep(args.interval)
def _cmd_completion(args) -> int:
try:
print(completion_script(args.shell))
except ValueError as exc:
print(f'error: {exc}', file=sys.stderr)
return 1
return 0
def _add_common(parser: argparse.ArgumentParser) -> None:
parser.add_argument('--config', default=str(DEFAULT_CONFIG), help='Path to config.toml.')
parser.add_argument('--url', action='append', metavar='NODE=URL', help='Override/add a node URL.')
@@ -111,11 +144,12 @@ def main(argv: list[str] | None = None) -> int:
list_cmd = sub.add_parser('list', help='List remote files.')
_add_common(list_cmd)
list_cmd.add_argument('--format', choices=['table', 'json', 'tsv'], default='table')
list_cmd.set_defaults(func=_cmd_list)
download_cmd = sub.add_parser('download', help='Download one file via private API endpoint.')
_add_common(download_cmd)
download_cmd.add_argument('file_id')
download_cmd.add_argument('file', help='Remote file ID or exact file name.')
download_cmd.add_argument('-o', '--output')
download_cmd.set_defaults(func=_cmd_download)
@@ -136,6 +170,10 @@ def main(argv: list[str] | None = None) -> int:
watch_cmd.add_argument('--interval', type=float, default=10.0)
watch_cmd.set_defaults(func=_cmd_watch)
completion_cmd = sub.add_parser('completion', help='Print shell completion script.')
completion_cmd.add_argument('shell', choices=['zsh', 'bash'])
completion_cmd.set_defaults(func=_cmd_completion)
args = parser.parse_args(argv)
return args.func(args)