diff --git a/docs/changes.rst b/docs/changes.rst index 3373a22dc..9abbd4b8a 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -168,7 +168,7 @@ New features: check; default is "yes", use at your own risk, #9109. - diff: --sort-by=field[,field,...], #8998 - completion: generate completion scripts for supported shells, #9172, - uses shtab, supports bash, tcsh, zsh (zsh needs shtab > 1.7.2). + uses shtab, supports bash and zsh. Fixes: @@ -199,7 +199,7 @@ Other changes: - save space in test_create_* tests - CI/tests: add SFTP/rclone/S3 repo testing - CI: add local servers for S3 and SFTP testing - - CI: add *BSD and Haiku OS (on GitHub Actions) + - CI: add misc. BSDs and Haiku OS (on GitHub Actions) - CI: do dynamic code analysis, #6819 - transfer: add test for unexpected src repo index change, #9022 - pyproject.toml: correctly define test environments for FUSE testing @@ -222,6 +222,7 @@ Other changes: - how to debug borg mount, #5461 - document what happens when a new keyfile repo is created at the same path, #6230 - update install docs to include `SETUPTOOLS_SCM_PRETEND_VERSION` + - highlight archive series naming for fast incrementals, #8955 - add Arch Linux to the 'Installing from source' docs - add systemd-inhibit and examples, #8989 - code/docs: fix typos and grammar diff --git a/pyproject.toml b/pyproject.toml index 2a585cc7a..06b04c13f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0. "platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'", # for others: 2.6+ works consistently. "argon2-cffi", - "shtab>=1.7.0", + "shtab>=1.8.0", ] [project.optional-dependencies] diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 949c79105..abcc04f79 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -1,45 +1,87 @@ +""" +Shell completion support for Borg commands. + +This module implements the `borg completion` command, which generates shell completion +scripts for bash and zsh. It uses the shtab library for basic completion generation +and extends it with custom dynamic completions for Borg-specific argument types. + +Dynamic Completions +------------------- + +The following argument types have intelligent, context-aware completion: + +1. Archive names/IDs (archivename_validator): + - Completes archive names by default (e.g., "my-backup-2024") + - Completes archive IDs when prefixed with "aid:" (e.g., "aid:12345678") + - In zsh, shows archive metadata (name, timestamp, user@host) as descriptions + - Respects --repo/-r flags to query the correct repository + +2. Sort keys (SortBySpec): + - Completes comma-separated sort keys (timestamp, archive, name, id, tags, host, user) + - Prevents duplicate keys in the same option + +3. Files cache mode (FilesCacheMode): + - Completes comma-separated cache mode tokens (ctime, mtime, size, inode, rechunk, disabled) + - Enforces mutual exclusivity (e.g., ctime vs mtime, disabled vs others) + +4. Compression algorithms (CompressionSpec): + - Suggests compression specs with examples (lz4, zstd,3, auto,zstd,10, etc.) + +5. Chunker parameters (ChunkerParams): + - Suggests chunker param examples (default, fixed,4194304, buzhash,19,23,21,4095, etc.) + +6. Paths (PathSpec): + - Completes directories using standard shell directory completion + +7. Help topics: + - Completes help command topics and subcommand names + +8. Tags (tag_validator): + - Completes existing tags from the repository + +9. Relative time markers (relative_time_marker_validator): + - Suggests common time intervals (60S, 60M, 24H, 7d, 4w, 12m, 1000y) + +10. Timestamps (timestamp): + - Completes file paths when starting with / or . + - Otherwise suggests current timestamp in ISO format + +11. File sizes (parse_file_size): + - Suggests common file size values (500M, 1G, 10G, 100G, 1T, etc.) +""" + import argparse import shtab from ._common import process_epilog from ..constants import * # NOQA -from ..helpers import archivename_validator # used to detect ARCHIVE args for dynamic completion - -# Dynamic completion for archive IDs (aid:...) -# -# This integrates with shtab by: -# - tagging argparse actions that accept an ARCHIVE (identified by type == archivename_validator) -# with a .complete mapping pointing to our helper function. -# - using shtab.complete's 'preamble' parameter to inject the helper into the -# generated completion script for supported shells. -# -# Notes / constraints (per plan): -# - Calls `borg repo-list --format ...` and filters results by the typed aid: hex prefix. -# - Non-interactive only. We rely on Borg to fail fast without prompting in non-interactive contexts. -# If it cannot, we simply return no suggestions. - -# Name of the helper function inserted into the generated completion script(s) -AID_BASH_FN_NAME = "_borg_complete_aid" -AID_ZSH_FN_NAME = "_borg_complete_aid" +from ..helpers import ( + archivename_validator, + SortBySpec, + FilesCacheMode, + PathSpec, + ChunkerParams, + tag_validator, + relative_time_marker_validator, + parse_file_size, +) +from ..helpers.time import timestamp +from ..compress import CompressionSpec +from ..helpers.parseformat import partial_format +from ..manifest import AI_HUMAN_SORT_KEYS # Global bash preamble that is prepended to the generated completion script. # It aggregates only what we need: # - wordbreak fixes for ':' and '=' so tokens like 'aid:' and '--repo=/path' stay intact # - a minimal dynamic completion helper for aid: archive IDs -BASH_PREAMBLE = r""" +BASH_PREAMBLE_TMPL = r""" # keep ':' and '=' intact so tokens like 'aid:' and '--repo=/path' stay whole if [[ ${COMP_WORDBREAKS-} == *:* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//:}; fi if [[ ${COMP_WORDBREAKS-} == *=* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//=}; fi -# Complete aid: archive IDs by querying "borg repo-list --short" -# Note: we only suggest the first 8 hex digits (short ID) for completion. -_borg_complete_aid() { +_borg_complete_archive() { local cur="${COMP_WORDS[COMP_CWORD]}" - [[ "$cur" == aid:* ]] || return 0 - - local prefix="${cur#aid:}" - [[ -n "$prefix" && ! "$prefix" =~ ^[0-9a-fA-F]*$ ]] && return 0 # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V local repo_arg=() @@ -55,26 +97,248 @@ _borg_complete_aid() { fi done - # ask borg for raw IDs; avoid prompts and suppress stderr + # Check if completing aid: prefix + if [[ "$cur" == aid:* ]]; then + local prefix="${cur#aid:}" + [[ -n "$prefix" && ! "$prefix" =~ ^[0-9a-fA-F]*$ ]] && return 0 + + # ask borg for raw IDs; avoid prompts and suppress stderr + local out + if [[ -n "${repo_arg[*]}" ]]; then + out=$( borg repo-list "${repo_arg[@]}" --format '{id}{NL}' 2>/dev/null /dev/null /dev/null /dev/null /dev/null /dev/null /dev/null /dev/null archive IDs by querying "borg repo-list --short" -# Note: we only suggest the first 8 hex digits (short ID) for completion. -_borg_complete_aid() { +ZSH_PREAMBLE_TMPL = r""" +_borg_complete_archive() { local cur cur="${words[$CURRENT]}" - [[ "$cur" == aid:* ]] || return 0 - - local prefix="${cur#aid:}" - # allow only hex digits as prefix; empty prefix also allowed (list all) - [[ -n "$prefix" && ! "$prefix" == [0-9a-fA-F]# ]] && return 0 # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V local -a repo_arg=() @@ -110,50 +367,291 @@ _borg_complete_aid() { fi done - # ask borg for raw IDs; avoid prompts and suppress stderr + # Check if completing aid: prefix + if [[ "$cur" == aid:* ]]; then + local prefix="${cur#aid:}" + # allow only hex digits as prefix; empty prefix also allowed (list all) + [[ -n "$prefix" && ! "$prefix" == [0-9a-fA-F]# ]] && return 0 + + # ask borg for IDs with metadata; avoid prompts and suppress stderr + # Use tab as delimiter to avoid issues with spaces in archive names + local out + if (( ${#repo_arg[@]} > 0 )); then + out=$( borg repo-list "${repo_arg[@]}" --format '{id}{TAB}{archive}{TAB}{time}{TAB}{username}@{hostname}{NL}' \ + 2>/dev/null /dev/null 0 )); then + out=$( borg repo-list "${repo_arg[@]}" --format '{archive}{NL}' 2>/dev/null /dev/null 0 )); then - out=$( borg repo-list "${repo_arg[@]}" --short 2>/dev/null /dev/null /dev/null /dev/null