completion: support SortBySpec type options

This commit is contained in:
Thomas Waldmann 2025-11-18 21:36:36 +01:00
parent 8646c4e045
commit e376d8910c
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
2 changed files with 121 additions and 4 deletions

View file

@ -4,7 +4,8 @@ import shtab
from ._common import process_epilog
from ..constants import * # NOQA
from ..helpers import archivename_validator # used to detect ARCHIVE args for dynamic completion
from ..helpers import archivename_validator, SortBySpec # used to detect ARCHIVE args for dynamic completion
from ..manifest import AI_HUMAN_SORT_KEYS
# Dynamic completion for archive IDs (aid:...)
#
@ -23,11 +24,15 @@ from ..helpers import archivename_validator # used to detect ARCHIVE args for d
AID_BASH_FN_NAME = "_borg_complete_aid"
AID_ZSH_FN_NAME = "_borg_complete_aid"
# Name of the helper function inserted for completing SortBySpec options
SORTBY_BASH_FN_NAME = "_borg_complete_sortby"
SORTBY_ZSH_FN_NAME = "_borg_complete_sortby"
# 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
@ -75,6 +80,52 @@ _borg_complete_aid() {
done <<< "$out"
return 0
}
# Complete comma-separated sort keys for any option with type=SortBySpec.
# Keys are validated against Borg's AI_HUMAN_SORT_KEYS.
_borg_complete_sortby() {
local cur="${COMP_WORDS[COMP_CWORD]}"
# Extract value part for --opt=value forms; otherwise the value is the word itself
local val prefix_eq
if [[ "$cur" == *=* ]]; then
prefix_eq="${cur%%=*}="
val="${cur#*=}"
else
prefix_eq=""
val="$cur"
fi
# Split into head (selected keys + trailing comma if any) and fragment (last token being typed)
local head frag
if [[ "$val" == *,* ]]; then
head="${val%,*},"
frag="${val##*,}"
else
head=""
frag="$val"
fi
# Build a comma-delimited list for cheap membership testing
local headlist
if [[ -n "$head" ]]; then
headlist=",${head%,},"
else
headlist="," # nothing selected yet
fi
# Valid keys (embedded at generation time)
local keys=(___SORT_KEYS___)
local k
for k in "${keys[@]}"; do
# skip already-selected keys
[[ "$headlist" == *",${k},"* ]] && continue
# match prefix of last fragment
[[ -n "$frag" && "$k" != "$frag"* ]] && continue
printf '%s\n' "${prefix_eq}${head}${k}"
done
}
"""
@ -84,7 +135,7 @@ _borg_complete_aid() {
# - We use zsh's $words/$CURRENT arrays to inspect the command line.
# - Candidates are returned via `compadd`.
# - We try to detect repo context from --repo=V, --repo V, -r=V, -rV, -r V.
ZSH_PREAMBLE = r"""
ZSH_PREAMBLE_TMPL = r"""
# Complete aid:<hex-prefix> archive IDs by querying "borg repo-list --short"
# Note: we only suggest the first 8 hex digits (short ID) for completion.
_borg_complete_aid() {
@ -134,6 +185,50 @@ _borg_complete_aid() {
compadd -Q -- $candidates
return 0
}
# Complete comma-separated sort keys for any option with type=SortBySpec.
_borg_complete_sortby() {
local cur
cur="${words[$CURRENT]}"
local val prefix_eq
if [[ "$cur" == *"="* ]]; then
prefix_eq="${cur%%\=*}="
val="${cur#*=}"
else
prefix_eq=""
val="$cur"
fi
local head frag
if [[ "$val" == *","* ]]; then
head="${val%,*},"
frag="${val##*,}"
else
head=""
frag="$val"
fi
local headlist
if [[ -n "$head" ]]; then
headlist=",${head%,},"
else
headlist="," # nothing selected yet
fi
# Valid keys (embedded at generation time)
local -a keys=(___SORT_KEYS___)
local -a candidates=()
local k
for k in ${keys[@]}; do
[[ "$headlist" == *",${k},"* ]] && continue
[[ -n "$frag" && "$k" != "$frag"* ]] && continue
candidates+=( "${prefix_eq}${head}${k}" )
done
compadd -Q -- $candidates
return 0
}
"""
@ -156,6 +251,20 @@ def _attach_aid_completion(parser: argparse.ArgumentParser):
action.complete = {"bash": AID_BASH_FN_NAME, "zsh": AID_ZSH_FN_NAME} # type: ignore[attr-defined]
def _attach_sortby_completion(parser: argparse.ArgumentParser):
"""Tag all arguments with type SortBySpec with sort-key completion."""
for action in parser._actions:
# Recurse into subparsers
if isinstance(action, argparse._SubParsersAction):
for sub in action.choices.values():
_attach_sortby_completion(sub)
continue
if action.type is SortBySpec:
action.complete = {"bash": SORTBY_BASH_FN_NAME, "zsh": SORTBY_ZSH_FN_NAME} # type: ignore[attr-defined]
class CompletionMixIn:
def do_completion(self, args):
"""Output shell completion script for the given shell."""
@ -165,7 +274,13 @@ class CompletionMixIn:
# to enumerate archives and does not introduce any new commands or caching.
parser = self.build_parser()
_attach_aid_completion(parser)
preamble = {"bash": BASH_PREAMBLE, "zsh": ZSH_PREAMBLE}
_attach_sortby_completion(parser)
# Build preambles with embedded SortBy keys
sort_keys = " ".join(AI_HUMAN_SORT_KEYS)
bash_preamble = BASH_PREAMBLE_TMPL.replace("___SORT_KEYS___", sort_keys)
zsh_preamble = ZSH_PREAMBLE_TMPL.replace("___SORT_KEYS___", sort_keys)
preamble = {"bash": bash_preamble, "zsh": zsh_preamble}
script = shtab.complete(parser, shell=args.shell, preamble=preamble) # nosec B604
print(script)

View file

@ -8,6 +8,7 @@ def test_bash_completion(archivers, request):
archiver = request.getfixturevalue(archivers)
output = cmd(archiver, "completion", "bash")
assert "_borg_complete_aid() {" in output
assert "_borg_complete_sortby() {" in output
def test_zsh_completion(archivers, request):
@ -15,3 +16,4 @@ def test_zsh_completion(archivers, request):
archiver = request.getfixturevalue(archivers)
output = cmd(archiver, "completion", "zsh")
assert "_borg_complete_aid() {" in output
assert "_borg_complete_sortby() {" in output