diff --git a/pyproject.toml b/pyproject.toml index 1327420aff..647d3012fd 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.8.0,!=1.8.2,!=1.9.0,!=1.9.1", # these generate broken zsh completions, see tqdm/shtab#224 + "shtab>=1.9.2", # older versions generate broken zsh completions (tqdm/shtab#224) or have no fish support "backports-zstd; python_version < '3.14'", # for python < 3.14. "jsonargparse>=4.47.0", "PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files diff --git a/scripts/shell_completions/fish/borg.fish b/scripts/shell_completions/fish/borg.fish deleted file mode 100644 index 3703053ccb..0000000000 --- a/scripts/shell_completions/fish/borg.fish +++ /dev/null @@ -1,538 +0,0 @@ -# Completions for borg -# https://www.borgbackup.org/ -# Note: -# Listing archives works on password-protected repositories only if $BORG_PASSPHRASE is set. -# Install: -# Copy this file to /usr/share/fish/vendor_completions.d/ - -# Commands - -complete -c borg -f -n __fish_is_first_token -a 'analyze' -d 'Analyze archives to find "hot spots"' -complete -c borg -f -n __fish_is_first_token -a 'create' -d 'Create a new archive' -complete -c borg -f -n __fish_is_first_token -a 'extract' -d 'Extract archive contents' -complete -c borg -f -n __fish_is_first_token -a 'check' -d 'Check repository consistency' -complete -c borg -f -n __fish_is_first_token -a 'rename' -d 'Rename an existing archive' -complete -c borg -f -n __fish_is_first_token -a 'list' -d 'List archive or repository contents' -complete -c borg -f -n __fish_is_first_token -a 'diff' -d 'Find differences between archives' -complete -c borg -f -n __fish_is_first_token -a 'delete' -d 'Delete an archive' -complete -c borg -f -n __fish_is_first_token -a 'prune' -d 'Prune repository archives' -complete -c borg -f -n __fish_is_first_token -a 'compact' -d 'Free repository space' -complete -c borg -f -n __fish_is_first_token -a 'info' -d 'Show archive details' -complete -c borg -f -n __fish_is_first_token -a 'mount' -d 'Mount archive or a repository' -complete -c borg -f -n __fish_is_first_token -a 'umount' -d 'Unmount the mounted archive' -complete -c borg -f -n __fish_is_first_token -a 'repo-create' -d 'Create a new, empty repository' -complete -c borg -f -n __fish_is_first_token -a 'repo-delete' -d 'Delete a repository' -complete -c borg -f -n __fish_is_first_token -a 'repo-info' -d 'Show repository information' -complete -c borg -f -n __fish_is_first_token -a 'repo-list' -d 'List repository contents' -complete -c borg -f -n __fish_is_first_token -a 'repo-space' -d 'Manage reserved space in a repository' -complete -c borg -f -n __fish_is_first_token -a 'tag' -d 'Tag archives' -complete -c borg -f -n __fish_is_first_token -a 'transfer' -d 'Transfer of archives from another repository' -complete -c borg -f -n __fish_is_first_token -a 'undelete' -d 'Undelete archives' -complete -c borg -f -n __fish_is_first_token -a 'version' -d 'Display Borg client version / Borg server version' - -function __fish_borg_seen_key - if __fish_seen_subcommand_from key - and not __fish_seen_subcommand_from import export change-passphrase - return 0 - end - return 1 -end -complete -c borg -f -n __fish_is_first_token -a 'key' -d 'Manage a repository key' -complete -c borg -f -n __fish_borg_seen_key -a 'import' -d 'Import a repository key' -complete -c borg -f -n __fish_borg_seen_key -a 'export' -d 'Export a repository key' -complete -c borg -f -n __fish_borg_seen_key -a 'change-passphrase' -d 'Change key file passphrase' - -complete -c borg -f -n __fish_is_first_token -a 'serve' -d 'Start in server mode' -complete -c borg -f -n __fish_is_first_token -a 'recreate' -d 'Recreate contents of existing archives' -complete -c borg -f -n __fish_is_first_token -a 'export-tar' -d 'Create tarball from an archive' -complete -c borg -f -n __fish_is_first_token -a 'with-lock' -d 'Run a command while the repository lock is held' -complete -c borg -f -n __fish_is_first_token -a 'break-lock' -d 'Break the repository lock' - -function __fish_borg_seen_benchmark - if __fish_seen_subcommand_from benchmark - and not __fish_seen_subcommand_from crud - return 0 - end - return 1 -end -complete -c borg -f -n __fish_is_first_token -a 'benchmark' -d 'Benchmark borg operations' -complete -c borg -f -n __fish_borg_seen_benchmark -a 'crud' -d 'Benchmark borg CRUD operations' - -function __fish_borg_seen_help - if __fish_seen_subcommand_from help - and not __fish_seen_subcommand_from patterns placeholders compression - return 0 - end - return 1 -end -complete -c borg -f -n __fish_is_first_token -a 'help' -d 'Miscellaneous Help' -complete -c borg -f -n __fish_borg_seen_help -a 'patterns' -d 'Help for patterns' -complete -c borg -f -n __fish_borg_seen_help -a 'placeholders' -d 'Help for placeholders' -complete -c borg -f -n __fish_borg_seen_help -a 'compression' -d 'Help for compression' - -# Common options -complete -c borg -f -s h -l 'help' -d 'Show help information' -complete -c borg -f -l 'version' -d 'Show version information' -complete -c borg -f -l 'critical' -d 'Log level CRITICAL' -complete -c borg -f -l 'error' -d 'Log level ERROR' -complete -c borg -f -l 'warning' -d 'Log level WARNING (default)' -complete -c borg -f -l 'info' -d 'Log level INFO' -complete -c borg -f -s v -l 'verbose' -d 'Log level INFO' -complete -c borg -f -l 'debug' -d 'Log level DEBUG' -complete -c borg -f -l 'debug-topic' -d 'Enable TOPIC debugging' -complete -c borg -f -s p -l 'progress' -d 'Show progress information' -complete -c borg -f -l 'log-json' -d 'Output one JSON object per log line' -complete -c borg -f -l 'lock-wait' -d 'Wait for lock max N seconds [1]' -complete -c borg -f -l 'show-version' -d 'Log version information' -complete -c borg -f -l 'show-rc' -d 'Log the return code' -complete -c borg -f -l 'umask' -d 'Set umask to M [0077]' - -# borg analyze options -complete -c borg -f -s a -l 'match-archives' -d 'Only archive names matching PATTERN' -n "__fish_seen_subcommand_from analyze" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from analyze" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from analyze" - -# borg create options -complete -c borg -f -s n -l 'dry-run' -d 'Do not create a backup archive' -n "__fish_seen_subcommand_from create" -complete -c borg -f -s s -l 'stats' -d 'Print verbose statistics' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'filter' -d 'Only items with given STATUSCHARS' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'json' -d 'Print verbose stats as json' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'stdin-name' -d 'Use NAME in archive for stdin data' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'content-from-command' -d 'Interpret PATH as command and store its stdout' -n "__fish_seen_subcommand_from create" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from create" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from create" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'exclude-caches' -d 'Exclude directories tagged as cache' -n "__fish_seen_subcommand_from create" -complete -c borg -l 'exclude-if-present' -d 'Exclude directories that contain FILENAME' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'keep-exclude-tags' -d 'Keep tag files of excluded directories' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'exclude-nodump' -d 'Exclude files flagged NODUMP' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'exclude-dataless' -d 'Exclude files flagged DATALESS (macOS)' -n "__fish_seen_subcommand_from create" -# Filesystem options -complete -c borg -f -s x -l 'one-file-system' -d 'Stay in the same file system' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'numeric-ids' -d 'Only store numeric user:group identifiers' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'noatime' -d 'Do not store atime' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'noctime' -d 'Do not store ctime' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'nobirthtime' -d 'Do not store creation date' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'nobsdflags' -d 'Do not store bsdflags' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'noacls' -d 'Do not read and store ACLs into archive' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'noxattrs' -d 'Do not read and store xattrs into archive' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'noflags' -d 'Do not store flags' -n "__fish_seen_subcommand_from create" -set -l files_cache_mode "ctime,size,inode mtime,size,inode ctime,size mtime,size rechunk,ctime rechunk,mtime size disabled" -complete -c borg -f -l 'files-cache' -d 'Operate files cache in MODE' -a "$files_cache_mode" -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'read-special' -d 'Open device files like regular files' -n "__fish_seen_subcommand_from create" -# Archive options -complete -c borg -f -l 'comment' -d 'Add COMMENT to the archive' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'timestamp' -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)' -n "__fish_seen_subcommand_from create" -complete -c borg -l 'timestamp' -d 'Set creation time by reference FILE' -n "__fish_seen_subcommand_from create" -complete -c borg -f -l 'chunker-params' -d 'Chunker PARAMETERS [19,23,21,4095]' -n "__fish_seen_subcommand_from create" -complete -c borg -f -s C -l 'compression' -d 'Select compression ALGORITHM,LEVEL [lz4]' -a "$compression_methods" -n "__fish_seen_subcommand_from create" - -# borg extract options -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -s n -l 'dry-run' -d 'Do not actually extract any files' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'numeric-ids' -d 'Only obey numeric user:group identifiers' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'nobsdflags' -d 'Do not extract/set bsdflags' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'noflags' -d 'Do not extract/set flags' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'noacls' -d 'Do not extract/set ACLs' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'noxattrs' -d 'Do not extract/set xattrs' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'stdout' -d 'Write all extracted data to stdout' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'sparse' -d 'Create holes in output sparse file' -n "__fish_seen_subcommand_from extract" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from extract" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from extract" -complete -c borg -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from extract" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from extract" -complete -c borg -f -l 'strip-components' -d 'Remove NUMBER of leading path elements' -n "__fish_seen_subcommand_from extract" - -# borg check options -complete -c borg -f -l 'repository-only' -d 'Only perform repository checks' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'archives-only' -d 'Only perform archives checks' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'verify-data' -d 'Cryptographic integrity verification' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'repair' -d 'Attempt to repair found inconsistencies' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'max-duration' -d 'Partial repo check for max. SECONDS' -n "__fish_seen_subcommand_from check" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from check" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from check" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from check" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from check" - -# borg rename -# no specific options - -# borg list options -complete -c borg -f -l 'short' -d 'Only print file/directory names' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'format' -d 'Specify FORMAT for file listing' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'json' -d 'List contents in json format' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'json-lines' -d 'List contents in json lines format' -n "__fish_seen_subcommand_from list" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from list" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from list" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from list" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from list" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from list" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from list" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from list" - -# borg diff options -complete -c borg -f -l 'numeric-ids' -d 'Only consider numeric user:group' -n "__fish_seen_subcommand_from diff" -complete -c borg -f -l 'same-chunker-params' -d 'Override check of chunker parameters' -n "__fish_seen_subcommand_from diff" -complete -c borg -f -l 'sort' -d 'Sort the output lines by file path' -n "__fish_seen_subcommand_from diff" -complete -c borg -f -l 'json-lines' -d 'Format output as JSON Lines' -n "__fish_seen_subcommand_from diff" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from diff" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from diff" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from diff" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from diff" - -# borg delete options -complete -c borg -f -s n -l 'dry-run' -d 'Do not change the repository' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'list' -d 'Output verbose list of archives' -n "__fish_seen_subcommand_from delete" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from delete" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from delete" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from delete" - -# borg prune options -complete -c borg -f -s n -l 'dry-run' -d 'Do not change the repository' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'force' -d 'Force pruning of corrupted archives' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s s -l 'stats' -d 'Print verbose statistics' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-within' -d 'Keep archives within time INTERVAL' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-last' -d 'NUMBER of secondly archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-secondly' -d 'NUMBER of secondly archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-minutely' -d 'NUMBER of minutely archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s H -l 'keep-hourly' -d 'NUMBER of hourly archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s d -l 'keep-daily' -d 'NUMBER of daily archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s w -l 'keep-weekly' -d 'NUMBER of weekly archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s m -l 'keep-monthly' -d 'NUMBER of monthly archives to keep' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-13weekly' -d 'NUMBER of quarterly archives to keep (13 week strategy)' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -l 'keep-3monthly' -d 'NUMBER of quarterly archives to keep (3 month strategy)' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s y -l 'keep-yearly' -d 'NUMBER of yearly archives to keep' -n "__fish_seen_subcommand_from prune" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from prune" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from prune" - -# borg compact options -complete -c borg -f -s n -l 'dry-run' -d 'Do nothing' -n "__fish_seen_subcommand_from compact" -complete -c borg -f -s s -l 'stats' -d 'Print statistics (might be much slower)' -n "__fish_seen_subcommand_from compact" - -# borg info options -complete -c borg -f -l 'json' -d 'Format output in json format' -n "__fish_seen_subcommand_from info" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from info" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from info" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from info" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from info" - -# borg repo-list options -complete -c borg -f -l 'short' -d 'Only print the archive IDs, nothing else' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'format' -d 'Specify format for archive listing' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'json' -d 'Format output as JSON' -n "__fish_seen_subcommand_from repo-list" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from repo-list" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from repo-list" -complete -c borg -f -l 'deleted' -d 'Consider only soft-deleted archives' -n "__fish_seen_subcommand_from repo-list" - -# borg mount options -complete -c borg -f -s f -l 'foreground' -d 'Stay in foreground, do not daemonize' -n "__fish_seen_subcommand_from mount" -# FIXME This list is probably not full, but I tried to pick only those that are relevant to borg mount -o: -set -l fuse_options "ac_attr_timeout= allow_damaged_files allow_other allow_root attr_timeout= auto auto_cache auto_unmount default_permissions entry_timeout= gid= group_id= kernel_cache max_read= negative_timeout= noauto noforget remember= remount rootmode= uid= umask= user user_id= versions" -complete -c borg -f -s o -d 'Fuse mount OPTION' -a "$fuse_options" -n "__fish_seen_subcommand_from mount" -# Archive filters -complete -c borg -f -s P -l 'prefix' -d 'Only archive names starting with PREFIX' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from mount" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from mount" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from mount" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from mount" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from mount" -complete -c borg -f -l 'strip-components' -d 'Remove NUMBER of leading path elements' -n "__fish_seen_subcommand_from mount" - -# borg umount -# no specific options - -# borg tag options -complete -c borg -f -l 'set' -d 'Set tags (can be given multiple times)' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'add' -d 'Add tags (can be given multiple times)' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'remove' -d 'Remove tags (can be given multiple times)' -n "__fish_seen_subcommand_from tag" -# Archive filters -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from tag" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from tag" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from tag" - -# borg key change-passphrase -# no specific options - -# borg key export -complete -c borg -f -l 'paper' -d 'Create an export for printing' -n "__fish_seen_subcommand_from export" -complete -c borg -f -l 'qr-html' -d 'Create an html file for printing and qr' -n "__fish_seen_subcommand_from export" - -# borg transfer options -complete -c borg -f -s n -l 'dry-run' -d 'Do not change repository, just check' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'other-repo' -d 'Transfer archives from the other repository' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'from-borg1' -d 'Other repository is borg 1.x' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'upgrader' -d 'Use the upgrader to convert transferred data' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -s C -l 'compression' -d 'Select compression algorithm' -a "$compression_methods" -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'recompress' -d 'Recompress chunks CONDITION' -a "$recompress_when" -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'chunker-params' -d 'Chunker PARAMETERS [19,23,21,4095]' -n "__fish_seen_subcommand_from transfer" -# Archive filters -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from transfer" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from transfer" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from transfer" - -# borg key import -complete -c borg -f -l 'paper' -d 'Import from a backup done with --paper' -n "__fish_seen_subcommand_from import" - -# borg undelete options -complete -c borg -f -s n -l 'dry-run' -d 'Do not change repository' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'list' -d 'Output verbose list of archives' -n "__fish_seen_subcommand_from undelete" -# Archive filters -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from undelete" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from undelete" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from undelete" - - -# borg recreate -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'filter' -d 'Only items with given STATUSCHARS' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -s n -l 'dry-run' -d 'Do not change the repository' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -s s -l 'stats' -d 'Print verbose statistics' -n "__fish_seen_subcommand_from recreate" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'exclude-caches' -d 'Exclude directories tagged as cache' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'exclude-if-present' -d 'Exclude directories that contain FILENAME' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'keep-exclude-tags' -d 'Keep tag files of excluded directories' -n "__fish_seen_subcommand_from recreate" -# Archive filters -complete -c borg -f -s a -l 'match-archives' -d 'Only consider archives matching all patterns' -n "__fish_seen_subcommand_from recreate" -set -l sort_keys "timestamp archive name id tags host user" -complete -c borg -f -l 'sort-by' -d 'Sorting KEYS [timestamp]' -a "$sort_keys" -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'first' -d 'Only first N archives' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'last' -d 'Only last N archives' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'oldest' -d 'Consider archives within TIMESPAN from oldest' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'newest' -d 'Consider archives within TIMESPAN from newest' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'older' -d 'Consider archives older than TIMESPAN' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'newer' -d 'Consider archives newer than TIMESPAN' -n "__fish_seen_subcommand_from recreate" -# Archive options -complete -c borg -f -l 'target' -d "Create a new ARCHIVE" -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'comment' -d 'Add COMMENT to the archive' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'timestamp' -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'timestamp' -d 'Set creation time using reference FILE' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -s C -l 'compression' -d 'Select compression ALGORITHM,LEVEL [lz4]' -a "$compression_methods" -n "__fish_seen_subcommand_from recreate" -set -l recompress_when "if-different always never" -complete -c borg -f -l 'recompress' -d 'Recompress chunks CONDITION' -a "$recompress_when" -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'chunker-params' -d 'Chunker PARAMETERS [19,23,21,4095]' -n "__fish_seen_subcommand_from recreate" - -# borg export-tar options -complete -c borg -l 'tar-filter' -d 'Filter program to pipe data through' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -f -l 'tar-format' -d 'Select tar format: BORG, PAX or GNU' -n "__fish_seen_subcommand_from export-tar" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from export-tar" -complete -c borg -f -l 'strip-components' -d 'Remove NUMBER of leading path elements' -n "__fish_seen_subcommand_from export-tar" - -# borg import-tar options -complete -c borg -l 'tar-filter' -d 'Filter program to pipe data through' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -s s -l 'stats' -d 'Print statistics for the created archive' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'list' -d 'Print verbose list of items' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'filter' -d 'Only display items with given STATUSCHARS' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'json' -d 'Output stats as JSON' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'ignore-zeros' -d 'Ignore zero-filled blocks in the input' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'comment' -d 'Add COMMENT to the archive' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'timestamp' -d 'Set creation TIME (yyyy-mm-ddThh:mm:ss)' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -l 'chunker-params' -d 'Chunker PARAMETERS [19,23,21,4095]' -n "__fish_seen_subcommand_from import-tar" -complete -c borg -f -s C -l 'compression' -d 'Select compression ALGORITHM,LEVEL [lz4]' -a "$compression_methods" -n "__fish_seen_subcommand_from import-tar" -# Exclusion options -complete -c borg -s e -l 'exclude' -d 'Exclude paths matching PATTERN' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'exclude-from' -d 'Read exclude patterns from EXCLUDEFILE' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'pattern' -d 'Include/exclude paths matching PATTERN' -n "__fish_seen_subcommand_from recreate" -complete -c borg -l 'patterns-from' -d 'Include/exclude paths from PATTERNFILE' -n "__fish_seen_subcommand_from recreate" -complete -c borg -f -l 'strip-components' -d 'Remove NUMBER of leading path elements' -n "__fish_seen_subcommand_from recreate" - -# borg serve -complete -c borg -l 'restrict-to-path' -d 'Restrict repository access to PATH' -n "__fish_seen_subcommand_from serve" -complete -c borg -l 'restrict-to-repository' -d 'Restrict repository access at PATH' -n "__fish_seen_subcommand_from serve" - - -# borg with-lock -# no specific options - -# borg break-lock -# no specific options - -# borg benchmark -# no specific options - -# borg help -# no specific options - - -function __fish_borg_archives - # additionally to aid:XXXXXXXX we show archive (series) name and timestamp - borg repo-list --format="aid:{id:.8}{TAB}{archive} {start}{NEWLINE}" 2>/dev/null -end - -function __fish_borg_archive_arg --description 'Test if current command is a specific borg command with token count' --argument command token_count - # Check if we're in the context of a specific borg command - set -l tokens (commandline --tokenize) - set -l cmdline (commandline --current-process) - - # Make sure we're in a borg command context - if not test $tokens[1] = "borg" - return 1 - end - - # Make sure we're in the specific command context - if not test $tokens[2] = "$command" - return 1 - end - - # Check if we're at the right token position - if not test (count $tokens) "-eq" "$token_count" - return 1 - end - - # Additional check to ensure we're not in the middle of typing an option - if string match --quiet --regex -- "^-" (commandline --current-token) - return 1 - end - - return 0 -end - -# The following completions use the -F flag to force disable filename completion -# for various borg commands, ensuring only archive names are suggested. -# We also use the -e flag to explicitly erase all default completions before adding our custom ones. - -# Global rules to disable filename completions for specific borg commands -# This ensures that no filename completions are shown for these commands -complete -c borg -e -n '__fish_seen_subcommand_from diff delete list info extract mount export-tar rename tag undelete recreate transfer check analyze' - -# First, explicitly erase all default completions for each command -# This is the most specific rule and should take precedence -complete -c borg -e -n '__fish_borg_archive_arg "diff" 2' -complete -c borg -e -n '__fish_borg_archive_arg "diff" 3' -complete -c borg -e -n '__fish_borg_archive_arg "delete" 2' -complete -c borg -e -n '__fish_borg_archive_arg "list" 2' -complete -c borg -e -n '__fish_borg_archive_arg "info" 2' -complete -c borg -e -n '__fish_borg_archive_arg "extract" 2' -complete -c borg -e -n '__fish_borg_archive_arg "mount" 2' -complete -c borg -e -n '__fish_borg_archive_arg "export-tar" 2' -complete -c borg -e -n '__fish_borg_archive_arg "rename" 2' -complete -c borg -e -n '__fish_borg_archive_arg "tag" 2' -complete -c borg -e -n '__fish_borg_archive_arg "undelete" 2' -complete -c borg -e -n '__fish_borg_archive_arg "recreate" 2' -complete -c borg -e -n '__fish_borg_archive_arg "transfer" 2' -complete -c borg -e -n '__fish_borg_archive_arg "check" 2' -complete -c borg -e -n '__fish_borg_archive_arg "analyze" 2' - -# Also add specific rules to disable filename completions at the exact position -# This ensures that no filename completions are shown at the position where we expect archive names -complete -c borg --no-files -n '__fish_borg_archive_arg "diff" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "diff" 3' -complete -c borg --no-files -n '__fish_borg_archive_arg "delete" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "list" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "info" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "extract" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "mount" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "export-tar" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "rename" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "tag" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "undelete" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "recreate" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "transfer" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "check" 2' -complete -c borg --no-files -n '__fish_borg_archive_arg "analyze" 2' - -# Then add our custom completions with high priority and no-files -# This ensures no filename completions are shown and our custom completions take precedence -complete -c borg -p 100 -n '__fish_borg_archive_arg "diff" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "diff" 3' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "delete" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "list" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "info" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "extract" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "mount" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "export-tar" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "rename" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "tag" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "undelete" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "recreate" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "transfer" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "check" 2' -a '(__fish_borg_archives)' --no-files -complete -c borg -p 100 -n '__fish_borg_archive_arg "analyze" 2' -a '(__fish_borg_archives)' --no-files diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 6b73444913..52e4809021 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -2,8 +2,16 @@ 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. +scripts for bash, zsh, tcsh and fish. For bash, zsh and tcsh, it uses the shtab library +for basic completion generation and extends it with custom dynamic completions for +Borg-specific argument types. For fish, it currently brings its own generator that +walks the parser tree and emits fish ``complete`` commands, extended with fish versions +of the same dynamic completions: released shtab versions have several issues in their +fish backend (no file completion, conditions confused by option values and by values +matching command names, positional order ignored). Fixes are upstream in +https://github.com/tqdm/shtab/pull/232 and https://github.com/tqdm/shtab/pull/233 - +once they are merged and released, borg will use shtab's fish backend (like it already +does for bash/zsh) and the then-duplicate fish generator code here will be removed. Dynamic Completions ------------------- @@ -13,7 +21,7 @@ 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 + - In zsh and fish, shows archive metadata (name, timestamp, user@host) as descriptions - Respects --repo/-r flags to query the correct repository 2. Sort keys (SortBySpec): @@ -67,6 +75,7 @@ ) from ..helpers.argparsing import ArgumentParser from ..helpers.argparsing import ActionSubCommands +from ..helpers.argparsing import SUPPRESS from ..helpers.time import timestamp from ..helpers.parseformat import partial_format from ..manifest import AI_HUMAN_SORT_KEYS @@ -626,6 +635,350 @@ } """ +# Global fish preamble. It provides: +# - _borg_scan / _borg_using_command: command line parsing helpers used in the +# conditions (-n) of the generated `complete` commands. They rely on the +# $_borg_commands and $_borg_opts_with_value lists emitted by the generator. +# - dynamic completion functions mirroring the bash/zsh ones above. +FISH_PREAMBLE_TMPL = r""" +# Parse the command line: set $_borg_cmdpath to the (sub)command path seen so far +# (e.g. "create" or "key export") and $_borg_npos to the number of positional +# arguments already given after it. Options and their values are skipped, based +# on the generated $_borg_opts_with_value and $_borg_commands lists. +function _borg_scan + set -g _borg_cmdpath '' + set -g _borg_npos 0 + set -l tokens (commandline -opc) + set -e tokens[1] + set -l expect_value 0 + for t in $tokens + if test $expect_value -eq 1 + set expect_value 0 + continue + end + switch "$t" + case '--*=*' + continue + case '-*' + if contains -- $t $_borg_opts_with_value + set expect_value 1 + end + continue + case '*' + if test $_borg_npos -eq 0 + set -l candidate $t + if test -n "$_borg_cmdpath" + set candidate "$_borg_cmdpath $t" + end + if contains -- $candidate $_borg_commands + set -g _borg_cmdpath $candidate + continue + end + end + set -g _borg_npos (math $_borg_npos + 1) + end + end +end + +# Condition helper: true if the current (sub)command path equals the given one. +function _borg_using_command + _borg_scan + test "$_borg_cmdpath" = "$argv" +end + +# derive repo context from the command line: --repo=V, --repo V, -r=V, -rV, or -r V +function _borg_repo_args + set -l tokens (commandline -opc) + set -l n (count $tokens) + for i in (seq 2 $n) + set -l w $tokens[$i] + switch "$w" + case '--repo=*' + printf '%s\n' --repo (string replace -- '--repo=' '' $w) + return + case '-r=*' + printf '%s\n' -r (string replace -- '-r=' '' $w) + return + case '--repo' '-r' + if test $i -lt $n + printf '%s\n' $w $tokens[(math $i + 1)] + end + return + case '-r*' + printf '%s\n' -r (string replace -r -- '^-r' '' $w) + return + end + end +end + +# Complete archive names, or archive IDs when the current token starts with "aid:" +function _borg_complete_archive + set -l cur (commandline -ct) + set -l repo_args (_borg_repo_args) + if string match -q -- 'aid:*' $cur + set -l prefix (string replace -- 'aid:' '' $cur) + if test -n "$prefix"; and not string match -qr -- '^[0-9a-fA-F]+$' $prefix + return + end + # ask borg for IDs with metadata; avoid prompts and suppress stderr + borg repo-list $repo_args --format '{id}{TAB}{archive}{TAB}{time}{TAB}{username}@{hostname}{NL}' \ + 2>/dev/null /dev/null /dev/null str: + """Escape text for inclusion in a single-quoted fish string.""" + return text.replace("\\", "\\\\").replace("'", "\\'") + + +def _fish_help_text(action) -> str: + """Expand %(...)s placeholders in an action's help like argparse does and collapse whitespace.""" + if not action.help or action.help is SUPPRESS: + return "" + params = {k: v for k, v in vars(action).items() if v is not SUPPRESS} + params["prog"] = "borg" + if params.get("choices") is not None: + params["choices"] = ", ".join(str(c) for c in params["choices"]) + try: + text = action.help % params + except (KeyError, ValueError, TypeError): + text = action.help + return " ".join(text.split()) + + +def _fish_option_line(cond, action) -> str: + """Build one fish `complete` command for an option action.""" + parts = ["complete -c borg"] + if cond: + parts.append(f"-n '{cond}'") + for opt in action.option_strings: + if opt.startswith("--"): + parts.append(f"-l {opt[2:]}") + else: + parts.append(f"-s {opt[1:]}") + if action.nargs == 0: # a flag, takes no value + parts.append("-f") + else: + complete = getattr(action, "complete", None) + fish_fn = complete.get("fish") if isinstance(complete, dict) else None + if action.choices: + parts.append(f"-x -a '{_fish_quote(' '.join(str(c) for c in action.choices))}'") + elif fish_fn: + parts.append(f"-x -a '({fish_fn})'") + else: + # no completion known for the value: fall back to file completion + parts.append("-r") + help_text = _fish_help_text(action) + if help_text: + parts.append(f"-d '{_fish_quote(help_text)}'") + return " ".join(parts) + + +def _fish_completion_script(parser: ArgumentParser, preamble: str) -> str: + """Generate the fish completion script by walking the parser tree.""" + + def iter_parsers(parser, path=""): + yield path, parser + for action in parser._actions: + if isinstance(action, ActionSubCommands): + for name, sub in action.choices.items(): + yield from iter_parsers(sub, f"{path} {name}".strip()) + + parsers = list(iter_parsers(parser)) + commands = [path for path, _ in parsers if path] + + # Option strings that expect a value; _borg_scan uses this list to skip option + # values when looking for subcommand names and positional arguments. If an + # option string is a flag in one command but takes a value in another one + # (currently only --keep: prune vs. key change-location), it ends up in the + # list, i.e. is treated as value-taking - the lesser evil. + opts_with_value = sorted({opt for _, p in parsers for a in p._actions if a.nargs != 0 for opt in a.option_strings}) + + # Options available identically in every context (-h and the common options) + # are emitted only once, without a condition; everything else is emitted per + # command, guarded by a _borg_using_command condition. + global_keys = set.intersection( + *({tuple(a.option_strings) for a in p._actions if a.option_strings} for _, p in parsers) + ) + + def quoted_list(items): + return " ".join(f"'{_fish_quote(i)}'" for i in items) + + lines = [ + "# fish completion script for borg, generated by 'borg completion fish'", + "", + preamble.strip("\n"), + "", + f"set -g _borg_commands {quoted_list(commands)}", + f"set -g _borg_opts_with_value {quoted_list(opts_with_value)}", + "", + "complete -c borg -e # remove any previously defined borg completions", + ] + + for path, p in parsers: + cond = f"_borg_using_command {path}" if path else "_borg_using_command" + lines.append("") + lines.append(f"# borg {path}" if path else "# borg (top level)") + + subcommands_action = next((a for a in p._actions if isinstance(a, ActionSubCommands)), None) + if subcommands_action is not None: + helps = {ca.dest: ca.help for ca in getattr(subcommands_action, "_choices_actions", [])} + for name in subcommands_action.choices: + line = f"complete -c borg -f -n '{cond}; and test $_borg_npos -eq 0' -a '{name}'" + help_text = " ".join((helps.get(name) or "").split()) + if help_text: + line += f" -d '{_fish_quote(help_text)}'" + lines.append(line) + + for action in p._actions: + if not action.option_strings or action.help is SUPPRESS: + continue + key = tuple(action.option_strings) + if key in global_keys and path: + continue # already emitted at the top level, without a condition + lines.append(_fish_option_line(None if key in global_keys else cond, action)) + + pos_index = 0 + for action in p._actions: + if action.option_strings or isinstance(action, ActionSubCommands): + continue + open_ended = action.nargs not in (None, "?") # i.e. "*", "+" or REMAINDER + complete = getattr(action, "complete", None) + fish_fn = complete.get("fish") if isinstance(complete, dict) else None + if action.choices: + candidates = f"-a '{_fish_quote(' '.join(str(c) for c in action.choices))}'" + elif fish_fn: + candidates = f"-a '({fish_fn})'" + else: + candidates = None # no completion known: fish's default file completion applies + if candidates: + op = "-ge" if open_ended else "-eq" + line = f"complete -c borg -f -n '{cond}; and test $_borg_npos {op} {pos_index}' {candidates}" + help_text = _fish_help_text(action) + if help_text: + line += f" -d '{_fish_quote(help_text)}'" + lines.append(line) + if open_ended: + break # everything further on the command line belongs to this argument + pos_index += 1 + + return "\n".join(lines) + "\n" + def _attach_completion(parser: ArgumentParser, type_class, completion_dict: dict): """Tag all arguments with type `type_class` with completion choices from `completion_dict`.""" @@ -660,32 +1013,21 @@ def do_completion(self, args): # 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_completion( - parser, archivename_validator, {"bash": "_borg_complete_archive", "zsh": "_borg_complete_archive"} - ) - _attach_completion(parser, SortBySpec, {"bash": "_borg_complete_sortby", "zsh": "_borg_complete_sortby"}) - _attach_completion( - parser, FilesCacheMode, {"bash": "_borg_complete_filescachemode", "zsh": "_borg_complete_filescachemode"} - ) - _attach_completion( - parser, - CompressionSpec, - {"bash": "_borg_complete_compression_spec", "zsh": "_borg_complete_compression_spec"}, - ) - _attach_completion(parser, PathSpec, shtab.DIRECTORY) - _attach_completion( - parser, ChunkerParams, {"bash": "_borg_complete_chunker_params", "zsh": "_borg_complete_chunker_params"} - ) - _attach_completion(parser, tag_validator, {"bash": "_borg_complete_tags", "zsh": "_borg_complete_tags"}) - _attach_completion( - parser, - relative_time_marker_validator, - {"bash": "_borg_complete_relative_time", "zsh": "_borg_complete_relative_time"}, - ) - _attach_completion(parser, timestamp, {"bash": "_borg_complete_timestamp", "zsh": "_borg_complete_timestamp"}) - _attach_completion( - parser, parse_file_size, {"bash": "_borg_complete_file_size", "zsh": "_borg_complete_file_size"} - ) + + def for_all_shells(fn_name): + # same-named completion function in the bash, zsh and fish preambles + return {"bash": fn_name, "zsh": fn_name, "fish": fn_name} + + _attach_completion(parser, archivename_validator, for_all_shells("_borg_complete_archive")) + _attach_completion(parser, SortBySpec, for_all_shells("_borg_complete_sortby")) + _attach_completion(parser, FilesCacheMode, for_all_shells("_borg_complete_filescachemode")) + _attach_completion(parser, CompressionSpec, for_all_shells("_borg_complete_compression_spec")) + _attach_completion(parser, PathSpec, {**shtab.DIRECTORY, "fish": "__fish_complete_directories"}) + _attach_completion(parser, ChunkerParams, for_all_shells("_borg_complete_chunker_params")) + _attach_completion(parser, tag_validator, for_all_shells("_borg_complete_tags")) + _attach_completion(parser, relative_time_marker_validator, for_all_shells("_borg_complete_relative_time")) + _attach_completion(parser, timestamp, for_all_shells("_borg_complete_timestamp")) + _attach_completion(parser, parse_file_size, for_all_shells("_borg_complete_file_size")) # Collect all commands and help topics for "borg help" completion help_choices = list(self.helptext.keys()) @@ -693,8 +1035,7 @@ def do_completion(self, args): if isinstance(action, ActionSubCommands): help_choices.extend(action.choices.keys()) - help_completion_fn = "_borg_help_topics" - _attach_help_completion(parser, {"bash": help_completion_fn, "zsh": help_completion_fn}) + _attach_help_completion(parser, for_all_shells("_borg_help_topics")) # Build preambles using partial_format to avoid escaping braces etc. sort_keys = " ".join(AI_HUMAN_SORT_KEYS) @@ -734,6 +1075,12 @@ def do_completion(self, args): "FILE_SIZE_CHOICES": file_size_choices_str, "HELP_CHOICES": help_choices, } + if args.shell == "fish": + # borg brings its own fish backend until the shtab fixes are released, see the module docstring. + fish_preamble = partial_format(FISH_PREAMBLE_TMPL, mapping) + print(_fish_completion_script(parser, fish_preamble)) + return + bash_preamble = partial_format(BASH_PREAMBLE_TMPL, mapping) zsh_preamble = partial_format(ZSH_PREAMBLE_TMPL, mapping) @@ -747,6 +1094,7 @@ def do_completion(self, args): print(script) def build_parser_completion(self, subparsers, common_parser, mid_common_parser): + # includes fish, which do_completion dispatches to borg's own backend, not to shtab's. shells = tuple(shtab.SUPPORTED_SHELLS) completion_epilog = process_epilog( diff --git a/src/borg/testsuite/archiver/completion_cmd_test.py b/src/borg/testsuite/archiver/completion_cmd_test.py index ffa091199c..b5612ec3d5 100644 --- a/src/borg/testsuite/archiver/completion_cmd_test.py +++ b/src/borg/testsuite/archiver/completion_cmd_test.py @@ -1,10 +1,12 @@ import functools import os import subprocess +import sys import tempfile import pytest +import borg from . import cmd, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local") # NOQA @@ -22,6 +24,7 @@ def cmd_available(cmd): needs_bash = pytest.mark.skipif(not cmd_available("bash --version"), reason="Bash not available") needs_zsh = pytest.mark.skipif(not cmd_available("zsh --version"), reason="Zsh not available") +needs_fish = pytest.mark.skipif(not cmd_available("fish --version"), reason="Fish not available") def _run_bash_completion_fn(completion_script, setup_code): @@ -38,6 +41,42 @@ def _run_bash_completion_fn(completion_script, setup_code): return result +def _fish_quote(text): + """Escape text for inclusion in a single-quoted fish string.""" + return text.replace("\\", "\\\\").replace("'", "\\'") + + +def _fish_complete_candidates(completion_script, cmdline, path_prepend=None): + """Source the completion script in fish, return the completion candidates for cmdline.""" + code = completion_script + f"\ncomplete -C'{_fish_quote(cmdline)}'\n" + with tempfile.NamedTemporaryFile(mode="w", suffix=".fish", delete=False) as f: + f.write(code) + script_path = f.name + env = dict(os.environ) + if path_prepend: + env["PATH"] = path_prepend + os.pathsep + env.get("PATH", "") + try: + result = subprocess.run(["fish", script_path], capture_output=True, text=True, timeout=120, env=env) + finally: + os.unlink(script_path) + assert result.returncode == 0, f"fish failed: {result.stderr}" + # each output line is "candidatedescription" (or just "candidate") + return [line.split("\t")[0] for line in result.stdout.splitlines() if line.strip()] + + +def _borg_shim_dir(tmp_path): + """Create a dir with a borg script that runs the borg under test, for use inside fish.""" + src_dir = os.path.dirname(os.path.dirname(os.path.abspath(borg.__file__))) + shim_dir = tmp_path / "borg-shim" + shim_dir.mkdir() + shim = shim_dir / "borg" + shim.write_text( + f'#!/bin/sh\nexport PYTHONPATH="{src_dir}${{PYTHONPATH:+:$PYTHONPATH}}"\nexec "{sys.executable}" -m borg "$@"\n' + ) + shim.chmod(0o755) + return str(shim_dir) + + # -- output sanity checks ----------------------------------------------------- @@ -57,6 +96,14 @@ def test_zsh_completion_nontrivial(archivers, request): assert output.count("\n") > 100, f"Zsh completion suspiciously few lines: {output.count(chr(10))}" +def test_fish_completion_nontrivial(archivers, request): + """Verify the generated Fish completion is non-trivially sized.""" + archiver = request.getfixturevalue(archivers) + output = cmd(archiver, "completion", "fish") + assert len(output) > 5000, f"Fish completion suspiciously small: {len(output)} chars" + assert output.count("\n") > 100, f"Fish completion suspiciously few lines: {output.count(chr(10))}" + + # -- syntax validation -------------------------------------------------------- @@ -90,6 +137,15 @@ def test_zsh_completion_syntax(archivers, request): assert result.returncode == 0, f"Generated Zsh completion has syntax errors: {result.stderr.decode()}" +@needs_fish +def test_fish_completion_syntax(archivers, request): + """Verify the generated Fish completion script has valid syntax.""" + archiver = request.getfixturevalue(archivers) + output = cmd(archiver, "completion", "fish") + result = _check_shell_syntax(output, "fish", ".fish") + assert result.returncode == 0, f"Generated Fish completion has syntax errors: {result.stderr.decode()}" + + # -- borg-specific preamble function behavior (bash) -------------------------- @@ -182,3 +238,97 @@ def test_bash_archive_aid_completion(archivers, request): assert len(lines) >= 1, "Expected at least one archive ID completion" for line in lines: assert line.startswith("aid:"), f"Expected aid: prefix, got: {line}" + + +# -- borg-specific completion behavior (fish) --------------------------------- + + +@needs_fish +def test_fish_subcommand_completion(archivers, request): + """Command names should be completed at the top level and inside command groups.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "fish") + + candidates = _fish_complete_candidates(script, "borg ") + for name in ("create", "extract", "repo-list", "key", "debug"): + assert name in candidates, f"expected command {name} in: {candidates}" + + candidates = _fish_complete_candidates(script, "borg key ") + for name in ("export", "import", "change-passphrase"): + assert name in candidates, f"expected key subcommand {name} in: {candidates}" + + +@needs_fish +def test_fish_option_completion(archivers, request): + """Option names and static option values should be completed.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "fish") + + candidates = _fish_complete_candidates(script, "borg create --compres") + assert "--compression" in candidates, f"expected --compression in: {candidates}" + + candidates = _fish_complete_candidates(script, "borg create --compression ") + assert "lz4" in candidates, f"expected lz4 in: {candidates}" + assert "zstd,3" in candidates, f"expected zstd,3 in: {candidates}" + + +@needs_fish +def test_fish_sortby_dedup(archivers, request): + """_borg_complete_sortby should not re-offer already-selected sort keys.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "fish") + + candidates = _fish_complete_candidates(script, "borg repo-list --sort-by timestamp,") + assert "timestamp,archive" in candidates, f"expected timestamp,archive in: {candidates}" + assert "timestamp,timestamp" not in candidates, f"timestamp was re-offered: {candidates}" + + +@needs_fish +def test_fish_filescachemode_exclusivity(archivers, request): + """_borg_complete_filescachemode should enforce ctime/mtime and disabled mutual exclusion.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "fish") + + candidates = _fish_complete_candidates(script, "borg create --files-cache ctime,") + assert "ctime,size" in candidates, f"expected ctime,size in: {candidates}" + assert "ctime,mtime" not in candidates, f"mtime offered after ctime: {candidates}" + assert "ctime,disabled" not in candidates, f"disabled offered after ctime: {candidates}" + + candidates = _fish_complete_candidates(script, "borg create --files-cache disabled,") + assert candidates == [], f"completions offered after disabled: {candidates}" + + +@needs_fish +def test_fish_archive_name_completion(archivers, request, tmp_path): + """Archive names should be completed from a real repo.""" + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "mybackup-2024", archiver.input_path) + cmd(archiver, "create", "mybackup-2025", archiver.input_path) + + script = cmd(archiver, "completion", "fish") + repo = archiver.repository_path + + candidates = _fish_complete_candidates( + script, f"borg delete --repo {repo} mybackup", path_prepend=_borg_shim_dir(tmp_path) + ) + assert "mybackup-2024" in candidates, f"archive name missing: {candidates}" + assert "mybackup-2025" in candidates, f"archive name missing: {candidates}" + + +@needs_fish +def test_fish_archive_aid_completion(archivers, request, tmp_path): + """aid: prefixed archive IDs should be completed from a real repo.""" + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "testarchive", archiver.input_path) + + script = cmd(archiver, "completion", "fish") + repo = archiver.repository_path + + candidates = _fish_complete_candidates( + script, f"borg info --repo {repo} aid:", path_prepend=_borg_shim_dir(tmp_path) + ) + assert len(candidates) >= 1, "Expected at least one archive ID completion" + for candidate in candidates: + assert candidate.startswith("aid:"), f"Expected aid: prefix, got: {candidate}" diff --git a/src/borg/testsuite/shell_completions_test.py b/src/borg/testsuite/shell_completions_test.py deleted file mode 100644 index 4fa26faa5a..0000000000 --- a/src/borg/testsuite/shell_completions_test.py +++ /dev/null @@ -1,22 +0,0 @@ -import subprocess -from pathlib import Path - -import pytest - -SHELL_COMPLETIONS_DIR = Path(__file__).parent / ".." / ".." / ".." / "scripts" / "shell_completions" - - -def test_fish_completion_is_valid(): - """Test that the Fish completion file is valid Fish syntax.""" - fish_completion_file = SHELL_COMPLETIONS_DIR / "fish" / "borg.fish" - assert fish_completion_file.is_file() - - # Check if Fish is available - try: - subprocess.run(["fish", "--version"], capture_output=True, check=True) - except (subprocess.SubprocessError, FileNotFoundError): - pytest.skip("Fish not available") - - # Test whether the Fish completion file can be sourced without errors - result = subprocess.run(["fish", "-c", f"source {str(fish_completion_file)}"], capture_output=True) - assert result.returncode == 0, f"Fish completion file has syntax errors: {result.stderr.decode()}"