mirror of
https://github.com/borgbackup/borg.git
synced 2026-05-28 04:03:21 -04:00
Merge pull request #9174 from ThomasWaldmann/shtab-master
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.7) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
Some checks are pending
Lint / lint (push) Waiting to run
CI / lint (push) Waiting to run
CI / security (push) Waiting to run
CI / asan_ubsan (push) Blocked by required conditions
CI / native_tests (push) Blocked by required conditions
CI / vm_tests (Haiku, false, haiku, r1beta5) (push) Blocked by required conditions
CI / vm_tests (NetBSD, false, netbsd, 10.1) (push) Blocked by required conditions
CI / vm_tests (OpenBSD, false, openbsd, 7.7) (push) Blocked by required conditions
CI / vm_tests (borg-freebsd-14-x86_64-gh, FreeBSD, true, freebsd, 14.3) (push) Blocked by required conditions
CI / windows_tests (push) Blocked by required conditions
CodeQL / Analyze (push) Waiting to run
completion: generate completion scripts for supported shells, fixes #9172
This commit is contained in:
commit
45fbc2f0e3
8 changed files with 363 additions and 0 deletions
76
docs/man/borg-completion.1
Normal file
76
docs/man/borg-completion.1
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.
|
||||
.nr rst2man-indent-level 0
|
||||
.
|
||||
.de1 rstReportMargin
|
||||
\\$1 \\n[an-margin]
|
||||
level \\n[rst2man-indent-level]
|
||||
level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
-
|
||||
\\n[rst2man-indent0]
|
||||
\\n[rst2man-indent1]
|
||||
\\n[rst2man-indent2]
|
||||
..
|
||||
.de1 INDENT
|
||||
.\" .rstReportMargin pre:
|
||||
. RS \\$1
|
||||
. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
|
||||
. nr rst2man-indent-level +1
|
||||
.\" .rstReportMargin post:
|
||||
..
|
||||
.de UNINDENT
|
||||
. RE
|
||||
.\" indent \\n[an-margin]
|
||||
.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.nr rst2man-indent-level -1
|
||||
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
|
||||
..
|
||||
.TH "BORG-COMPLETION" "1" "2025-11-17" "" "borg backup tool"
|
||||
.SH NAME
|
||||
borg-completion \- Output shell completion script for the given shell.
|
||||
.SH SYNOPSIS
|
||||
.sp
|
||||
borg [common options] completion [options] SHELL
|
||||
.SH DESCRIPTION
|
||||
.sp
|
||||
This command prints a shell completion script for the given shell.
|
||||
.sp
|
||||
Please note that for some dynamic completions (like archive IDs), the shell
|
||||
completion script will call borg to query the repository. This will work best
|
||||
if that call can be made without prompting for user input, so you may want to
|
||||
set BORG_REPO and BORG_PASSPHRASE environment variables.
|
||||
.SH OPTIONS
|
||||
.sp
|
||||
See \fIborg\-common(1)\fP for common options of Borg commands.
|
||||
.SS arguments
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B SHELL
|
||||
shell to generate completion for (one of: %(choices)s)
|
||||
.UNINDENT
|
||||
.SH EXAMPLES
|
||||
.sp
|
||||
To activate completion in your current shell session, evaluate the output
|
||||
of this command. To enable it persistently, add the corresponding line to
|
||||
your shell\(aqs startup file.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.EX
|
||||
# Bash (in ~/.bashrc)
|
||||
eval \(dq$(borg completion bash)\(dq
|
||||
|
||||
# Zsh (in ~/.zshrc)
|
||||
eval \(dq$(borg completion zsh)\(dq
|
||||
.EE
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SH SEE ALSO
|
||||
.sp
|
||||
\fIborg\-common(1)\fP
|
||||
.SH AUTHOR
|
||||
The Borg Collective
|
||||
.\" Generated by docutils manpage writer.
|
||||
.
|
||||
|
|
@ -68,5 +68,6 @@ Usage
|
|||
usage/benchmark
|
||||
|
||||
usage/help
|
||||
usage/completion
|
||||
usage/debug
|
||||
usage/notes
|
||||
|
|
|
|||
17
docs/usage/completion.rst
Normal file
17
docs/usage/completion.rst
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.. include:: completion.rst.inc
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
To activate completion in your current shell session, evaluate the output
|
||||
of this command. To enable it persistently, add the corresponding line to
|
||||
your shell's startup file.
|
||||
|
||||
::
|
||||
|
||||
# Bash (in ~/.bashrc)
|
||||
eval "$(borg completion bash)"
|
||||
|
||||
# Zsh (in ~/.zshrc)
|
||||
eval "$(borg completion zsh)"
|
||||
|
||||
50
docs/usage/completion.rst.inc
Normal file
50
docs/usage/completion.rst.inc
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!
|
||||
|
||||
.. _borg_completion:
|
||||
|
||||
borg completion
|
||||
---------------
|
||||
.. code-block:: none
|
||||
|
||||
borg [common options] completion [options] SHELL
|
||||
|
||||
.. only:: html
|
||||
|
||||
.. class:: borg-options-table
|
||||
|
||||
+-------------------------------------------------------+-----------+--------------------------------------------------------+
|
||||
| **positional arguments** |
|
||||
+-------------------------------------------------------+-----------+--------------------------------------------------------+
|
||||
| | ``SHELL`` | shell to generate completion for (one of: %(choices)s) |
|
||||
+-------------------------------------------------------+-----------+--------------------------------------------------------+
|
||||
| .. class:: borg-common-opt-ref |
|
||||
| |
|
||||
| :ref:`common_options` |
|
||||
+-------------------------------------------------------+-----------+--------------------------------------------------------+
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type='text/javascript'>
|
||||
$(document).ready(function () {
|
||||
$('.borg-options-table colgroup').remove();
|
||||
})
|
||||
</script>
|
||||
|
||||
.. only:: latex
|
||||
|
||||
SHELL
|
||||
shell to generate completion for (one of: %(choices)s)
|
||||
|
||||
|
||||
:ref:`common_options`
|
||||
|
|
||||
|
||||
Description
|
||||
~~~~~~~~~~~
|
||||
|
||||
This command prints a shell completion script for the given shell.
|
||||
|
||||
Please note that for some dynamic completions (like archive IDs), the shell
|
||||
completion script will call borg to query the repository. This will work best
|
||||
if that call can be made without prompting for user input, so you may want to
|
||||
set BORG_REPO and BORG_PASSPHRASE environment variables.
|
||||
|
|
@ -37,6 +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",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ from .analyze_cmd import AnalyzeMixIn
|
|||
from .benchmark_cmd import BenchmarkMixIn
|
||||
from .check_cmd import CheckMixIn
|
||||
from .compact_cmd import CompactMixIn
|
||||
from .completion_cmd import CompletionMixIn
|
||||
from .create_cmd import CreateMixIn
|
||||
from .debug_cmd import DebugMixIn
|
||||
from .delete_cmd import DeleteMixIn
|
||||
|
|
@ -112,6 +113,7 @@ class Archiver(
|
|||
BenchmarkMixIn,
|
||||
CheckMixIn,
|
||||
CompactMixIn,
|
||||
CompletionMixIn,
|
||||
CreateMixIn,
|
||||
DebugMixIn,
|
||||
DeleteMixIn,
|
||||
|
|
@ -354,6 +356,7 @@ class Archiver(
|
|||
self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_check(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_compact(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_completion(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_create(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_debug(subparsers, common_parser, mid_common_parser)
|
||||
self.build_parser_delete(subparsers, common_parser, mid_common_parser)
|
||||
|
|
|
|||
198
src/borg/archiver/completion_cmd.py
Normal file
198
src/borg/archiver/completion_cmd.py
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
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"
|
||||
|
||||
# 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"""
|
||||
# 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:<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() {
|
||||
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=()
|
||||
local i w
|
||||
for (( i=0; i<${#COMP_WORDS[@]}; i++ )); do
|
||||
w="${COMP_WORDS[i]}"
|
||||
if [[ "$w" == --repo=* ]]; then repo_arg=( --repo "${w#--repo=}" ); break
|
||||
elif [[ "$w" == -r=* ]]; then repo_arg=( -r "${w#-r=}" ); break
|
||||
elif [[ "$w" == -r* && "$w" != "-r" ]]; then repo_arg=( -r "${w#-r}" ); break
|
||||
elif [[ "$w" == "--repo" || "$w" == "-r" ]]; then
|
||||
if (( i+1 < ${#COMP_WORDS[@]} )); then repo_arg=( "$w" "${COMP_WORDS[i+1]}" ); fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# ask borg for raw IDs; avoid prompts and suppress stderr
|
||||
local out
|
||||
if [[ -n "${repo_arg[*]}" ]]; then
|
||||
out=$( borg repo-list "${repo_arg[@]}" --short 2>/dev/null </dev/null )
|
||||
else
|
||||
out=$( borg repo-list --short 2>/dev/null </dev/null )
|
||||
fi
|
||||
[[ -z "$out" ]] && return 0
|
||||
|
||||
# filter by (case-insensitive) hex prefix and emit candidates
|
||||
local IFS=$'\n' id prelower idlower
|
||||
prelower="$(printf '%s' "$prefix" | tr '[:upper:]' '[:lower:]')"
|
||||
while IFS= read -r id; do
|
||||
[[ -z "$id" ]] && continue
|
||||
idlower="$(printf '%s' "$id" | tr '[:upper:]' '[:lower:]')"
|
||||
# Print only the first 8 hex digits of the ID for completion suggestions.
|
||||
[[ "$idlower" == "$prelower"* ]] && printf 'aid:%s\n' "${id:0:8}"
|
||||
done <<< "$out"
|
||||
return 0
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# Global zsh preamble providing dynamic completion for aid:<hex> archive IDs.
|
||||
#
|
||||
# Notes:
|
||||
# - 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"""
|
||||
# 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() {
|
||||
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=()
|
||||
local i w
|
||||
for i in {1..$#words}; do
|
||||
w="$words[$i]"
|
||||
if [[ "$w" == --repo=* ]]; then repo_arg=( --repo "${w#--repo=}" ); break
|
||||
elif [[ "$w" == -r=* ]]; then repo_arg=( -r "${w#-r=}" ); break
|
||||
elif [[ "$w" == -r* && "$w" != "-r" ]]; then repo_arg=( -r "${w#-r}" ); break
|
||||
elif [[ "$w" == "--repo" || "$w" == "-r" ]]; then
|
||||
if (( i+1 <= $#words )); then repo_arg=( "$w" "${words[$((i+1))]}" ); fi
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# ask borg for raw IDs; avoid prompts and suppress stderr
|
||||
local out
|
||||
if (( ${#repo_arg[@]} > 0 )); then
|
||||
out=$( borg repo-list "${repo_arg[@]}" --short 2>/dev/null </dev/null )
|
||||
else
|
||||
out=$( borg repo-list --short 2>/dev/null </dev/null )
|
||||
fi
|
||||
[[ -z "$out" ]] && return 0
|
||||
|
||||
# filter by (case-insensitive) hex prefix and emit candidates
|
||||
local prelower id idlower
|
||||
prelower="${prefix:l}"
|
||||
local -a candidates=()
|
||||
for id in ${(f)out}; do
|
||||
[[ -z "$id" ]] && continue
|
||||
idlower="${id:l}"
|
||||
if [[ "$idlower" == "$prelower"* ]]; then
|
||||
candidates+=( "aid:${id[1,8]}" )
|
||||
fi
|
||||
done
|
||||
# -Q: do not escape special chars, so ':' remains as-is
|
||||
compadd -Q -- $candidates
|
||||
return 0
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _attach_aid_completion(parser: argparse.ArgumentParser):
|
||||
"""Tag all arguments that accept an ARCHIVE with aid:-completion.
|
||||
|
||||
We detect ARCHIVE arguments by their type being archivename_validator.
|
||||
This function mutates the parser actions to add a .complete mapping used by shtab.
|
||||
"""
|
||||
|
||||
for action in parser._actions:
|
||||
# Recurse into subparsers
|
||||
if isinstance(action, argparse._SubParsersAction):
|
||||
for sub in action.choices.values():
|
||||
_attach_aid_completion(sub)
|
||||
continue
|
||||
|
||||
# Assign dynamic completion only for arguments that take an archive name.
|
||||
if action.type is archivename_validator:
|
||||
action.complete = {"bash": AID_BASH_FN_NAME, "zsh": AID_ZSH_FN_NAME} # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class CompletionMixIn:
|
||||
def do_completion(self, args):
|
||||
"""Output shell completion script for the given shell."""
|
||||
# Automagically generates completions for subcommands and options. Also
|
||||
# adds dynamic completion for archive IDs with the aid: prefix for all ARCHIVE
|
||||
# arguments (identified by archivename_validator). It reuses `borg repo-list`
|
||||
# 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}
|
||||
script = shtab.complete(parser, shell=args.shell, preamble=preamble) # nosec B604
|
||||
print(script)
|
||||
|
||||
def build_parser_completion(self, subparsers, common_parser, mid_common_parser):
|
||||
shells = tuple(shtab.SUPPORTED_SHELLS)
|
||||
|
||||
completion_epilog = process_epilog(
|
||||
"""
|
||||
This command prints a shell completion script for the given shell.
|
||||
|
||||
Please note that for some dynamic completions (like archive IDs), the shell
|
||||
completion script will call borg to query the repository. This will work best
|
||||
if that call can be made without prompting for user input, so you may want to
|
||||
set BORG_REPO and BORG_PASSPHRASE environment variables.
|
||||
"""
|
||||
)
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
"completion",
|
||||
parents=[common_parser],
|
||||
add_help=False,
|
||||
description=self.do_completion.__doc__,
|
||||
epilog=completion_epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
help="output shell completion script",
|
||||
)
|
||||
subparser.set_defaults(func=self.do_completion)
|
||||
subparser.add_argument(
|
||||
"shell", metavar="SHELL", choices=shells, help="shell to generate completion for (one of: %(choices)s)"
|
||||
)
|
||||
17
src/borg/testsuite/archiver/completion_cmd_test.py
Normal file
17
src/borg/testsuite/archiver/completion_cmd_test.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from . import cmd, generate_archiver_tests
|
||||
|
||||
pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local") # NOQA
|
||||
|
||||
|
||||
def test_bash_completion(archivers, request):
|
||||
"""Ensure the generated Bash completion includes our helper."""
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
output = cmd(archiver, "completion", "bash")
|
||||
assert "_borg_complete_aid() {" in output
|
||||
|
||||
|
||||
def test_zsh_completion(archivers, request):
|
||||
"""Ensure the generated Zsh completion includes our helper."""
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
output = cmd(archiver, "completion", "zsh")
|
||||
assert "_borg_complete_aid() {" in output
|
||||
Loading…
Reference in a new issue