mirror of
https://github.com/borgbackup/borg.git
synced 2026-03-26 20:34:45 -04:00
Merge 5ba8acfe61 into 857db0532d
This commit is contained in:
commit
17c0b0c644
11 changed files with 930 additions and 4 deletions
40
scripts/test-ux.sh
Normal file
40
scripts/test-ux.sh
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
set -x
|
||||
# Stable argparse usage() wrapping when comparing to test-ux.sh.blessed_stderr.
|
||||
export COLUMNS="${COLUMNS:-80}"
|
||||
# Fixed paths; remove leftovers so each run matches test-ux.sh.blessed_stderr.
|
||||
rm -rf /tmp/demo-repo
|
||||
#errors that should be have helpful help
|
||||
|
||||
borg --repo /tmp/demo-repo init -e repokey-aes-ocb
|
||||
borg --repo /tmp/demo-repo rcreate -e repokey-aes-ocb
|
||||
|
||||
#Typo suggestions (Did you mean ...?)
|
||||
|
||||
borg repo-creat
|
||||
borg repoo-list
|
||||
Borg1 -> Borg2 option hints
|
||||
|
||||
borg --repo /tmp/demo-repo list --glob-archives 'my*'
|
||||
borg --repo /tmp/demo-repo create --numeric-owner test ~/data
|
||||
borg --repo /tmp/demo-repo create --nobsdflags test ~/data
|
||||
borg --repo /tmp/demo-repo create --remote-ratelimit 1000 test ~/data
|
||||
|
||||
#Missing encryption guidance for repo-create
|
||||
|
||||
borg --repo /tmp/demo-repo repo-create
|
||||
|
||||
#repo::archive migration help (BORG_REPO / --repo guidance)
|
||||
|
||||
borg --repo /tmp/demo-repo::test1 list
|
||||
|
||||
#Missing repo recovery hint (includes repo-create example + -e modes)
|
||||
|
||||
borg --repo /tmp/does-not-exist repo-info
|
||||
borg --repo /tmp/does-not-exist list
|
||||
|
||||
#Common fixes block (missing repo / unknown command)
|
||||
|
||||
borg list
|
||||
borg frobnicate
|
||||
|
||||
#Options are preserved by command-line correction.
|
||||
263
scripts/test-ux.sh.blessed_stderr
Normal file
263
scripts/test-ux.sh.blessed_stderr
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
+ export COLUMNS=80
|
||||
+ COLUMNS=80
|
||||
+ rm -rf /tmp/demo-repo
|
||||
+ borg --repo /tmp/demo-repo init -e repokey-aes-ocb
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument <command>: invalid choice: 'init' (choose from 'analyze', 'benchmark', 'check', 'compact', 'completion', 'create', 'debug', 'delete', 'diff', 'extract', 'help', 'info', 'key', 'list', 'break-lock', 'with-lock', 'mount', 'umount', 'prune', 'repo-compress', 'repo-create', 'repo-delete', 'repo-info', 'repo-list', 'recreate', 'rename', 'repo-space', 'serve', 'tag', 'export-tar', 'import-tar', 'transfer', 'undelete', 'version')
|
||||
Common fixes:
|
||||
- Maybe you meant `repo-create` not `init`:
|
||||
borg --repo /tmp/demo-repo repo-create -e repokey-aes-ocb
|
||||
- Run 'borg help' to list valid borg2 commands.
|
||||
+ borg --repo /tmp/demo-repo rcreate -e repokey-aes-ocb
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument <command>: invalid choice: 'rcreate' (choose from 'analyze', 'benchmark', 'check', 'compact', 'completion', 'create', 'debug', 'delete', 'diff', 'extract', 'help', 'info', 'key', 'list', 'break-lock', 'with-lock', 'mount', 'umount', 'prune', 'repo-compress', 'repo-create', 'repo-delete', 'repo-info', 'repo-list', 'recreate', 'rename', 'repo-space', 'serve', 'tag', 'export-tar', 'import-tar', 'transfer', 'undelete', 'version')
|
||||
Common fixes:
|
||||
- Maybe you meant `repo-create` not `rcreate`:
|
||||
borg --repo /tmp/demo-repo repo-create -e repokey-aes-ocb
|
||||
- Run 'borg help' to list valid borg2 commands.
|
||||
+ borg repo-creat
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument <command>: invalid choice: 'repo-creat' (choose from 'analyze', 'benchmark', 'check', 'compact', 'completion', 'create', 'debug', 'delete', 'diff', 'extract', 'help', 'info', 'key', 'list', 'break-lock', 'with-lock', 'mount', 'umount', 'prune', 'repo-compress', 'repo-create', 'repo-delete', 'repo-info', 'repo-list', 'recreate', 'rename', 'repo-space', 'serve', 'tag', 'export-tar', 'import-tar', 'transfer', 'undelete', 'version')
|
||||
Common fixes:
|
||||
- Maybe you meant `repo-create` not `repo-creat`:
|
||||
borg repo-create
|
||||
- Run 'borg help' to list valid borg2 commands.
|
||||
+ borg repoo-list
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument <command>: invalid choice: 'repoo-list' (choose from 'analyze', 'benchmark', 'check', 'compact', 'completion', 'create', 'debug', 'delete', 'diff', 'extract', 'help', 'info', 'key', 'list', 'break-lock', 'with-lock', 'mount', 'umount', 'prune', 'repo-compress', 'repo-create', 'repo-delete', 'repo-info', 'repo-list', 'recreate', 'rename', 'repo-space', 'serve', 'tag', 'export-tar', 'import-tar', 'transfer', 'undelete', 'version')
|
||||
Common fixes:
|
||||
- Maybe you meant `repo-list` not `repoo-list`:
|
||||
borg repo-list
|
||||
- Run 'borg help' to list valid borg2 commands.
|
||||
+ Borg1 - option hints
|
||||
./scripts/test-ux.sh: line 15: Borg1: command not found
|
||||
+ borg --repo /tmp/demo-repo list --glob-archives 'my*'
|
||||
Common fixes:
|
||||
- borg1 option "--glob-archives" is not used in borg2. Use "--match-archives 'sh:PATTERN'" instead.
|
||||
- Example: borg list ARCHIVE --match-archives 'sh:old-*'
|
||||
usage: borg [options] list [-h] [--critical] [--error] [--warning] [--info]
|
||||
[--debug] [--debug-topic TOPIC] [-p] [--iec]
|
||||
[--log-json] [--lock-wait SECONDS] [--show-version]
|
||||
[--show-rc] [--umask M] [--remote-path PATH]
|
||||
[--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER]
|
||||
[--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO] [--short]
|
||||
[--format FORMAT] [--json-lines] [--depth N]
|
||||
[-e PATTERN] [--exclude-from EXCLUDEFILE]
|
||||
[--pattern PATTERN] [--patterns-from PATTERNFILE]
|
||||
NAME [PATH ...]
|
||||
tip: For details of accepted options run: borg list --help
|
||||
error: argument NAME: Invalid archive name: "my*" [invalid chars detected matching "/\"<|>?*"]
|
||||
+ borg --repo /tmp/demo-repo create --numeric-owner test /root/data
|
||||
Common fixes:
|
||||
- borg1 option "--numeric-owner" is not used in borg2. Use "--numeric-ids" instead.
|
||||
usage: borg [options] create [-h] [--critical] [--error] [--warning] [--info]
|
||||
[--debug] [--debug-topic TOPIC] [-p] [--iec]
|
||||
[--log-json] [--lock-wait SECONDS]
|
||||
[--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER]
|
||||
[--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO] [-n] [-s] [--list]
|
||||
[--filter STATUSCHARS] [--json]
|
||||
[--stdin-name NAME] [--stdin-user USER]
|
||||
[--stdin-group GROUP] [--stdin-mode M]
|
||||
[--content-from-command] [--paths-from-stdin]
|
||||
[--paths-from-command]
|
||||
[--paths-from-shell-command]
|
||||
[--paths-delimiter DELIM] [-e PATTERN]
|
||||
[--exclude-from EXCLUDEFILE] [--pattern PATTERN]
|
||||
[--patterns-from PATTERNFILE] [--exclude-caches]
|
||||
[--exclude-if-present NAME] [--keep-exclude-tags]
|
||||
[-x] [--numeric-ids] [--atime] [--noctime]
|
||||
[--nobirthtime] [--noflags] [--noacls]
|
||||
[--noxattrs] [--sparse] [--files-cache MODE]
|
||||
[--files-changed MODE] [--read-special]
|
||||
[--comment COMMENT] [--timestamp TIMESTAMP]
|
||||
[--chunker-params PARAMS] [-C COMPRESSION]
|
||||
[--hostname HOSTNAME] [--username USERNAME]
|
||||
[--tags TAG [TAG ...]]
|
||||
NAME [PATH ...]
|
||||
tip: For details of accepted options run: borg create --help
|
||||
error: Unrecognized arguments: --numeric-owner
|
||||
+ borg --repo /tmp/demo-repo create --nobsdflags test /root/data
|
||||
Common fixes:
|
||||
- borg1 option "--nobsdflags" is not used in borg2. Use "--noflags" instead.
|
||||
usage: borg [options] create [-h] [--critical] [--error] [--warning] [--info]
|
||||
[--debug] [--debug-topic TOPIC] [-p] [--iec]
|
||||
[--log-json] [--lock-wait SECONDS]
|
||||
[--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER]
|
||||
[--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO] [-n] [-s] [--list]
|
||||
[--filter STATUSCHARS] [--json]
|
||||
[--stdin-name NAME] [--stdin-user USER]
|
||||
[--stdin-group GROUP] [--stdin-mode M]
|
||||
[--content-from-command] [--paths-from-stdin]
|
||||
[--paths-from-command]
|
||||
[--paths-from-shell-command]
|
||||
[--paths-delimiter DELIM] [-e PATTERN]
|
||||
[--exclude-from EXCLUDEFILE] [--pattern PATTERN]
|
||||
[--patterns-from PATTERNFILE] [--exclude-caches]
|
||||
[--exclude-if-present NAME] [--keep-exclude-tags]
|
||||
[-x] [--numeric-ids] [--atime] [--noctime]
|
||||
[--nobirthtime] [--noflags] [--noacls]
|
||||
[--noxattrs] [--sparse] [--files-cache MODE]
|
||||
[--files-changed MODE] [--read-special]
|
||||
[--comment COMMENT] [--timestamp TIMESTAMP]
|
||||
[--chunker-params PARAMS] [-C COMPRESSION]
|
||||
[--hostname HOSTNAME] [--username USERNAME]
|
||||
[--tags TAG [TAG ...]]
|
||||
NAME [PATH ...]
|
||||
tip: For details of accepted options run: borg create --help
|
||||
error: Unrecognized arguments: --nobsdflags
|
||||
+ borg --repo /tmp/demo-repo create --remote-ratelimit 1000 test /root/data
|
||||
Common fixes:
|
||||
- borg1 option "--remote-ratelimit" is not used in borg2. Use "--upload-ratelimit" instead.
|
||||
usage: borg [options] create [-h] [--critical] [--error] [--warning] [--info]
|
||||
[--debug] [--debug-topic TOPIC] [-p] [--iec]
|
||||
[--log-json] [--lock-wait SECONDS]
|
||||
[--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER]
|
||||
[--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO] [-n] [-s] [--list]
|
||||
[--filter STATUSCHARS] [--json]
|
||||
[--stdin-name NAME] [--stdin-user USER]
|
||||
[--stdin-group GROUP] [--stdin-mode M]
|
||||
[--content-from-command] [--paths-from-stdin]
|
||||
[--paths-from-command]
|
||||
[--paths-from-shell-command]
|
||||
[--paths-delimiter DELIM] [-e PATTERN]
|
||||
[--exclude-from EXCLUDEFILE] [--pattern PATTERN]
|
||||
[--patterns-from PATTERNFILE] [--exclude-caches]
|
||||
[--exclude-if-present NAME] [--keep-exclude-tags]
|
||||
[-x] [--numeric-ids] [--atime] [--noctime]
|
||||
[--nobirthtime] [--noflags] [--noacls]
|
||||
[--noxattrs] [--sparse] [--files-cache MODE]
|
||||
[--files-changed MODE] [--read-special]
|
||||
[--comment COMMENT] [--timestamp TIMESTAMP]
|
||||
[--chunker-params PARAMS] [-C COMPRESSION]
|
||||
[--hostname HOSTNAME] [--username USERNAME]
|
||||
[--tags TAG [TAG ...]]
|
||||
NAME [PATH ...]
|
||||
tip: For details of accepted options run: borg create --help
|
||||
error: Unrecognized arguments: --remote-ratelimit
|
||||
+ borg --repo /tmp/demo-repo repo-create
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: Option 'repo-create.encryption' is required but not provided or its value is None.
|
||||
Use -e/--encryption to choose a mode, for example: -e repokey-aes-ocb
|
||||
Available encryption modes: none, authenticated, authenticated-blake2, keyfile-aes-ocb, repokey-aes-ocb, keyfile-chacha20-poly1305, repokey-chacha20-poly1305, keyfile-blake2-aes-ocb, repokey-blake2-aes-ocb, keyfile-blake2-chacha20-poly1305, repokey-blake2-chacha20-poly1305
|
||||
+ borg --repo /tmp/demo-repo::test1 list
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument -r/--repo: Invalid location format: "/tmp/demo-repo::test1".
|
||||
|
||||
Common fixes:
|
||||
* Borg 2 does not accept repo::archive syntax. Corrected command lines:
|
||||
borg list --repo /tmp/demo-repo list ::test1
|
||||
OR
|
||||
export BORG_REPO=/tmp/demo-repo
|
||||
borg list ::test1
|
||||
+ borg --repo /tmp/does-not-exist repo-info
|
||||
Repository /tmp/does-not-exist does not exist.
|
||||
Common fixes:
|
||||
- Specify Correct Path ("/tmp/does-not-exist" does not exist).
|
||||
- Create repository (-r): borg repo-create -r "/tmp/does-not-exist" -e repokey-aes-ocb
|
||||
- Create repository (BORG_REPO):
|
||||
export BORG_REPO=/tmp/does-not-exist
|
||||
borg repo-create -e repokey-aes-ocb
|
||||
Available -e modes: none, authenticated, authenticated-blake2, keyfile-aes-ocb, repokey-aes-ocb, keyfile-chacha20-poly1305, repokey-chacha20-poly1305, keyfile-blake2-aes-ocb, repokey-blake2-aes-ocb, keyfile-blake2-chacha20-poly1305, repokey-blake2-chacha20-poly1305
|
||||
+ borg --repo /tmp/does-not-exist list
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: Option 'list.paths' is required but not provided or its value is None.
|
||||
|
||||
Common fixes:
|
||||
- "/tmp/does-not-exist" does not exist, pick a repository that exists or create one:
|
||||
borg repo-create --repo /tmp/does-not-exist -e repokey-aes-ocb
|
||||
Available -e modes: none, authenticated, authenticated-blake2, keyfile-aes-ocb, repokey-aes-ocb, keyfile-chacha20-poly1305, repokey-chacha20-poly1305, keyfile-blake2-aes-ocb, repokey-blake2-aes-ocb, keyfile-blake2-chacha20-poly1305, repokey-blake2-chacha20-poly1305
|
||||
+ borg list
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: Option 'list.paths' is required but not provided or its value is None.
|
||||
borg list requires an archive NAME to list contents.
|
||||
Common fixes:
|
||||
- Provide archive name: borg list NAME
|
||||
- To list archives in a repository, use: borg -r REPO repo-list
|
||||
+ borg frobnicate
|
||||
usage: borg [--config CONFIG] [--print_config [=flags]] [-V] [--cockpit] [-h]
|
||||
[--critical] [--error] [--warning] [--info] [--debug]
|
||||
[--debug-topic TOPIC] [-p] [--iec] [--log-json]
|
||||
[--lock-wait SECONDS] [--show-version] [--show-rc] [--umask M]
|
||||
[--remote-path PATH] [--upload-ratelimit RATE]
|
||||
[--upload-buffer UPLOAD_BUFFER] [--debug-profile FILE] [--rsh RSH]
|
||||
[--socket [PATH]] [-r REPO]
|
||||
<command> ...
|
||||
tip: For details of accepted options run: borg --help
|
||||
error: argument <command>: invalid choice: 'frobnicate' (choose from 'analyze', 'benchmark', 'check', 'compact', 'completion', 'create', 'debug', 'delete', 'diff', 'extract', 'help', 'info', 'key', 'list', 'break-lock', 'with-lock', 'mount', 'umount', 'prune', 'repo-compress', 'repo-create', 'repo-delete', 'repo-info', 'repo-list', 'recreate', 'rename', 'repo-space', 'serve', 'tag', 'export-tar', 'import-tar', 'transfer', 'undelete', 'version')
|
||||
Common fixes:
|
||||
- Run 'borg help' to list valid borg2 commands.
|
||||
|
|
@ -166,9 +166,28 @@ class Archiver(
|
|||
logging.getLogger("borg.output.list").info("%1s %s", status, remove_surrogates(path))
|
||||
|
||||
def preprocess_args(self, args):
|
||||
borg1_option_equivalents = {
|
||||
"--glob-archives": "--match-archives 'sh:PATTERN'",
|
||||
"--numeric-owner": "--numeric-ids",
|
||||
"--nobsdflags": "--noflags",
|
||||
"--remote-ratelimit": "--upload-ratelimit",
|
||||
}
|
||||
deprecations = [
|
||||
# ('--old', '--new' or None, 'Warning: "--old" has been deprecated. Use "--new" instead.'),
|
||||
]
|
||||
seen_borg1_options = []
|
||||
for arg in args:
|
||||
if arg in borg1_option_equivalents and arg not in seen_borg1_options:
|
||||
seen_borg1_options.append(arg)
|
||||
if seen_borg1_options:
|
||||
print("Common fixes:", file=sys.stderr)
|
||||
for arg in seen_borg1_options:
|
||||
print(
|
||||
f'- borg1 option "{arg}" is not used in borg2. ' f'Use "{borg1_option_equivalents[arg]}" instead.',
|
||||
file=sys.stderr,
|
||||
)
|
||||
if "--glob-archives" in seen_borg1_options:
|
||||
print("- Example: borg list ARCHIVE --match-archives 'sh:old-*'", file=sys.stderr)
|
||||
for i, arg in enumerate(args[:]):
|
||||
for old_name, new_name, warning in deprecations:
|
||||
# either --old_name or --old_name=...
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ the subcommand namespace and the outer (top-level) default flows through
|
|||
unchanged.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
# here are the only imports from argparse and jsonargparse,
|
||||
|
|
@ -103,12 +108,288 @@ from jsonargparse import ArgumentParser as _ArgumentParser # we subclass that t
|
|||
from jsonargparse import Namespace, ActionSubCommands, SUPPRESS, REMAINDER # noqa: F401
|
||||
from jsonargparse.typing import register_type, PositiveInt # noqa: F401
|
||||
|
||||
# Borg 1.x / informal names -> borg2 top-level subcommand (same list as parser choices targets).
|
||||
_TOP_COMMAND_SYNONYMS = {
|
||||
"init": "repo-create",
|
||||
"rcreate": "repo-create",
|
||||
"repocreate": "repo-create",
|
||||
"rm": "delete",
|
||||
"clean": "compact",
|
||||
"unrm": "undelete",
|
||||
"undel": "undelete",
|
||||
"restore": "undelete",
|
||||
}
|
||||
|
||||
# Example line after 'Maybe you meant `<canonical>` not `<typed>`:\n\t' (placeholders intentionally generic).
|
||||
_TOP_COMMAND_EXAMPLES = {
|
||||
"repo-create": "borg -r REPO repo-create -e repokey-aes-ocb",
|
||||
"delete": "borg -r REPO delete ARCHIVE_OR_AID",
|
||||
"compact": "borg -r REPO compact",
|
||||
"undelete": "borg -r REPO undelete …",
|
||||
"list": "borg -r REPO list ARCHIVE",
|
||||
}
|
||||
|
||||
# Top-level subcommand names (must match build_parser / <command> choices).
|
||||
_TOP_LEVEL_COMMANDS = frozenset(
|
||||
{
|
||||
"analyze",
|
||||
"benchmark",
|
||||
"check",
|
||||
"compact",
|
||||
"completion",
|
||||
"create",
|
||||
"debug",
|
||||
"delete",
|
||||
"diff",
|
||||
"extract",
|
||||
"help",
|
||||
"info",
|
||||
"key",
|
||||
"list",
|
||||
"break-lock",
|
||||
"with-lock",
|
||||
"mount",
|
||||
"umount",
|
||||
"prune",
|
||||
"repo-compress",
|
||||
"repo-create",
|
||||
"repo-delete",
|
||||
"repo-info",
|
||||
"repo-list",
|
||||
"recreate",
|
||||
"rename",
|
||||
"repo-space",
|
||||
"serve",
|
||||
"tag",
|
||||
"export-tar",
|
||||
"import-tar",
|
||||
"transfer",
|
||||
"undelete",
|
||||
"version",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _parse_unrecognized_arguments_raw(message: str) -> str | None:
|
||||
if "unrecognized arguments" not in message.lower():
|
||||
return None
|
||||
m = re.search(r"Unrecognized arguments:\s*(.+?)(?:\n|$)", message, re.IGNORECASE | re.DOTALL)
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1).strip()
|
||||
|
||||
|
||||
def _find_contiguous_subsequence(haystack: list[str], needle: list[str]) -> int | None:
|
||||
if not needle or len(needle) > len(haystack):
|
||||
return None
|
||||
for i in range(len(haystack) - len(needle) + 1):
|
||||
if haystack[i : i + len(needle)] == needle:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def _remove_contiguous_subsequence(haystack: list[str], needle: list[str]) -> list[str] | None:
|
||||
i = _find_contiguous_subsequence(haystack, needle)
|
||||
if i is None:
|
||||
return None
|
||||
return haystack[:i] + haystack[i + len(needle) :]
|
||||
|
||||
|
||||
def _suggest_move_options_after_subcommand(message: str) -> str | None:
|
||||
"""
|
||||
If the user put subcommand-specific flags before <command> (e.g. borg --stats create ...),
|
||||
suggest the same argv with those flags after the subcommand.
|
||||
"""
|
||||
raw = _parse_unrecognized_arguments_raw(message)
|
||||
if not raw:
|
||||
return None
|
||||
try:
|
||||
tokens = shlex.split(raw)
|
||||
except ValueError:
|
||||
return None
|
||||
if not tokens:
|
||||
return None
|
||||
argv = sys.argv
|
||||
sub_idx = None
|
||||
for i, a in enumerate(argv):
|
||||
if a in _TOP_LEVEL_COMMANDS:
|
||||
sub_idx = i
|
||||
break
|
||||
if sub_idx is None or sub_idx < 2:
|
||||
return None
|
||||
prefix = argv[1:sub_idx]
|
||||
if _find_contiguous_subsequence(prefix, tokens) is None:
|
||||
return None
|
||||
keep = _remove_contiguous_subsequence(prefix, tokens)
|
||||
if keep is None:
|
||||
return None
|
||||
corrected = [argv[0]] + keep + [argv[sub_idx]] + tokens + argv[sub_idx + 1 :]
|
||||
return " ".join(shlex.quote(c) for c in corrected)
|
||||
|
||||
|
||||
def _argv_tail_after_invalid_choice(invalid: str) -> list[str]:
|
||||
"""Tokens after the invalid top-level subcommand in sys.argv, if any."""
|
||||
try:
|
||||
idx = sys.argv.index(invalid)
|
||||
except ValueError:
|
||||
return []
|
||||
return sys.argv[idx + 1 :]
|
||||
|
||||
|
||||
def _repo_path_from_argv() -> str | None:
|
||||
"""Return the path/URL after ``-r``/``--repo`` in ``sys.argv``, if present."""
|
||||
argv = sys.argv
|
||||
for i, a in enumerate(argv):
|
||||
if a in ("--repo", "-r") and i + 1 < len(argv):
|
||||
return argv[i + 1]
|
||||
return None
|
||||
|
||||
|
||||
def _local_repo_path_missing(repo_path: str) -> bool:
|
||||
"""True if *repo_path* looks like a local filesystem path and does not exist."""
|
||||
if not repo_path or "://" in repo_path:
|
||||
return False
|
||||
if repo_path.startswith("/") or (len(repo_path) > 2 and repo_path[1] == ":"):
|
||||
return not os.path.exists(repo_path)
|
||||
return False
|
||||
|
||||
|
||||
def _argv_display_for_hint(argv: list[str]) -> list[str]:
|
||||
"""Normalize argv to a readable `borg ...` line when launched via python -m or a borg binary."""
|
||||
if (
|
||||
len(argv) >= 3
|
||||
and os.path.basename(argv[0]).lower().startswith("python")
|
||||
and argv[1] == "-m"
|
||||
and argv[2] == "borg"
|
||||
):
|
||||
return ["borg"] + argv[3:]
|
||||
if len(argv) >= 1 and os.path.basename(argv[0]).lower() in ("borg", "borg.exe"):
|
||||
return ["borg"] + argv[1:]
|
||||
return list(argv)
|
||||
|
||||
|
||||
def _corrected_command_line_for_invalid_subcommand(invalid: str, canonical: str) -> str | None:
|
||||
"""Replace invalid with canonical in sys.argv; keep all other tokens (same order)."""
|
||||
try:
|
||||
idx = sys.argv.index(invalid)
|
||||
except ValueError:
|
||||
return None
|
||||
if idx < 1:
|
||||
return None
|
||||
argv = list(sys.argv)
|
||||
argv[idx] = canonical
|
||||
display = _argv_display_for_hint(argv)
|
||||
if not display:
|
||||
return None
|
||||
return " ".join(shlex.quote(a) for a in display)
|
||||
|
||||
|
||||
def _apply_argv_tail_to_example(canonical: str, example: str, argv_tail: list[str]) -> str:
|
||||
"""Replace generic placeholders with argv tokens the user actually typed after the bad command."""
|
||||
if not argv_tail:
|
||||
return example
|
||||
tail = " ".join(shlex.quote(a) for a in argv_tail)
|
||||
if canonical == "delete" and "ARCHIVE_OR_AID" in example:
|
||||
return example.replace("ARCHIVE_OR_AID", tail)
|
||||
if canonical == "list" and "ARCHIVE" in example:
|
||||
return example.replace("ARCHIVE", tail)
|
||||
if canonical == "undelete" and "…" in example:
|
||||
return example.replace("…", tail)
|
||||
return example
|
||||
|
||||
|
||||
class ArgumentParser(_ArgumentParser):
|
||||
# the borg code always uses RawDescriptionHelpFormatter and add_help=False:
|
||||
def __init__(self, *args, formatter_class=RawDescriptionHelpFormatter, add_help=False, **kwargs):
|
||||
super().__init__(*args, formatter_class=formatter_class, add_help=add_help, **kwargs)
|
||||
|
||||
def _top_command_choice_hint(self, message: str) -> str | None:
|
||||
match = re.search(r"invalid choice: '([^']+)' \(choose from ([^)]+)\)", message)
|
||||
if not match:
|
||||
return None
|
||||
invalid = match.group(1)
|
||||
choices = [choice.strip().strip("'\"") for choice in match.group(2).split(",")]
|
||||
canonical = _TOP_COMMAND_SYNONYMS.get(invalid)
|
||||
if canonical is None:
|
||||
candidates = difflib.get_close_matches(invalid, choices, n=1, cutoff=0.6)
|
||||
if not candidates:
|
||||
return None
|
||||
canonical = candidates[0]
|
||||
if canonical == invalid:
|
||||
return None
|
||||
example = _corrected_command_line_for_invalid_subcommand(invalid, canonical)
|
||||
if example is None:
|
||||
example = _TOP_COMMAND_EXAMPLES.get(canonical, f"borg -r REPO {canonical}")
|
||||
example = _apply_argv_tail_to_example(canonical, example, _argv_tail_after_invalid_choice(invalid))
|
||||
return f"Maybe you meant `{canonical}` not `{invalid}`:\n\t{example}"
|
||||
|
||||
def _common_fix_hints(self, message: str) -> list[str]:
|
||||
hints = []
|
||||
reorder = _suggest_move_options_after_subcommand(message)
|
||||
if reorder:
|
||||
hints.append(f"Put subcommand-specific options after `<command>`: {reorder}")
|
||||
if "missing repository" in message.lower():
|
||||
hints.append("Set the repository via --repo REPO or BORG_REPO.")
|
||||
list_name_missing = "list.name is none" in message.lower() or ("list.name" in message and "is None" in message)
|
||||
if list_name_missing:
|
||||
hints.append("For 'borg list', set repository via -r/--repo or BORG_REPO and pass an archive name.")
|
||||
if "invalid choice" in message and "<command>" in message:
|
||||
cmd_hint = self._top_command_choice_hint(message)
|
||||
if cmd_hint:
|
||||
hints.append(cmd_hint)
|
||||
hints.append("Run 'borg help' to list valid borg2 commands.")
|
||||
return hints
|
||||
|
||||
def error(self, message, *args, **kwargs):
|
||||
message = str(message)
|
||||
if "Option 'repo-create.encryption' is required but not provided" in message:
|
||||
from ..crypto.key import key_argument_names
|
||||
|
||||
modes = key_argument_names()
|
||||
mode_list = ", ".join(modes)
|
||||
message = (
|
||||
f"{message}\n"
|
||||
"Use -e/--encryption to choose a mode, for example: -e repokey-aes-ocb\n"
|
||||
f"Available encryption modes: {mode_list}"
|
||||
)
|
||||
if "Option 'list.paths' is required but not provided" in message:
|
||||
repo_path = _repo_path_from_argv()
|
||||
if repo_path and _local_repo_path_missing(repo_path):
|
||||
from ..crypto.key import key_argument_names
|
||||
|
||||
mode_list = ", ".join(key_argument_names())
|
||||
message = (
|
||||
f"{message}\n\n"
|
||||
"Common fixes:\n"
|
||||
f'- "{repo_path}" does not exist, pick a repository that exists or create one:\n'
|
||||
f"\tborg repo-create --repo {repo_path} -e repokey-aes-ocb\n"
|
||||
f"Available -e modes: {mode_list}"
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
f"{message}\n"
|
||||
"borg list requires an archive NAME to list contents.\n"
|
||||
"Common fixes:\n"
|
||||
"- Provide archive name: borg list NAME\n"
|
||||
"- To list archives in a repository, use: borg -r REPO repo-list"
|
||||
)
|
||||
loc_match = re.search(r'Invalid location format: "([^"]+)"', message)
|
||||
if loc_match and "::" in loc_match.group(1):
|
||||
repo, archive = loc_match.group(1).split("::", 1)
|
||||
message = (
|
||||
f"{message}\n\n"
|
||||
"Common fixes:\n"
|
||||
" * Borg 2 does not accept repo::archive syntax. Corrected command lines:\n"
|
||||
f"\tborg list --repo {repo} list ::{archive}\n"
|
||||
f"\t\tOR\n"
|
||||
f"\texport BORG_REPO={repo}\n"
|
||||
f"\tborg list ::{archive}"
|
||||
)
|
||||
common_hints = self._common_fix_hints(message)
|
||||
if common_hints:
|
||||
message = f"{message}\nCommon fixes:\n- " + "\n- ".join(common_hints)
|
||||
super().error(message, *args, **kwargs)
|
||||
|
||||
|
||||
def flatten_namespace(ns: Any) -> Namespace:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -616,6 +616,9 @@ class Location:
|
|||
|
||||
self.raw = text # as given by user, might contain placeholders
|
||||
self.processed = replace_placeholders(self.raw, overrides) # after placeholder replacement
|
||||
if "::" in self.processed:
|
||||
# Keep this message short; ArgumentParser.error() adds Common fixes with examples.
|
||||
raise ValueError(f'Invalid location format: "{self.processed}".')
|
||||
valid = self._parse(self.processed)
|
||||
if valid:
|
||||
self.valid = True
|
||||
|
|
|
|||
|
|
@ -150,6 +150,26 @@ class LegacyRepository:
|
|||
|
||||
exit_mcode = 13
|
||||
|
||||
def __init__(self, location):
|
||||
from .crypto.key import key_argument_names
|
||||
|
||||
mode_list = ", ".join(key_argument_names())
|
||||
location = str(location)
|
||||
guidance = (
|
||||
f"Repository {location} does not exist.\n"
|
||||
"Common fixes:\n"
|
||||
f'- Specify Correct Path ("{location}" does not exist).\n'
|
||||
f'- Create repository (-r): borg repo-create -r "{location}" -e repokey-aes-ocb\n'
|
||||
f"- Create repository (BORG_REPO):\n"
|
||||
f" export BORG_REPO={location}\n"
|
||||
f" borg repo-create -e repokey-aes-ocb\n"
|
||||
f"Available -e modes: {mode_list}"
|
||||
)
|
||||
super().__init__(guidance)
|
||||
|
||||
def get_message(self):
|
||||
return self.args[0]
|
||||
|
||||
class InsufficientFreeSpaceError(Error):
|
||||
"""Insufficient free space to complete transaction (required: {}, available: {})."""
|
||||
|
||||
|
|
@ -160,6 +180,26 @@ class LegacyRepository:
|
|||
|
||||
exit_mcode = 15
|
||||
|
||||
def __init__(self, location):
|
||||
from .crypto.key import key_argument_names
|
||||
|
||||
mode_list = ", ".join(key_argument_names())
|
||||
location = str(location)
|
||||
guidance = (
|
||||
f"{location} is not a valid repository. Check repo config.\n"
|
||||
"Common fixes:\n"
|
||||
f'- Specify Correct Path ("{location}" is not a Borg repository).\n'
|
||||
f'- Create repository (-r): borg repo-create -r "{location}" -e repokey-aes-ocb\n'
|
||||
f"- Create repository (BORG_REPO):\n"
|
||||
f" export BORG_REPO={location}\n"
|
||||
f" borg repo-create -e repokey-aes-ocb\n"
|
||||
f"Available -e modes: {mode_list}"
|
||||
)
|
||||
super().__init__(guidance)
|
||||
|
||||
def get_message(self):
|
||||
return self.args[0]
|
||||
|
||||
class InvalidRepositoryConfig(Error):
|
||||
"""{} does not have a valid configuration. Check repo config [{}]."""
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,26 @@ class Repository:
|
|||
|
||||
exit_mcode = 13
|
||||
|
||||
def __init__(self, location):
|
||||
from .crypto.key import key_argument_names
|
||||
|
||||
mode_list = ", ".join(key_argument_names())
|
||||
location = str(location)
|
||||
guidance = (
|
||||
f"Repository {location} does not exist.\n"
|
||||
"Common fixes:\n"
|
||||
f'- Specify Correct Path ("{location}" does not exist).\n'
|
||||
f'- Create repository (-r): borg repo-create -r "{location}" -e repokey-aes-ocb\n'
|
||||
f"- Create repository (BORG_REPO):\n"
|
||||
f" export BORG_REPO={location}\n"
|
||||
f" borg repo-create -e repokey-aes-ocb\n"
|
||||
f"Available -e modes: {mode_list}"
|
||||
)
|
||||
super().__init__(guidance)
|
||||
|
||||
def get_message(self):
|
||||
return self.args[0]
|
||||
|
||||
class InsufficientFreeSpaceError(Error):
|
||||
"""Insufficient free space to complete the transaction (required: {}, available: {})."""
|
||||
|
||||
|
|
@ -62,6 +82,26 @@ class Repository:
|
|||
|
||||
exit_mcode = 15
|
||||
|
||||
def __init__(self, location):
|
||||
from .crypto.key import key_argument_names
|
||||
|
||||
mode_list = ", ".join(key_argument_names())
|
||||
location = str(location)
|
||||
guidance = (
|
||||
f"{location} is not a valid repository. Check the repository config.\n"
|
||||
"Common fixes:\n"
|
||||
f'- Specify Correct Path ("{location}" is not a Borg repository).\n'
|
||||
f'- Create repository (-r): borg repo-create -r "{location}" -e repokey-aes-ocb\n'
|
||||
f"- Create repository (BORG_REPO):\n"
|
||||
f" export BORG_REPO={location}\n"
|
||||
f" borg repo-create -e repokey-aes-ocb\n"
|
||||
f"Available -e modes: {mode_list}"
|
||||
)
|
||||
super().__init__(guidance)
|
||||
|
||||
def get_message(self):
|
||||
return self.args[0]
|
||||
|
||||
class InvalidRepositoryConfig(Error):
|
||||
"""{} does not have a valid config. Check the repository config [{}]."""
|
||||
|
||||
|
|
@ -253,13 +293,13 @@ class Repository:
|
|||
try:
|
||||
self.store.open()
|
||||
except StoreBackendDoesNotExist:
|
||||
raise self.DoesNotExist(str(self._location)) from None
|
||||
raise self.DoesNotExist(self._location.processed) from None
|
||||
else:
|
||||
self.store_opened = True
|
||||
try:
|
||||
readme = self.store.load("config/readme").decode()
|
||||
except StoreObjectNotFound:
|
||||
raise self.DoesNotExist(str(self._location)) from None
|
||||
raise self.DoesNotExist(self._location.processed) from None
|
||||
if readme != REPOSITORY_README:
|
||||
raise self.InvalidRepository(str(self._location))
|
||||
self.version = int(self.store.load("config/version").decode())
|
||||
|
|
|
|||
227
src/borg/testsuite/archiver/cli_guidance_test.py
Normal file
227
src/borg/testsuite/archiver/cli_guidance_test.py
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ...helpers.argparsing import ArgumentParser
|
||||
from ...repository import Repository
|
||||
from . import exec_cmd
|
||||
|
||||
|
||||
def test_unknown_command_typo_suggests_fuzzy_match(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("repo-creat")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `repo-creat`:" in output
|
||||
assert "\tborg repo-create" in output
|
||||
|
||||
|
||||
def test_unknown_command_typo_list(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("lst")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `list` not `lst`:" in output
|
||||
assert "\tborg list" in output
|
||||
|
||||
|
||||
def test_fuzzy_typo_preserves_following_args(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("creat", "foo", "--stats")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `create` not `creat`:" in output
|
||||
assert "\tborg create foo --stats" in output
|
||||
|
||||
|
||||
def test_legacy_rm_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `delete` not `rm`:" in output
|
||||
assert "\tborg delete" in output
|
||||
|
||||
|
||||
def test_legacy_rm_synonym_preserves_trailing_tokens_in_delete_example(cmd_fixture, tmp_path):
|
||||
"""Tokens after 'rm' must appear in the suggested delete line (not a generic placeholder)."""
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("-r", repo, "rm", "dsfasdfsdfsdf")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `delete` not `rm`:" in output
|
||||
assert "ARCHIVE_OR_AID" not in output
|
||||
assert f"\tborg -r {repo} delete dsfasdfsdfsdf" in output
|
||||
|
||||
|
||||
def test_rm_synonym_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/tmp/borg/outC", "rm", "dsfasdfsdfsdf"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'rm' (choose from 'delete', 'list')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `delete` not `rm`:" in hint
|
||||
assert "ARCHIVE_OR_AID" not in hint
|
||||
assert "\tborg -r /tmp/borg/outC delete dsfasdfsdfsdf" in hint
|
||||
|
||||
|
||||
def test_lst_typo_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/r", "lst", "my-archive"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'lst' (choose from 'list', 'delete')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `list` not `lst`:" in hint
|
||||
assert "ARCHIVE" not in hint
|
||||
assert "\tborg -r /r list my-archive" in hint
|
||||
|
||||
|
||||
def test_restore_synonym_example_includes_argv_tail(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["python", "-m", "borg", "-r", "/r", "restore", "arch1"])
|
||||
parser = ArgumentParser(prog="borg")
|
||||
message = "error: argument <command>: invalid choice: 'restore' (choose from 'undelete', 'list')"
|
||||
hint = parser._top_command_choice_hint(message)
|
||||
assert hint is not None
|
||||
assert "Maybe you meant `undelete` not `restore`:" in hint
|
||||
assert "…" not in hint
|
||||
assert "\tborg -r REPO undelete arch1" in hint
|
||||
|
||||
|
||||
def test_maybe_you_meant_rm_is_common_fix_bullet(cmd_fixture):
|
||||
"""Invalid-command hint must appear under Common fixes with a '- ' bullet."""
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:" in output
|
||||
assert "- Maybe you meant `delete` not `rm`:" in output
|
||||
|
||||
|
||||
def test_maybe_you_meant_line_has_dash_prefix_before_maybe(cmd_fixture):
|
||||
"""Regression: '- ' must prefix the Maybe-you-meant line (not a bare paragraph before Common fixes)."""
|
||||
exit_code, output = cmd_fixture("rm")
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:\n- Maybe you meant `delete` not `rm`:" in output
|
||||
|
||||
|
||||
def test_legacy_clean_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("clean")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `compact` not `clean`:" in output
|
||||
assert "\tborg compact" in output
|
||||
|
||||
|
||||
def test_legacy_restore_synonym(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("restore")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `undelete` not `restore`:" in output
|
||||
assert "\tborg undelete" in output
|
||||
|
||||
|
||||
def test_legacy_init_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "init", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `init`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_legacy_rcreate_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "rcreate", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `rcreate`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_legacy_repocreate_synonym(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repocreate", "-e", "none")
|
||||
assert exit_code == 2
|
||||
assert "Maybe you meant `repo-create` not `repocreate`:" in output
|
||||
assert f"\tborg --repo {repo} repo-create -e none" in output
|
||||
|
||||
|
||||
def test_repo_create_missing_encryption_shows_available_modes(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-create")
|
||||
assert exit_code == 2
|
||||
assert "Use -e/--encryption to choose a mode" in output
|
||||
assert "Available encryption modes:" in output
|
||||
|
||||
|
||||
def test_repo_double_colon_syntax_shows_migration_hint(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo::archive")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-info")
|
||||
assert exit_code == 2
|
||||
assert "does not accept repo::archive syntax" in output
|
||||
assert "borg list --repo" in output
|
||||
assert "\t\tOR\n" in output
|
||||
assert "export BORG_REPO=" in output
|
||||
|
||||
|
||||
def test_missing_repository_error_shows_create_example(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "missing-repo")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "repo-info")
|
||||
assert exit_code == 2
|
||||
assert "does not exist." in output
|
||||
assert "Common fixes:" in output
|
||||
assert f'Specify Correct Path ("{repo}" does not exist).' in output
|
||||
assert "borg repo-info -r" not in output
|
||||
assert "Create repository (-r): borg repo-create" in output
|
||||
assert "Create repository (BORG_REPO):" in output
|
||||
assert "Available -e modes:" in output
|
||||
|
||||
|
||||
def test_repository_does_not_exist_common_fix_explains_missing_path():
|
||||
msg = Repository.DoesNotExist("/tmp/foo").get_message()
|
||||
assert 'Specify Correct Path ("/tmp/foo" does not exist).' in msg
|
||||
assert "borg repo-info -r" not in msg
|
||||
|
||||
|
||||
def test_repository_invalid_common_fix_explains_not_a_borg_repo():
|
||||
msg = Repository.InvalidRepository("/tmp/foo").get_message()
|
||||
assert 'Specify Correct Path ("/tmp/foo" is not a Borg repository).' in msg
|
||||
assert "borg repo-info -r" not in msg
|
||||
|
||||
|
||||
def test_list_name_none_common_fix_hint():
|
||||
parser = ArgumentParser(prog="borg")
|
||||
hints = parser._common_fix_hints("Validation failed: list.name is None")
|
||||
assert "For 'borg list', set repository via -r/--repo or BORG_REPO and pass an archive name." in hints
|
||||
|
||||
|
||||
def test_list_paths_required_shows_repo_create_when_repo_path_missing(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "does-not-exist")
|
||||
exit_code, output = cmd_fixture("--repo", repo, "list")
|
||||
assert exit_code == 2
|
||||
assert "Option 'list.paths' is required but not provided" in output
|
||||
assert f'"{repo}" does not exist, pick a repository that exists or create one:' in output
|
||||
assert f"borg repo-create --repo {repo} -e repokey-aes-ocb" in output
|
||||
assert "Available -e modes:" in output
|
||||
|
||||
|
||||
def test_list_paths_required_shows_path_and_repo_creation_hints(cmd_fixture, tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
repo.mkdir()
|
||||
exit_code, output = cmd_fixture("--repo", os.fspath(repo), "list")
|
||||
assert exit_code == 2
|
||||
assert "Option 'list.paths' is required but not provided" in output
|
||||
assert "borg list requires an archive NAME to list contents." in output
|
||||
assert "- Provide archive name: borg list NAME" in output
|
||||
assert "- To list archives in a repository, use: borg -r REPO repo-list" in output
|
||||
|
||||
|
||||
def test_argument_parser_error_accepts_jsonargparse_extra_arg():
|
||||
parser = ArgumentParser(prog="borg")
|
||||
with pytest.raises(SystemExit):
|
||||
parser.error("bad message", ValueError("wrapped"))
|
||||
|
||||
|
||||
def test_unrecognized_args_before_subcommand_shows_reordered_example(cmd_fixture):
|
||||
exit_code, output = cmd_fixture("--stats", "create", "foo")
|
||||
assert exit_code == 2
|
||||
assert "Unrecognized arguments" in output
|
||||
assert "Common fixes:" in output
|
||||
assert "Put subcommand-specific options after `<command>`:" in output
|
||||
assert "create" in output and "--stats" in output
|
||||
|
||||
|
||||
def test_preprocess_prints_glob_archives_migration_hint(tmp_path):
|
||||
repo = os.fspath(tmp_path / "repo")
|
||||
exit_code, output = exec_cmd("--repo", repo, "list", "dummy-archive", "--glob-archives", "sh:old", fork=False)
|
||||
assert exit_code == 2
|
||||
assert "Common fixes:" in output
|
||||
assert '- borg1 option "--glob-archives" is not used in borg2.' in output
|
||||
assert "--match-archives 'sh:PATTERN'" in output
|
||||
assert "- Example: borg list ARCHIVE --match-archives 'sh:old-*'" in output
|
||||
13
src/borg/testsuite/helpers/argparsing_test.py
Normal file
13
src/borg/testsuite/helpers/argparsing_test.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from ...helpers.argparsing import _suggest_move_options_after_subcommand
|
||||
|
||||
|
||||
def test_suggest_reorder_unrecognized_args_before_subcommand(monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", ["borg", "--stats", "create", "foo"])
|
||||
s = _suggest_move_options_after_subcommand("error: Unrecognized arguments: --stats")
|
||||
assert s is not None
|
||||
assert "create" in s and "--stats" in s
|
||||
assert s.index("create") < s.index("--stats")
|
||||
|
|
@ -913,7 +913,7 @@ def test_remote_rpc_exception_transport(remote_repository):
|
|||
remote_repository.call("inject_exception", {"kind": "DoesNotExist"})
|
||||
except LegacyRepository.DoesNotExist as e:
|
||||
assert len(e.args) == 1
|
||||
assert e.args[0] == remote_repository.location.processed
|
||||
assert remote_repository.location.processed in e.args[0]
|
||||
|
||||
try:
|
||||
remote_repository.call("inject_exception", {"kind": "AlreadyExists"})
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ def test_remote_rpc_exception_transport(remote_repository):
|
|||
remote_repository.call("inject_exception", {"kind": "DoesNotExist"})
|
||||
except Repository.DoesNotExist as e:
|
||||
assert len(e.args) == 1
|
||||
assert e.args[0] == remote_repository.location.processed
|
||||
assert remote_repository.location.processed in e.args[0]
|
||||
|
||||
try:
|
||||
remote_repository.call("inject_exception", {"kind": "AlreadyExists"})
|
||||
|
|
|
|||
Loading…
Reference in a new issue