mirror of
https://github.com/borgbackup/borg.git
synced 2026-10-12 02:11:36 +00:00
completion: no directory completion for repository URLs, fixes #10460
-r/--repo and --other-repo completed local directories even when the value already started with a URL scheme like ssh:// or rclone:. In fish, this offered e.g. "ssh://etc/" (fish completes the part after the ":" as a path), tcsh printed "ssh:// not found". Now, a URL scheme other than file: (the ones Location parses as a URL) gets no completion in any shell, while local paths still complete directories: - bash, zsh, fish: new _borg_complete_repo_dirs helper - tcsh: a leading completion rule completing nothing for a URL, it is used before the directory rules of the repository options. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1c5a3c37f0
commit
fd71c9eca0
@@ -56,7 +56,8 @@ The following argument types have intelligent, context-aware completion:
|
||||
|
||||
12. Repositories (location_validator):
|
||||
- Completes directories for -r/--repo and --other-repo (a local repository is a
|
||||
directory; remote locations like ssh://... just do not match one)
|
||||
directory), but nothing once the value starts with a URL scheme other than
|
||||
file: (e.g. ssh://... or rclone:...)
|
||||
|
||||
13. Encryption modes (the --encryption choices):
|
||||
- Completes the repo-create encryption modes, in zsh and fish with a short
|
||||
@@ -79,6 +80,7 @@ from ..crypto.key import encryption_argument_names
|
||||
from ..helpers import (
|
||||
archivename_validator,
|
||||
location_validator,
|
||||
Location,
|
||||
SortBySpec,
|
||||
FilesCacheMode,
|
||||
PathSpec,
|
||||
@@ -96,6 +98,16 @@ from ..helpers.time import timestamp
|
||||
from ..helpers.parseformat import partial_format
|
||||
from ..manifest import AI_HUMAN_SORT_KEYS
|
||||
|
||||
# URL schemes of the repository locations that are not local directories, see Location.
|
||||
REPO_URL_SCHEMES = [scheme for scheme in Location.BORG_SCHEMES + Location.BORGSTORE_SCHEMES if scheme != "file"]
|
||||
|
||||
# tcsh completion rule completing nothing for a repository URL (e.g. ssh://...): the rules of the
|
||||
# repository options complete directories, and tcsh can not make that depend on the current word.
|
||||
# But tcsh uses the first rule that matches, and this one comes first. A current word rule matches
|
||||
# the beginning of the word being completed, so it covers --repo=URL (-r=URL, --other-repo=URL) as
|
||||
# well as a URL being a word of its own (--repo URL, but also any other word starting with a URL).
|
||||
TCSH_REPO_URL_RULE = "'c/{--repo=,-r=,--other-repo=,}{" + ",".join(REPO_URL_SCHEMES) + "}:/n/'"
|
||||
|
||||
# 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
|
||||
@@ -186,6 +198,13 @@ _borg_complete_encryption() {
|
||||
compgen -W "${choices}" -- "$1"
|
||||
}
|
||||
|
||||
# Complete local repository directories, but nothing for a URL like ssh://... or rclone:...
|
||||
# The name must contain "_dir", so that shtab completes the results as filenames (e.g. adds a "/").
|
||||
_borg_complete_repo_dirs() {
|
||||
[[ "$1" =~ ^({REPO_URL_SCHEMES}): ]] && return 0
|
||||
compgen -d -- "$1"
|
||||
}
|
||||
|
||||
# Complete tags from repository
|
||||
_borg_complete_tags() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
@@ -433,6 +452,12 @@ _borg_complete_encryption() {
|
||||
compadd -V 'encryption modes' -Q -l -d descriptions -a choices
|
||||
}
|
||||
|
||||
# Complete local repository directories, but nothing for a URL like ssh://... or rclone:...
|
||||
_borg_complete_repo_dirs() {
|
||||
[[ "$PREFIX" == ({REPO_URL_SCHEMES}):* ]] && return 1
|
||||
_files -/
|
||||
}
|
||||
|
||||
# Complete tags from repository
|
||||
_borg_complete_tags() {
|
||||
local cur
|
||||
@@ -649,6 +674,17 @@ function _borg_complete_encryption
|
||||
printf '%s\n' {ENCRYPTION_DESCRIPTIONS}
|
||||
end
|
||||
|
||||
# Complete local repository directories, but nothing for a URL like ssh://... or rclone:...
|
||||
# (fish's directory completion would complete the part after the ":" as a path, e.g. ssh://etc/).
|
||||
function _borg_complete_repo_dirs
|
||||
# the value being completed: without a leading --repo= (-r=, --other-repo=) or -r
|
||||
set -l cur (commandline -ct | string replace -r -- '^(-[^=]*=|-r)' '')
|
||||
if string match -qr -- '^({REPO_URL_SCHEMES}):' $cur
|
||||
return
|
||||
end
|
||||
__fish_complete_directories
|
||||
end
|
||||
|
||||
# Complete relative time markers
|
||||
function _borg_complete_relative_time
|
||||
printf '%s\n' {RELATIVE_TIME_CHOICES}
|
||||
@@ -1005,10 +1041,11 @@ class CompletionMixIn:
|
||||
return {"bash": fn_name, "zsh": fn_name, "tcsh": f"`{tcsh_fn or fn_name}`", "fish": f"({fn_name})"}
|
||||
|
||||
_attach_completion(parser, archivename_validator, for_all_shells("_borg_complete_archive"))
|
||||
# -r/--repo and --other-repo: complete local repository directories (remote locations
|
||||
# like ssh://... simply do not match anything then), see #3086.
|
||||
_attach_completion(parser, location_validator(other=False), shtab.DIRECTORY)
|
||||
_attach_completion(parser, location_validator(other=True), shtab.DIRECTORY)
|
||||
# -r/--repo and --other-repo: complete local repository directories, see #3086, but no
|
||||
# directories for a URL like ssh://..., see #10460 (tcsh: see TCSH_REPO_URL_RULE).
|
||||
repo_dirs = for_all_shells("_borg_complete_repo_dirs") | {"tcsh": shtab.DIRECTORY["tcsh"]}
|
||||
_attach_completion(parser, location_validator(other=False), repo_dirs)
|
||||
_attach_completion(parser, location_validator(other=True), repo_dirs)
|
||||
for sortby_type, sortby_fn, _ in SORTBY_COMPLETERS:
|
||||
_attach_completion(parser, sortby_type, for_all_shells(sortby_fn, tcsh_fn=TCSH_SORTBY_FN))
|
||||
_attach_completion(parser, FilesCacheMode, for_all_shells("_borg_complete_filescachemode"))
|
||||
@@ -1092,6 +1129,7 @@ class CompletionMixIn:
|
||||
"RELATIVE_TIME_CHOICES": relative_time_choices_str,
|
||||
"FILE_SIZE_CHOICES": file_size_choices_str,
|
||||
"HELP_CHOICES": help_choices,
|
||||
"REPO_URL_SCHEMES": "|".join(REPO_URL_SCHEMES),
|
||||
"SH_COMPLETE": TCSH_SH_COMPLETE,
|
||||
# after SH_COMPLETE, so that the {PROG} in there is substituted as well
|
||||
"PROG": completion_prog,
|
||||
@@ -1107,7 +1145,11 @@ class CompletionMixIn:
|
||||
template = preamble_templates.get(args.shell)
|
||||
# Build the preamble using partial_format to avoid escaping braces etc.
|
||||
preambles = [partial_format(template, mapping)] if template else []
|
||||
print(parser.get_completion_script(f"shtab-{args.shell}", preambles=preambles))
|
||||
script = parser.get_completion_script(f"shtab-{args.shell}", preambles=preambles)
|
||||
if args.shell == "tcsh":
|
||||
complete_cmd = f"complete {completion_prog} \\\n"
|
||||
script = script.replace(complete_cmd, f"{complete_cmd} {TCSH_REPO_URL_RULE} \\\n", 1)
|
||||
print(script)
|
||||
|
||||
def build_parser_completion(self, subparsers, common_parser, mid_common_parser):
|
||||
shells = tuple(shtab.SUPPORTED_SHELLS)
|
||||
|
||||
@@ -596,16 +596,70 @@ def test_completion_falls_back_to_borg(archivers, request, shell, monkeypatch):
|
||||
def test_completion_repo_is_a_directory(archivers, request, shell):
|
||||
"""-r/--repo and --other-repo complete local repository directories."""
|
||||
lines = completion_lines(archivers, request, shell)
|
||||
# how each shell spells "complete directories", and how it refers to the two options
|
||||
directory, repo, other_repo = {
|
||||
"bash": ("_shtab_compgen_dirs", "_shtab_borg___repo_COMPGEN=", "_shtab_borg_repo_create___other_repo_COMPGEN="),
|
||||
"zsh": ("_files -/", "{-r,--repo}", '"--other-repo['),
|
||||
"fish": ("__fish_complete_directories", " -l repo ", " -l other-repo "),
|
||||
# the helper completing directories (but not for a URL), and how each shell refers to the two options
|
||||
directory = "_borg_complete_repo_dirs"
|
||||
repo, other_repo = {
|
||||
"bash": ("_shtab_borg___repo_COMPGEN=", "_shtab_borg_repo_create___other_repo_COMPGEN="),
|
||||
"zsh": ("{-r,--repo}", '"--other-repo['),
|
||||
"fish": (" -l repo ", " -l other-repo "),
|
||||
}[shell]
|
||||
assert any(repo in line and directory in line for line in lines), "-r/--repo does not complete directories"
|
||||
assert any(other_repo in line and directory in line for line in lines), "--other-repo does not complete dirs"
|
||||
|
||||
|
||||
# repository URLs: no directories must be completed for them, see #10460
|
||||
REPO_URLS = ["ssh://", "ssh://host/", "sftp://", "rclone:", "s3:", "b2:", "http://", "https://"]
|
||||
|
||||
|
||||
@needs_bash4
|
||||
def test_bash_repo_url_no_directories(archivers, request, tmp_path):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
(tmp_path / "somerepo").mkdir()
|
||||
for url in REPO_URLS:
|
||||
# a local directory looking like the start of the URL, so that plain directory completion finds it
|
||||
(tmp_path / url.split("/")[0]).mkdir(exist_ok=True)
|
||||
script = cmd(archiver, "completion", "bash")
|
||||
for url in REPO_URLS:
|
||||
result = _run_bash_completion_fn(script, f'cd "{tmp_path}"\n_borg_complete_repo_dirs "{url}"\n')
|
||||
assert result.returncode == 0, f"stderr: {result.stderr}"
|
||||
assert result.stdout == "", f"directories offered for {url}: {result.stdout}"
|
||||
result = _run_bash_completion_fn(script, f'cd "{tmp_path}"\n_borg_complete_repo_dirs some\n')
|
||||
assert result.stdout.split() == ["somerepo"]
|
||||
|
||||
|
||||
@needs_zsh
|
||||
def test_zsh_repo_url_no_directories(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
script = cmd(archiver, "completion", "zsh")
|
||||
# stub the zsh directory completion, it needs the whole completion system
|
||||
setup = "_files() { print -r -- COMPLETING-DIRS }\n"
|
||||
for url in REPO_URLS:
|
||||
result = _run_zsh_completion_fn(script, setup + f"PREFIX='{url}'\n_borg_complete_repo_dirs\n")
|
||||
assert "COMPLETING-DIRS" not in result.stdout, f"directories offered for {url}"
|
||||
result = _run_zsh_completion_fn(script, setup + "PREFIX=/some/dir\n_borg_complete_repo_dirs\n")
|
||||
assert "COMPLETING-DIRS" in result.stdout
|
||||
|
||||
|
||||
@needs_fish
|
||||
def test_fish_repo_url_no_directories(archivers, request):
|
||||
archiver = request.getfixturevalue(archivers)
|
||||
script = cmd(archiver, "completion", "fish")
|
||||
for url in REPO_URLS:
|
||||
for cmdline in (f"borg repo-create --repo={url}", f"borg repo-create -r {url}", f"borg repo-create -r{url}"):
|
||||
candidates = _fish_complete_candidates(script, cmdline)
|
||||
assert candidates == [], f"directories offered for {cmdline}: {candidates}"
|
||||
candidates = _fish_complete_candidates(script, f"borg transfer --other-repo {url}")
|
||||
assert candidates == [], f"directories offered for --other-repo {url}: {candidates}"
|
||||
|
||||
|
||||
def test_tcsh_repo_url_no_directories(archivers, request):
|
||||
"""tcsh uses the first matching rule, the one completing nothing for URLs must precede the directory ones."""
|
||||
lines = [line.strip().rstrip(" \\") for line in completion_lines(archivers, request, "tcsh")]
|
||||
url_rule = lines.index("'c/{--repo=,-r=,--other-repo=,}{ssh,sftp,http,https,s3,b2,rclone}:/n/'")
|
||||
for repo_rule in ("'c/--repo=/d/'", "'n/--repo/d/'", "'n/-r/d/'", "'c/--other-repo=/d/'", "'n/--other-repo/d/'"):
|
||||
assert url_rule < lines.index(repo_rule)
|
||||
|
||||
|
||||
@needs_fish
|
||||
def test_fish_repo_directory_completion(archivers, request, tmp_path):
|
||||
"""-r/--repo completes directories, but no files."""
|
||||
|
||||
Reference in New Issue
Block a user