diff --git a/docs/changes.rst b/docs/changes.rst index c96558723..5af5f4568 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -167,10 +167,15 @@ Fixes: Other changes: +- remove handwritten bash and zsh shell completions, #9178. + these are now auto-generated via ``borg completion bash/zsh`` (using shtab). + fish completions are kept until shtab gains fish support. - mount: warn about symlinks pointing outside of the mountpoint, #9254 - Version: do not access private attributes, #9263 - tests / CI: + - completion: focused tests for auto-generated shell completions + (syntax validation, size sanity, borg-specific preamble behavior) - fix and re-enable Windows CI (some tests are skipped on Windows) - CI: faster with borg-dir/borg.exe, #9236 - fix mismatch in xattr test, #9238 diff --git a/scripts/shell_completions/bash/borg b/scripts/shell_completions/bash/borg deleted file mode 100644 index 77a5b353a..000000000 --- a/scripts/shell_completions/bash/borg +++ /dev/null @@ -1,223 +0,0 @@ -# Bash 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/bash-completion/completions/ or /etc/bash_completion.d/. - -_borg() -{ - compopt -o default - COMPREPLY=() - local cur="${COMP_WORDS[COMP_CWORD]}" - local prev="${COMP_WORDS[COMP_CWORD-1]}" - local prevprev="${COMP_WORDS[COMP_CWORD-2]}" - local common_opts="-h --help --critical --error --warning --info -v --verbose --debug --debug-topic -p --progress --iec --log-json --lock-wait --show-version --show-rc --umask --remote-path --upload-ratelimit --upload-buffer --debug-profile --rsh -r --repo" - local archive_filter_opts="--sort-by --first --last --oldest --newest --older --newer" - local opts="${common_opts}" - - # Commands - if [[ ${COMP_CWORD} == 1 ]] ; then - local borg_commands="analyze benchmark break-lock check compact create debug delete diff export-tar extract help import-tar info key list mount prune recreate rename repo-compress repo-create repo-delete repo-info repo-list repo-space serve tag transfer umount undelete version with-lock" - COMPREPLY=( $(compgen -W "${borg_commands}" -- ${cur}) ) - compopt +o default - return 0 - fi - - case "${prev}" in - 'key') - COMPREPLY=( $(compgen -W "change-location change-passphrase export import" -- ${cur}) ) - return 0 - ;; - 'benchmark') - COMPREPLY=( $(compgen -W "cpu crud" -- ${cur}) ) - return 0 - ;; - 'debug') - COMPREPLY=( $(compgen -W "info dump-archive-items dump-archive dump-manifest dump-repo-objs search-repo-objs get-obj id-hash parse-obj format-obj put-obj delete-obj convert-profile" -- ${cur}) ) - return 0 - ;; - 'help') - COMPREPLY=( $(compgen -W "patterns placeholders compression" -- ${cur}) ) - return 0 - ;; - '--encryption' | '-e') - local encryption_modes="authenticated authenticated-blake2 keyfile-aes-ocb keyfile-blake2-aes-ocb keyfile-blake2-chacha20-poly1305 keyfile-chacha20-poly1305 none repokey-aes-ocb repokey-blake2-aes-ocb repokey-blake2-chacha20-poly1305 repokey-chacha20-poly1305" - COMPREPLY=( $(compgen -W "${encryption_modes}" -- ${cur}) ) - return 0 - ;; - '--files-cache') - local files_cache_mode="ctime,size,inode mtime,size,inode ctime,size mtime,size rechunk,ctime rechunk,mtime size disabled" - COMPREPLY=( $(compgen -W "${files_cache_mode}" -- ${cur}) ) - return 0 - ;; - '--compression' | '-C') - local compression_methods="none auto lz4 zstd,1 zstd,2 zstd,3 zstd,4 zstd,5 zstd,6 zstd,7 zstd,8 zstd,9 zstd,10 zstd,11 zstd,12 zstd,13 zstd,14 zstd,15 zstd,16 zstd,17 zstd,18 zstd,19 zstd,20 zstd,21 zstd,22 zlib,1 zlib,2 zlib,3 zlib,4 zlib,5 zlib,6 zlib,7 zlib,8 zlib,9 lzma,0 lzma,1 lzma,2 lzma,3 lzma,4 lzma,5 lzma,6 lzma,7 lzma,8 lzma,9" - COMPREPLY=( $(compgen -W "${compression_methods}" -- ${cur}) ) - return 0 - ;; - '--sort-by') - local sort_keys="timestamp archive name id tags host user" - COMPREPLY=( $(compgen -W "${sort_keys}" -- ${cur}) ) - return 0 - ;; - '-o') - # FIXME This list is probably not complete, but I tried to pick only those that are relevant to borg mount -o: - local 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" - COMPREPLY=( $(compgen -W "${fuse_options}" -- ${cur}) ) - return 0 - ;; - '--recompress') - local recompress_when="if-different always never" - COMPREPLY=( $(compgen -W "${recompress_when}" -- ${cur}) ) - return 0 - ;; - '--upgrader') - local upgraders="From12To20 NoOp" - COMPREPLY=( $(compgen -W "${upgraders}" -- ${cur}) ) - return 0 - ;; - esac - - if [[ ${cur} == -* ]] ; then - case "${COMP_LINE}" in - *' analyze '*) - local opts="-a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - *' repo-create '*) - local opts="--other-repo --from-borg1 -e --encryption --copy-crypt-key ${common_opts}" - ;; - *' repo-list '*) - local opts="--short --format --json ${common_opts} -a --match-archives ${archive_filter_opts} --deleted" - ;; - *' repo-info '*) - local opts="--json ${common_opts}" - ;; - *' repo-compress '*) - local opts="-C --compression -s --stats ${common_opts}" - ;; - *' repo-delete '*) - local opts="-n --dry-run --list --force --cache-only --keep-security-info ${common_opts}" - ;; - *' repo-space '*) - local opts="--reserve --free ${common_opts}" - ;; - *' create '*) - local opts="-n --dry-run -s --stats --list --filter --json --stdin-name --stdin-user --stdin-group --stdin-mode --content-from-command --paths-from-stdin --paths-from-command --paths-delimiter -e --exclude --exclude-from --pattern --patterns-from --exclude-caches --exclude-if-present --keep-exclude-tags --exclude-nodump -x --one-file-system --numeric-ids --atime --noctime --nobirthtime --noflags --noacls --noxattrs --sparse --files-cache --read-special --comment --timestamp --chunker-params -C --compression ${common_opts}" - ;; - *' extract '*) - local opts="--list -n --dry-run --numeric-ids --noflags --noacls --noxattrs --stdout --sparse -e --exclude --exclude-from --pattern --patterns-from --strip-components ${common_opts}" - ;; - *' check '*) - local opts="--repository-only --archives-only --verify-data --repair --find-lost-archives --max-duration -a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - # rename - # no specific options - *" list "*) - local opts="--short --format --json-lines -e --exclude --exclude-from --pattern --patterns-from ${common_opts}" - ;; - *' diff '*) - local opts="--numeric-ids --same-chunker-params --sort --json-lines -e --exclude --exclude-from --pattern --patterns-from ${common_opts}" - ;; - *' delete '*) - local opts="-n --dry-run --list -s --stats --cache-only --force -a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - *' prune '*) - local opts="-n --dry-run --force -s --stats --list --keep-within --keep-last --keep-secondly --keep-minutely -H --keep-hourly -d --keep-daily -w --keep-weekly -m --keep-monthly --keep-13weekly --keep-3monthly -y --keep-yearly -a --match-archives ${common_opts}" - ;; - *' compact '*) - local opts="-n --dry-run -s --stats ${common_opts}" - ;; - *' info '*) - local opts="--json -a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - *' mount '*) - local opts="-f --foreground -o --numeric-ids -a --match-archives ${archive_filter_opts} -e --exclude --exclude-from --pattern --patterns-from --strip-components ${common_opts}" - ;; - # umount - # no specific options - # key change-passphrase - # no specific options - *' change-location '*) - local opts="${common_opts} keyfile repokey --keep" - ;; - *' export '*) - local opts="--paper --qr-html ${common_opts}" - ;; - *' import '*) - local opts="--paper ${common_opts}" - ;; - *' recreate '*) - local opts="--list --filter -n --dry-run -s --stats -e --exclude --exclude-from --pattern --patterns-from --exclude-caches --exclude-if-present --keep-exclude-tags -a --match-archives ${archive_filter_opts} --target --comment --timestamp -C --compression --chunker-params ${common_opts}" - ;; - *' export-tar '*) - local opts="--tar-filter --list --tar-format -e --exclude --exclude-from --pattern --patterns-from --strip-components ${common_opts}" - ;; - *' import-tar '*) - local opts="--tar-filter -s --stats --list --filter --json --ignore-zeros ${common_opts} --comment --timestamp --chunker-params -C --compression" - ;; - *' transfer '*) - local opts="-n --dry-run --other-repo --upgrader ${common_opts} -a --match-archives ${archive_filter_opts}" - ;; - *' serve '*) - local opts="--restrict-to-path --restrict-to-repository ${common_opts}" - ;; - *' tag '*) - local opts="--set --add --remove -a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - *' undelete '*) - local opts="-n --dry-run --list -a --match-archives ${archive_filter_opts} ${common_opts}" - ;; - # debug - # has subcommands, handled separately - # version - # no specific options - # with-lock - # no specific options - # break-lock - # no specific options - # benchmark crud - # no specific options - esac - - COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) - return 0 - fi - - # Get the repository name if available - # If there is a space before the "::" it means that no repository name was typed, - # so probably $BORG_REPO was set and we can still list the archives. - local repository_name="${COMP_LINE%%::*}" - repository_name=${repository_name##* } - - # Listing archives. - # Since "::" is treated as separate word in Bash, - # it is $cur when the cursor is right behind it - # and $prev if the user has started to type an archive name. - local typed_word=${cur} - local -i list_archives=0 - if [[ ${cur} == "::" ]] ; then - list_archives=1 - typed_word="" - fi - if [[ ${prev} == "::" ]] ; then - list_archives=1 - fi - # Second archive listing for borg diff - if [[ ${COMP_LINE} =~ ^.*\ diff\ .*::[^\ ]+\ ${cur}$ ]] ; then - list_archives=1 - fi - # Additional archive listing for borg delete - if [[ ${COMP_LINE} =~ ^.*\ delete\ .*::[^\ ]+.*${cur}$ ]] ; then - list_archives=1 - fi - if (( $list_archives )) ; then - local archives=$(borg list --short "${repository_name}" 2>/dev/null) - COMPREPLY=( $(compgen -W "${archives}" -- "${typed_word}" ) ) - return 0 - fi - - return 0 -} - -complete -F _borg borg diff --git a/scripts/shell_completions/zsh/_borg b/scripts/shell_completions/zsh/_borg deleted file mode 100644 index 5c5229737..000000000 --- a/scripts/shell_completions/zsh/_borg +++ /dev/null @@ -1,1586 +0,0 @@ -#compdef borg borgfs -P -value-,BORG_*,-default- - -# Zsh completion for Borg Backup 2.0.0b13 (2024-11-04). -# -# Recommended _borg specific settings: -# -# zstyle -e ':completion:*:*:borg-*:argument-rest:*' tag-order \ -# '[[ $words[CURRENT] == -* ]] && reply=( "! archives archive-files" "-" )' -# zstyle ':completion:*:*:(borg|-value-,BORG_)*' sort false -# zstyle ':completion:*:*:borg-*:*' gain-privileges true -# zstyle ':completion:*' fake-parameters 'BORG_REPO:scalar' -# -# Custom styles: -# -# archive-description-format -# Default: `{archive:<36} {time} [{id}]`. -# archive-sort -# In which order archive names should be listed. -# Possible values are: `inverse`, `timestamp`, `name`, `id`. -# file-description-format -# Default: `{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}`. -# path-style-selector -# Style selector used to select a path (when Borg would use either `pp` or `pf`). -# Default: `fm`. - -(( $+functions[_borg_commands] )) || -_borg_commands() { - local -a commands_=( - 'analyze:analyze archives' - 'benchmark:benchmark command' - 'break-lock:break the repository and cache locks' - 'check:verify repository' - 'compact:compact repository' - 'create:create new archive' - 'debug:debugging command (not intended for normal use)' - 'delete:delete archives' - 'diff:find differences in archive contents' - 'export-tar:export archive contents as a tarball' - 'extract:extract archive contents' - 'help:extra help' - 'import-tar:create an archive from a tarball' - 'info:show archive information' - 'key:manage repository key' - 'list:list archive contents' - 'mount:mount archive or an entire repository as a FUSE filesystem' - 'prune:prune archives according to specified rules' - 'recreate:re-create archives' - 'rename:rename an existing archive' - 'repo-compress:repository (re-)compression' - 'repo-create:create an empty repository' - 'repo-delete:delete a repository' - 'repo-info:show repository information' - 'repo-list:list repository contents' - 'repo-space:manage reserved space in a repository' - 'serve:start in server mode' - 'tag:tag archives' - 'transfer:transfer of archives from another repository' - 'umount:unmount the FUSE filesystem' - 'undelete:undelete archive' - 'version:display borg client version / borg server version' - 'with-lock:run a user-specified command with the repository lock held' - ) - _describe -t commands 'borg commands' commands_ -} - -(( $+functions[_borg-analyze] )) || -_borg-analyze() { - local -a common_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - $common_archive_filters_options \ - $common_options -} - -(( $+functions[_borg-benchmark] )) || -_borg-benchmark() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options \ - ':type:(crud cpu)' \ - ':PATH:_files' -} - -(( $+functions[_borg-break-lock] )) || -_borg-break-lock() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options -} - -(( $+functions[_borg-check] )) || -_borg-check() { - local -a common_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - '--repository-only[only perform repository checks]' \ - '--archives-only[only perform archives checks]' \ - '(--repository-only)--verify-data[perform cryptographic archive data integrity verification]' \ - '--repair[attempt to repair any inconsistencies found]' \ - '--max-duration=[partial repo check for max. SECONDS]: : _borg_guard_unsigned_number "SECONDS"' \ - $common_archive_filters_options \ - $common_options -} - -(( $+functions[_borg-compact] )) || -_borg-compact() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - '(-n --dry-run)'{-n,--dry-run}'[do nothing]' \ - '(-s --stats)'{-s,--stats}'[print statistics (might be much slower)]' \ - $common_options -} - -(( $+functions[_borg-create] )) || -_borg-create() { - local -a common_options common_create_options state line - local curcontext="$curcontext" state_descr - declare -A opt_args - local -i ret=1 - __borg_setup_common_options - __borg_setup_common_create_options - - local lastspec='*:PATH:_files' - (( $words[(I)--content-from-command] )) && - lastspec='*:::PATH:->command' - - _arguments -C -s -w -S : \ - '*'{-e,--exclude}'=[exclude paths matching PATTERN]: : _borg_style_selector_or_archive_files -f -e "$line[1]" fm "${(@)line[2,-1]}"' \ - '*--pattern=[include/exclude paths matching PATTERN]: : _borg_style_selector_or_archive_files -p -f -e "$line[1]" sh "${(@)line[2,-1]}"' \ - $common_create_options \ - '(-s --stats)--json[Output stats as JSON. Implies --stats.]' \ - '--stdin-name=[use NAME in archive for stdin data (default: "stdin")]:NAME' \ - '--content-from-command[interpret PATH as command and store its stdout]' \ - '--exclude-nodump[exclude files flagged NODUMP]' \ - '(-x --one-file-system)'{-x,--one-file-system}'[stay in the same file system]' \ - '--numeric-ids[only store numeric user and group identifiers]' \ - '--atime[do store atime into archive]' \ - '--noctime[do not store ctime into archive]' \ - '--nobirthtime[do not store birthtime (creation date) into archive]' \ - '--noacls[do not read and store ACLs into archive]' \ - '--noxattrs[do not read and store xattrs into archive]' \ - '--noflags[do not read and store flags (e.g. NODUMP, IMMUTABLE) into archive]' \ - '--files-cache=[operate files cache in MODE. default: ctime,size,inode]:MODE:(ctime,size,inode mtime,size,inode ctime,size mtime,size rechunk,ctime rechunk,mtime size disabled)' \ - '--read-special[open and read block and char device files as well as FIFOs as if they were regular files]' \ - $common_options \ - ':ARCHIVE: _borg_archive -a -p' \ - $lastspec && ret=0 - - case $state in - (command) - if (( CURRENT <= 1 )); then - _command_names -e && ret=0 - else - _normal && ret=0 - fi - ;; - esac - - return ret -} - -(( $+functions[_borg-debug] )) || -_borg-debug() { - local -a state line common_options - local curcontext="$curcontext" state_descr - declare -A opt_args - local -i ret=1 - __borg_setup_common_options - - _arguments -s -w -C : \ - $common_options \ - ': :->command' \ - '*:: :->option-or-argument' && ret=0 - - case $state in - (command) - local -a debug_commands=( - 'info:show system infos for debugging / bug reports' - 'dump-archive-items:dump archive items (metadata)' - 'dump-archive:dump decoded archive metadata' - 'dump-manifest:dump decoded repository metadata' - 'dump-repo-objs:dump repo objects' - 'search-repo-objs:search repo objects' - 'get-obj:get object from repository' - 'put-obj:put object to repository' - 'delete-obj:delete object from repository' - 'convert-profile:convert Borg profile to Python profile' - ) - _describe -t commands 'command' debug_commands && ret=0 - ;; - (option-or-argument) - curcontext="${curcontext%:*}-$line[1]:" - - case $line[1] in - (info) - _arguments -s -w -S : \ - $common_options && ret=0 - ;; - (dump-archive-items) - _arguments -s -w -S : \ - $common_options \ - ':ARCHIVE: _borg_archive -a' && ret=0 - ;; - (dump-archive) - _arguments -s -w -S : \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - ':PATH:_files' && ret=0 - ;; - (dump-manifest) - _arguments -s -w -S : \ - $common_options \ - ':PATH:_files' && ret=0 - ;; - (dump-repo-objs) - _arguments -s -w -S : \ - $common_options && ret=0 - ;; - (search-repo-objs) - _arguments -s -w -S : \ - $common_options \ - ':WANTED (hex or string):' && ret=0 - ;; - (get-obj) - _arguments -s -w -S : \ - $common_options \ - ':ID (hex object):' \ - ':PATH:_files' && ret=0 - ;; - (put-obj) - _arguments -s -w -S : \ - $common_options \ - '*:PATH:_files' && ret=0 - ;; - (delete-obj) - _arguments -s -w -S : \ - $common_options \ - '*:ID (hex object):' && ret=0 - ;; - (convert-profile) - _arguments -s -w -S : \ - $common_options \ - ':INPUT:_files' \ - ':OUTPUT:_files' && ret=0 - ;; - (*) - if ! _call_function ret _borg_debug_$line[1]; then - _default && ret=0 - fi - ;; - esac - ;; - esac - - return ret -} - -(( $+functions[_borg-delete] )) || -_borg-delete() { - local -a common_options common_archive_filters_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - $common_archive_filters_options \ - $common_options \ - ':ARCHIVE: _borg_archive' \ - '*:ARCHIVE: _borg_archive' -} - -(( $+functions[_borg-diff] )) || -_borg-diff() { - local -a common_options common_exclude_options - __borg_setup_common_options - __borg_setup_common_exclude_options - - _arguments -s -w -S : \ - '--numeric-ids[only obey numeric user and group identifiers]' \ - '--same-chunker-params[override check of chunker parameters]' \ - '--sort[sort the output lines by file path]' \ - '--json-lines[format output as JSON Lines]' \ - $common_exclude_options \ - $common_options \ - ':ARCHIVE1: _borg_archive -a' \ - ':ARCHIVE2: _borg_archive' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-export-tar] )) || -_borg-export-tar() { - local -a common_options common_exclude_extract_options - __borg_setup_common_options - __borg_setup_common_exclude_extract_options - - _arguments -s -w -S : \ - '--tar-filter[filter program to pipe data through]: :_cmdstring' \ - '--list[output verbose list of items (files, dirs, ...)]' \ - '--tar-format[select tar format: BORG, PAX or GNU]:(BORG PAX GNU)' \ - $common_exclude_extract_options \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - ':FILE:_files' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-extract] )) || -_borg-extract() { - local -a common_options common_exclude_extract_options - __borg_setup_common_options - __borg_setup_common_exclude_extract_options - - _arguments -s -w -S : \ - '--list[output verbose list of items (files, dirs, ...)]' \ - '(-n --dry-run)'{-n,--dry-run}'[do not actually change any files]' \ - '--numeric-ids[only obey numeric user and group identifiers]' \ - '--noacls[do not extract/set ACLs]' \ - '--noxattrs[do not extract/set xattrs]' \ - '--noflags[do not extract/set flags (e.g. NODUMP, IMMUTABLE)]' \ - '--stdout[write all extracted data to stdout]' \ - '--sparse[create holes in output sparse file from all-zero chunks]' \ - $common_exclude_extract_options \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-help] )) || -_borg-help() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - '--epilog-only' \ - '--usage-only' \ - $common_options \ - ':: : _alternative "topics:TOPIC:(patterns placeholders compression)" ": :_borg_commands"' -} - -(( $+functions[_borg-import-tar] )) || -_borg-import-tar() { - local -a common_options common_exclude_extract_options - __borg_setup_common_options - __borg_setup_common_exclude_extract_options - - _arguments -s -w -S : \ - '--tar-filter[filter program to pipe data through]: :_cmdstring' \ - '(-s --stats)'{-s,--stats}'[print statistics for the created archive]' \ - '--list[output verbose list of items (files, dirs, ...)]' \ - '--filter[only display items with the given status characters]: :' \ - '--json[output stats as JSON (implies --stats)]' \ - '--ignore-zeros[ignore zero-filled blocks in the input tarball]' \ - '--comment[add a comment text to the archive]: :' \ - '--timestamp[manually specify the archive creation date/time]: :' \ - '--chunker-params[specify the chunker parameters]: :' \ - '(-C --compression)'{-C,--compression}'=[select compression algorithm]: :_borg_compression' \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - ':FILE:_files' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-info] )) || -_borg-info() { - local -a common_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - '--json[format output as JSON]' \ - $common_archive_filters_options \ - $common_options \ - '::ARCHIVE: _borg_archive' -} - -(( $+functions[_borg-key] )) || -_borg-key() { - local -a state line common_options - local curcontext="$curcontext" state_descr - declare -A opt_args - local -i ret=1 - __borg_setup_common_options - - _arguments -s -w -C : \ - $common_options \ - ': :->command' \ - '*:: :->option-or-argument' && ret=0 - - case $state in - (command) - local -a key_commands=( - 'change-passphrase:Change borg key passphrase' - 'export:Export a backup of the borg key' - 'import:Import a backup of the borg key' - ) - _describe -t commands 'command' key_commands && ret=0 - ;; - (option-or-argument) - curcontext="${curcontext%:*}-$line[1]:" - - case $line[1] in - (change-passphrase) - _arguments -s -w -S : \ - $common_options - ;; - (export) - _arguments -s -w -S : \ - '--paper[create an export suitable for printing and later type-in]' \ - '--qr-html[create an html file suitable for printing and later type-in or qr scan]' \ - $common_options \ - '::PATH:_files' && ret=0 - ;; - (import) - _arguments -s -w -S : \ - '--paper[interactively import from a backup done with --paper]' \ - $common_options \ - '::PATH:_files' && ret=0 - ;; - (*) - if ! _call_function ret _borg_key_$line[1]; then - _default && ret=0 - fi - ;; - esac - ;; - esac - - return ret -} - -(( $+functions[_borg-list] )) || -_borg-list() { - local -a common_options common_exclude_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_exclude_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - '--short[only print file/directory names, nothing else]' \ - '--format=[specify format for file listing]: : _borg_format_keys $line[1]' \ - '--json-lines[Format output as JSON Lines.]' \ - $common_archive_filters_options \ - $common_exclude_options \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-mount] )) || -_borg-mount() { - local -a common_options common_exclude_extract_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_exclude_extract_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - $* \ - '(-f --foreground)'{-f,--foreground}'[stay in foreground, do not daemonize]' \ - '-o[mount options]: :_fuse_values "mount options" - "versions[merged, versioned view of the files in the archives]" - "allow_damaged_files[read damaged files]" - "ignore_permissions[not enforce \"default_permissions\"]"' \ - $common_archive_filters_options \ - $common_exclude_extract_options \ - $common_options \ - ':MOUNTPOINT:_directories' \ - '*: : _borg_style_selector_or_archive_files "$line[1]" pp' -} - -(( $+functions[_borg-prune] )) || -_borg-prune() { - local -a common_options common_match_archives_filter_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_match_archives_filter_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - '*--force[force pruning of corrupted archives, use "--force --force" in case "--force" does not work]' \ - '--list[output verbose list of archives it keeps/prunes]' \ - '--keep-within[keep all archives within this time interval]: : _borg_guard_unsigned_number "INTERVAL"' \ - '(--keep-last --keep-secondly)'{--keep-last,--keep-secondly}'[number of secondly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '--keep-minutely[number of minutely archives to keep]: : _borg_guard_unsigned_number "N"' \ - '(-H --keep-hourly)'{-H,--keep-hourly}'[number of hourly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '(-d --keep-daily)'{-d,--keep-daily}'[number of daily archives to keep]: : _borg_guard_unsigned_number "N"' \ - '(-w --keep-weekly)'{-w,--keep-weekly}'[number of weekly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '(-m --keep-monthly)'{-m,--keep-monthly}'[number of monthly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '--keep-3monthly[number of 3monthly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '--keep-13weekly[number of 13weekly archives to keep]: : _borg_guard_unsigned_number "N"' \ - '(-y --keep-yearly)'{-y,--keep-yearly}'[number of yearly archives to keep]: : _borg_guard_unsigned_number "N"' \ - $common_match_archives_filter_options \ - $common_options -} - -(( $+functions[_borg-recreate] )) || -_borg-recreate() { - local -a common_options common_create_options - __borg_setup_common_options - __borg_setup_common_create_options - - _arguments -s -w -S : \ - $common_create_options \ - '--target=[create a new archive with the name ARCHIVE]:ARCHIVE: _borg_placeholder_or_archive' \ - $common_options \ - ':ARCHIVE: _borg_archive' \ - '*: : _borg_style_selector_or_archive_files -e "$line[1]" pp' -} - -(( $+functions[_borg-rename] )) || -_borg-rename() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options \ - ':ARCHIVE: _borg_archive -a' \ - ':NEWNAME' -} - -(( $+functions[_borg-repo-compress] )) || -_borg-repo-compress() { - local -a common_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - $common_options \ - '(-C --compression)'{-C,--compression}'=[select compression algorithm]: :_borg_compression' -} - -(( $+functions[_borg-repo-create] )) || -_borg-repo-create() { - local -i ret=1 - local -a common_options common_repo_options - __borg_setup_common_options - __borg_setup_common_repo_options - - # special handling for the required optional argument - if (( ! ${words[(I)(-e|--encryption)(|=*)]} )); then - local desc='select encryption key mode' - local -a long=( "--encryption:$desc" ) short=( "-e:$desc" ) remove_chars=( -r '= \t\n\-' ) - _describe -t required-options 'required option' long -S '=' $remove_chars -- short $remove_chars && ret=0 - fi - - _arguments -s -w -S : \ - '(-e --encryption)'{-e,--encryption}'=[select encryption key mode (required)]:MODE:(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)' \ - $common_repo_options \ - '--make-parent-dirs[create parent directories]' -} - -(( $+functions[_borg-repo-delete] )) || -_borg-repo-delete() { - local -a common_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - '--cache-only[delete only the local cache for the given repository]' \ - '*--force[force deletion of corrupted archives, use "--force --force" in case "--force" does not work]' \ - '--keep-security-info[keep the local security info when deleting a repository]' \ - $common_options -} - -(( $+functions[_borg-repo-info] )) || -_borg-repo-info() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - '--json[format output as JSON]' \ - $common_options -} - -(( $+functions[_borg-repo-list] )) || -_borg-repo-list() { - local -a common_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - '--short[only print archive IDs]' \ - '--format=[specify format for archive listing]: : _borg_format_keys $line[1]' \ - '--json[Format output as JSON.]' \ - $common_archive_filters_options \ - $common_options -} - -(( $+functions[_borg-repo-space] )) || -_borg-repo-space() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options \ - '--reserve=[amount of space to reserve in repository]: :_borg_quota_suffixes' \ - '--free[free all reserved space]' -} - -(( $+functions[_borg-serve] )) || -_borg-serve() { - local -a common_options common_repo_options - __borg_setup_common_options - __borg_setup_common_repo_options - - _arguments -s -w -S : \ - $common_repo_options \ - '*--restrict-to-path=[restrict repository access to PATH]:PATH:_files' \ - '*--restrict-to-repository=[restrict repository access]: :_borg_repository' -} - -(( $+functions[_borg-tag] )) || -_borg-tag() { - local -a common_options common_archive_filters_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - '--set=[set tags (can be given multiple times)]:TAG' \ - '--add=[add tags (can be given multiple times)]:TAG' \ - '--remove=[remove tags (can be given multiple times)]:TAG' \ - $common_archive_filters_options \ - $common_options \ - ':ARCHIVE: _borg_archive' \ - '*:ARCHIVE: _borg_archive' -} - -(( $+functions[_borg-transfer] )) || -_borg-transfer() { - local -a common_options common_archive_filters_options common_dry_run_stats_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - __borg_setup_common_dry_run_stats_options - - _arguments -s -w -S : \ - $common_dry_run_stats_options \ - '--other-repo=[transfer archives from the other repository]:SRC_REPOSITORY:_borg_repository' \ - '--from-borg1[other repository is borg 1.x]' \ - '--upgrader=[use the upgrader to convert transferred data]:UPGRADER:(From12To20 NoOp)' \ - '(-C --compression)'{-C,--compression}'=[select compression algorithm]: :_borg_compression' \ - '--recompress=[recompress chunks CONDITION]:WHEN:(always never)' \ - '--chunker-params=[specify the chunker parameters]: :_borg_chunker_params_examples' \ - $common_archive_filters_options \ - $common_options -} - -(( $+functions[_borg-undelete] )) || -_borg-undelete() { - local -a common_options common_archive_filters_options - __borg_setup_common_options - __borg_setup_common_archive_filters_options - - _arguments -s -w -S : \ - '(-n --dry-run)'{-n,--dry-run}'[do not change repository]' \ - '--list[output verbose list of archives]' \ - $common_archive_filters_options \ - $common_options \ - '::ARCHIVE: _borg_archive' -} - -(( $+functions[_borg-umount] )) || -_borg-umount() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options \ - ':MOUNTPOINT:_umountable' -} - -(( $+functions[_borg-version] )) || -_borg-version() { - local -a common_options - __borg_setup_common_options - - _arguments -s -w -S : \ - $common_options -} - -(( $+functions[_borg-with-lock] )) || -_borg-with-lock() { - local -a state line common_options - local curcontext="$curcontext" state_descr - declare -A opt_args - local -i ret=1 - __borg_setup_common_options - - _arguments -s -w -C -S : \ - $common_options \ - '(-):COMMAND: _command_names -e' \ - '(-)*:ARGS:->normal' && ret=0 - - case $state in - (normal) - shift 2 words - (( CURRENT -= 2 )) - _normal && ret=0 - ;; - esac - - return ret -} - -(( $+functions[__borg_setup_common_options] )) || -__borg_setup_common_options() { - typeset -ga common_options=( - '(- :)'{-h,--help}'[show this help message and exit]' - '--critical[work on log level CRITICAL]' - '--error[work on log level ERROR]' - '--warning[work on log level WARNING (default)]' - '(--info -v --verbose)'{--info,-v,--verbose}'[work on log level INFO]' - '--debug[work on log level DEBUG]' - '--debug-topic=[enable TOPIC debugging (can be specified multiple times)]:TOPIC' - '(-p --progress)'{-p,--progress}'[show progress information]' - '--log-json[Output one JSON object per log line instead of formatted text.]' - '--lock-wait=[wait at most SECONDS for acquiring a repository/cache lock (default: 1)]: : _borg_guard_unsigned_number "SECONDS"' - '(- :)--show-version[show/log the borg version]' - '--show-rc[show/log the return code (rc)]' - '--umask=[set umask to M (local only, default: 0077)]:M' - '--remote-path=[set remote path to executable (default: "borg")]: :_cmdstring' - '--upload-ratelimit=[set network upload rate limit in kiByte/s (default: 0=unlimited)]: : _borg_guard_unsigned_number "RATE"' - '--upload-buffer=[set network upload buffer size in MiB. (default: 0=no buffer)]: : _borg_guard_unsigned_number "UPLOAD_BUFFER"' - '--debug-profile=[write execution profile in Borg format into FILE]:FILE:_files' - '--rsh=[use COMMAND instead of ssh]: :_cmdstring' - '(-r --repo)'{-r,--repo}'=[repository]' - ) -} - -(( $+functions[__borg_setup_common_exclude_options] )) || -__borg_setup_common_exclude_options() { - typeset -ga common_exclude_options=( - '*'{-e,--exclude}'=[exclude paths matching PATTERN]: : _borg_style_selector_or_archive_files "$line[1]" fm' - '*--exclude-from=[read exclude patterns from EXCLUDEFILE, one per line]:EXCLUDEFILE:_files' - '*--pattern=[include/exclude paths matching PATTERN]: : _borg_style_selector_or_archive_files -p "$line[1]" sh' - '*--patterns-from=[read include/exclude patterns from PATTERNFILE, one per line]:PATTERNFILE:_files' - ) -} - -(( $+functions[__borg_setup_common_exclude_extract_options] )) || -__borg_setup_common_exclude_extract_options() { - local -a common_exclude_options - __borg_setup_common_exclude_options - typeset -ga common_exclude_extract_options=( - $common_exclude_options - '--strip-components=[Remove the specified number of leading path elements. Paths with fewer elements will be silently skipped.]: : _borg_guard_unsigned_number "NUMBER"' - ) -} - -(( $+functions[__borg_setup_common_match_archives_filter_options] )) || -__borg_setup_common_match_archives_filter_options() { - typeset -ga common_match_archives_filter_options=( - '(-P --prefix)*'{-a,--match-archives}'=[only consider archive names matching the pattern]:PATTERN: _borg_archive -n "${line[1]%%\:\:*}"' - ) -} - -(( $+functions[__borg_setup_common_archive_filters_options] )) || -__borg_setup_common_archive_filters_options() { - local -a common_match_archives_filter_options - __borg_setup_common_match_archives_filter_options - typeset -ga common_archive_filters_options=( - $common_match_archives_filter_options - '--sort-by=[Comma-separated list of sorting keys, default: timestamp]:KEYS:(timestamp archive name id tags host user)' - '(--last)--first=[consider first N archives after other filters were applied]:N: _borg_archive -n "${line[1]%%\:\:*}"' - '(--first)--last=[consider last N archives after other filters were applied]:N: _borg_archive -n "${line[1]%%\:\:*}"' - ) -} - -(( $+functions[__borg_setup_common_dry_run_stats_options] )) || -__borg_setup_common_dry_run_stats_options() { - typeset -ga common_dry_run_stats_options=( - '(-n --dry-run -s --stats)'{-n,--dry-run}'[do not change anything]' - '(-n --dry-run -s --stats)'{-s,--stats}'[print statistics at end]' - # NOTE: actual messages for subcommands differ in details - ) -} - -(( $+functions[__borg_setup_common_create_options] )) || -__borg_setup_common_create_options() { - local -a common_dry_run_stats_options common_exclude_options - __borg_setup_common_dry_run_stats_options - __borg_setup_common_exclude_options - typeset -ga common_create_options=( - $common_dry_run_stats_options - '--list[output verbose list of items (files, dirs, ...)]' - '--filter=[only display items with the given status characters]: :_borg_statuschars' - $common_exclude_options - '--exclude-caches[exclude directories that contain a CACHEDIR.TAG file]' - '*--exclude-if-present=[exclude directories that are tagged by containing a filesystem object with the given NAME]:NAME:_files' - '--keep-exclude-tags[if tag objects are specified with --exclude-if-present, don'\''t omit the tag objects themselves]' - '--comment=[add a comment text to the archive]:COMMENT:_borg_placeholders' - '--timestamp=[manually specify the archive creation date/time]:TIMESTAMP:_borg_timestamp' - '--chunker-params=[specify the chunker parameters]: :_borg_chunker_params_examples' - '(-C --compression)'{-C,--compression}'=[select compression algorithm]: :_borg_compression' - ) -} - -(( $+functions[__borg_setup_common_repo_options] )) || -__borg_setup_common_repo_options() { - local -a common_options - __borg_setup_common_options - typeset -ga common_repo_options=( - $common_options - ) -} - -(( $+functions[_borgfs] )) || -_borgfs() { - _borg-mount '(- :)'{-V,--version}'[show version number and exit]' -} - -(( $+functions[_borg_parameters] )) || -_borg_parameters() { - local name=$1 - shift - local -i ret=1 - local -a expl - - case $name in - (REPO) - local BORG_REPO - unset BORG_REPO - _borg_repository && ret=0 - ;; - ((|NEW_)PASSPHRASE) - _message -e 'passphrase' && ret=0 - ;; - (DISPLAY_PASSPHRASE) - _message -e 'answer to the "display the passphrase for verification" question' && ret=0 - ;; - (HOST_ID) - _message -e 'unique ID' && ret=0 - ;; - (FILES_CACHE_TTL) - _borg_guard_unsigned_number 'time to live (default: 20)' && ret=0 - ;; - (MOUNT_DATA_CACHE_ENTRIES) - _borg_guard_unsigned_number 'number of cached data chunks' && ret=0 - ;; - (PASSCOMMAND|RSH|REMOTE_PATH) - _cmdstring && ret=0 - ;; - (HOSTNAME_IS_UNIQUE|SHOW_SYSINFO|(UNKNOWN_UNENCRYPTED|RELOCATED)_REPO_ACCESS_IS_OK) - _description values expl 'value' - compadd "$expl[@]" yes no && ret=0 - ;; - ((CHECK|DELETE)_I_KNOW_WHAT_I_AM_DOING) - _description values expl 'value' - compadd "$expl[@]" YES NO && ret=0 - ;; - (SELFTEST) - _description values expl 'value' - compadd "$expl[@]" disabled && ret=0 - ;; - (WORKAROUNDS) - _wanted workarounds expl 'workaround' _sequence compadd - basesyncfile && ret=0 - ;; - (KEYS_DIR) - _directories && ret=0 - ;; - (*) - _default && ret=0 - ;; - esac - - return ret -} - -(( $+functions[_borg_repository] )) || -_borg_repository() { - local -a alts opts qopts - zparseopts -E -a opts S: - qopts=( ${(q)opts} ) - [[ -n $BORG_REPO ]] && alts+=( "default-repository: : __borg_default_repository $qopts" ) - alts+=( "cached-repositories:cached repositories:_borg_cached_repositories $qopts" ) - alts+=( 'directories: :_directories -r ":/ \t\n\-"' ) - alts+=( 'remote-repositories: : _borg_remote_repositories' ) - _alternative $alts -} - -(( $+functions[__borg_default_repository] )) || -__borg_default_repository() { - local -a opts suf - zparseopts -E -a opts S: - (( $opts[(I)-S] )) && suf=( -S '' ) - local -a default_repository=( "\:\::$BORG_REPO" ) - _describe -t default-repository 'default repository' default_repository "$suf[@]" -} - -(( $+functions[_borg_cached_repositories] )) || -_borg_cached_repositories() { - local -a cached_repos - local sed_script='/^previous_location = / { - s/// - # no port was given - /ssh:\/\/[^/:]+:[0-9]+/! { - # lstrip the `ssh://` prefix and add a colon before the first slash - s!ssh://([^:/]+)/(.*)!\1:/\2! - } - p - }' - local cachedir=${BORG_CACHE_DIR:-${XDG_CACHE_HOME:-${BORG_BASE_DIR:-$HOME}/.cache}/borg} - cached_repos=( ${(f)"$(_call_program -p cached-repositories sed -n -E ${(q)sed_script} \ - "${(q)cachedir}/*/config(#qN.om)" 2>/dev/null)"} ) - - if [[ $compstate[quote] != (\'|\") ]]; then - # hide ~BORG_REPO and other scalars - local BORG_REPO - unset BORG_REPO sed_script cachedir - cached_repos=( "${(@D)cached_repos}" ) - fi - - compadd -Q "$@" -r ': \t\n\-' -a cached_repos -} - -(( $+functions[_borg_remote_repositories] )) || -_borg_remote_repositories() { - local -a match mbegin mend expl alts - if compset -P '(#b)ssh://[^/]##@[^/]##:([0-9]##)/'; then - _remote_files -/ -- ssh -p $match[1] - return - fi - local -i have_scheme=0 - compset -P 'ssh://' && have_scheme=1 - if compset -P '*:'; then - (( have_scheme )) && alts+=( 'ports: : _borg_guard_unsigned_number "port"' ) - alts+=( 'remote-files:remote file: _remote_files -/ -- ssh' ) - _alternative $alts - elif compset -P 1 '(#b)(*)@'; then - local user=$match[1] - _wanted -C user-at hosts expl "host for $user" \ - _combination -s '[:@]' accounts users-hosts users="$user" hosts -S ':' - - elif compset -S '@*'; then - _wanted users expl "user" \ - _combination -s '[:@]' accounts users-hosts users -q - - else - alts=( - 'users:user:_users -S "@"' - 'hosts:host:_hosts -S ":"' - ) - (( ! have_scheme )) && alts+=( 'prefixes:ssh:compadd -S "" ssh://' ) - _alternative $alts - fi -} - -# _borg_archive [-F] [-n] [qrepo] -# -# -F don't apply archive filter options on the command line -# -n reverse order, disable matchers and don't do menu completion/selection -(( $+functions[_borg_archive] )) || -_borg_archive() { - local -A opts - zparseopts -A opts -D -E F n - - local qrepo=$1 - - if [[ -z $qrepo ]]; then - if [[ -n $BORG_REPO ]]; then - qrepo=${(q)BORG_REPO} - else - _message 'no repository specified' - return 1 - fi - fi - - local -i ret=1 - _tags archives - while _tags; do - - if _requested archives; then - - local -a expl disp archive_filters - local -i reversed_order=1 - - if (( ! $+opts[-F] )); then - local -a archive_filter_options=( -a --match-archives --first --last --sort-by ) tmp - local k - for k in $archive_filter_options; do - if [[ -n $opt_args[$k] ]]; then - IFS=: read -A tmp <<<$opt_args[$k] - archive_filters+=( $k=${^tmp:#} ) - fi - done - fi - - if (( $+opts[-n] )); then - __borg_skip_pattern_matching || return 1 - - disp+=( -U ) - - compstate[insert]='' - compstate[list]='list force' - - reversed_order=0 - fi - - local -a asort - zstyle -a ":completion:${curcontext}:archives" archive-sort asort - if (( $asort[(I)inverse] )); then - (( reversed_order = ! reversed_order )) - fi - local -a sort_by=( --sort-by=${(M)^asort:#(timestamp|name|id)} ) - # NOTE: in case of option repetition, the later one takes precedence - - if zstyle -t ":completion:${curcontext}:archives" verbose; then - typeset -gHa __borg_archive_names=() __borg_archive_descriptions=() - local fmt descfmt name desc - zstyle -s ":completion:${curcontext}:archives" archive-description-format descfmt || - descfmt='{id:.8} {time} {archive:<15} {tags:<10} {username:<10} {hostname:<10} {comment:.40}' - fmt="{archive}{NUL}$descfmt{NUL}" - _call_program -p archive-descriptions \ - ${(q)__borg_command:-borg} repo-list --format=${(q)fmt} ${(q)sort_by} $archive_filters --repo $qrepo 2>/dev/null | - while IFS= read -r -d $'\0' name && IFS= read -r -d $'\0' descr; do - __borg_archive_names[1,0]=( $name ) - __borg_archive_descriptions[1,0]=( "$descr" ) - done - (( $pipestatus[1] )) && { - _message "couldn't list repository: ${(Q)qrepo}" - return 1 - } - (( ! reversed_order )) && - __borg_archive_names=( "${(@aO)__borg_archive_names}" ) && - __borg_archive_descriptions=( "${(@aO)__borg_archive_descriptions}" ) - disp+=( -ld __borg_archive_descriptions ) - else - typeset -gHa __borg_archive_names=() - local fmt='{archive}{NUL}' - __borg_archive_names=( ${(@0aO)"$(_call_program -p archives \ - ${(q)__borg_command:-borg} repo-list --format=${(q)fmt} ${(q)sort_by} $archive_filters --repo $qrepo 2>/dev/null)"} ) - (( $pipestatus[1] )) && { - _message "couldn't list repository: ${(Q)qrepo}" - return 1 - } - (( ! reversed_order )) && - __borg_archive_names=( "${(@aO)__borg_archive_names}" ) - fi - - _all_labels archives expl 'ARCHIVE' compadd "$disp[@]" -a __borg_archive_names && ret=0 - - fi - - (( ret )) || return 0 - - done - - return 1 -} - - -(( $+functions[__borg_expand_path] )) || -__borg_expand_path() { - local _path=$1 - local -a match mbegin mend - if [[ $_path == (#b)(\~[^/]#)(|/*) ]]; then - local etilde - etilde=$~match[1] 2>/dev/null - _path="$etilde$match[2]" - fi - _path=${(e)_path//\\\\/\\\\\\\\} - eval typeset -g ${2:-REPLY}=\$_path -} - -(( $+functions[_borg_placeholder_or_archive] )) || -_borg_placeholder_or_archive() { - local qrepo=$1 - shift - _alternative \ - 'placeholders: :_borg_placeholders' \ - "archives: : _borg_archive ${(q)qrepo}" -} - -(( $+functions[_borg_placeholders] )) || -_borg_placeholders() { - local -a placeholders=( - 'hostname:The (short) hostname of the machine.' - 'fqdn:The full name of the machine.' - 'reverse-fqdn:The full name of the machine in reverse domain name notation.' - 'now:The current local date and time, by default in ISO-8601 format. You can also supply your own format string, e.g. {now:%Y-%m-%d_%H:%M:%S}' - 'utcnow:The current UTC date and time, by default in ISO-8601 format. You can also supply your own format string, e.g. {utcnow:%Y-%m-%d_%H:%M:%S}' - 'user:The user name (or UID, if no name is available) of the user running borg.' - 'pid:The current process ID.' - 'borgversion:The version of borg, e.g.: 1.0.8rc1' - 'borgmajor:The version of borg, only the major version, e.g.: 1' - 'borgminor:The version of borg, only major and minor version, e.g.: 1.0' - 'borgpatch:The version of borg, only major, minor and patch version, e.g.: 1.0.8' - ) - __borg_complete_keys _describe -t placeholders 'placeholder' placeholders '"$copts[@]"' -} - -(( $+functions[_borg_format_keys] )) || -_borg_format_keys() { - local archive=${(Q)1} - - local -a keys=( NEWLINE NL NUL SPACE TAB CR LF ) - local -a repository_keys=( archive name comment bcomment id start time end command_line hostname username ) - local -a archive_keys=( type mode uid gid user group path bpath source linktarget flags size csize dsize dcsize - num_chunks unique_chunks mtime ctime atime isomtime isoctime isoatime blake2b blake2s md5 sha1 sha224 sha256 sha384 - sha3_224 sha3_256 sha3_384 sha3_512 sha512 shake_128 shake_256 archiveid archivename extra health ) - - local akeys rkeys - akeys='archive-keys:archive keys:compadd -a archive_keys' - rkeys='repository-keys:repository keys:compadd -a repository_keys' - local -a alts=( 'keys:keys:compadd -a keys' ) - # If an archive is specified, show archive keys, otherwise show both repository and archive keys - if [[ -n $archive ]]; then - alts+=( $akeys ) - else - alts+=( $rkeys $akeys ) - fi - - __borg_complete_keys _alternative -O copts ${(q)alts} -} - -(( $+functions[__borg_complete_keys] )) || -__borg_complete_keys() { - compset -P '*[^A-Za-z]##' - compset -S '[^A-Za-z]##*' - - [[ -n $ISUFFIX ]] && compstate[to_end]='' - # NOTE: `[[ -n $ISUFFIX ]]` is a workaround for a bug that causes cursor movement to the right further than it should - # NOTE: the _oldlist completer doesn't respect compstate[to_end]='' - - local ipref suf - if [[ $IPREFIX[-1] != '{' ]]; then - ipref='{' - [[ $compstate[quote] != (\'|\") ]] && ipref='\{' - fi - if [[ $ISUFFIX[1] != (|\\)\} ]]; then - suf='}' - [[ $compstate[quote] != (\'|\") ]] && suf='\}' - fi - - local -a copts=( -i "$ipref" -S "$suf" ) - eval "$@" -} - -# _borg_style_selector_or_archive_files [-e] [-p] archive default_style_selector -# -# -e apply exclusion options on the command line -# -p complete `--pattern` -# -f complete files rather than borg paths -(( $+functions[_borg_style_selector_or_archive_files] )) || -_borg_style_selector_or_archive_files() { - local -A opts - zparseopts -A opts -D -E e p f - - local arch=$1 default_style_selector=$2 - shift 2 - - local -a match mbegin mend expl tags=( style-selectors archive-files ) ss_suf=( -S ':' -r ':' ) - (( $+opts[-f] )) && tags=( style-selectors files ) - local -i ret=1 - - if (( $+opts[-p] )); then - if ! compset -P '(#b)([RP\+\-\!])'; then - local -a pattern_rules=( - 'P:pattern style' - 'R:root path' - '+:include' - '-:exclude' - '!:exclude non-recurse' - ) - _describe -t pattern-rules 'pattern rule' pattern_rules -S '' - return - else - if [[ $compstate[quote] == (\'|\") ]]; then - compset -P ' #' - else - compset -P '(\\ )#' - fi - if [[ $match[1] == 'R' ]]; then - default_style_selector='pp' - elif [[ $match[1] == 'P' ]]; then - tags=( style-selectors ) - ss_suf=() - fi - fi - fi - - _tags $tags - while _tags; do - if _requested style-selectors; then - _all_labels style-selectors expl 'style selector' \ - __borg_style_selectors $default_style_selector "$ss_suf[@]" - && ret=0 - fi - if _requested archive-files; then - _all_labels archive-files expl 'PATTERN' \ - __borg_archive_files ${(k)opts} "$arch" $default_style_selector - && ret=0 - fi - if _requested files; then - local -a borg_paths=( ${(Q)${(e)${~@}}} ) - _all_labels files expl 'PATH' \ - __borg_pattern_files ${(k)opts} borg_paths - && ret=0 - fi - (( ret )) || return 0 - done - - return 1 -} - -(( $+functions[__borg_style_selectors] )) || -__borg_style_selectors() { - local default_style_selector=$1 path_style_selector - shift - zstyle -s ":completion:${curcontext}:archive-files" path-style-selector path_style_selector || - path_style_selector='fm' - local -a disp - local -A style_selectors - __borg_setup_style_selectors - if zstyle -T ":completion:${curcontext}:style-selectors" verbose; then - local -a style_selector_descriptions extra - local k v sep - for k v in ${(kv)style_selectors}; do - extra=() - [[ $k == $default_style_selector ]] && extra+=( 'default' ) - [[ $k == $path_style_selector ]] && __borg_choose_path_or_pattern "" "$default_style_selector" && - extra+=( 'path' ) - (( $#extra )) && v+=" (${(j:, :)extra})" - style_selector_descriptions+=( "${${k//\\/\\\\}//:/\\:}:$v" ) - done - zstyle -s ":completion:${curcontext}:style-selectors" list-separator sep || sep=-- - zformat -a style_selector_descriptions " $sep " $style_selector_descriptions - disp=( -ld style_selector_descriptions ) - fi - compadd "$disp[@]" "$@" -k style_selectors -} - -(( $+functions[__borg_archive_files] )) || -__borg_archive_files() { - local -A opts - zparseopts -A opts -D e p - - local arch=$1 default_style_selector=$2 - shift 2 - - if [[ -z $arch ]]; then - _message 'no archive specified' - return 1 - fi - - local -a qargs tmp disp pref match mbegin mend archive_files descs - local -A style_selectors - local k cword fmt descfmt style_selector path_style_selector name descr - - # take into account exclude options on the command line - if (( $+opts[-e] )); then - local -a exclude_options=( -e --exclude --exclude-from --pattern --pattern-from ) - local -a excludes - for k in $exclude_options; do - if [[ -n $opt_args[$k] ]]; then - IFS=: read -A tmp <<<$opt_args[$k] - excludes+=( $k="${^tmp[@]}" ) - fi - done - [[ -n $excludes ]] && qargs+=( "$excludes[@]" ) - fi - - (( $_matcher_num > 1 )) && return 1 - __borg_skip_pattern_matching || return 1 - - cword="$PREFIX$SUFFIX" - [[ $compstate[quote] != (\'|\") ]] && cword=${(Q)cword} - - [[ -z $cword ]] && return 1 - - if zstyle -t ":completion:${curcontext}:archive-files" verbose; then - zstyle -s ":completion:${curcontext}:archive-files" file-description-format descfmt || - descfmt='{mode} {user:6} {group:6} {size:8d} {mtime} {path}{extra}' - fmt="{bpath}{NUL}$descfmt{NUL}" - else - fmt='{bpath}{NUL}' - fi - qargs+=( --format=${(q)fmt} ) - - qargs+=( $arch ) - - __borg_setup_style_selectors - [[ $cword == (#b)(${~${(j:|:)${(kb)style_selectors}}}):* ]] && style_selector=$match[1] - - local -i path_expected=0 - __borg_choose_path_or_pattern "$style_selector" $default_style_selector $cword && path_expected=1 - - if [[ -n $cword ]]; then - if (( path_expected )); then - [[ -n $style_selector ]] && compset -P "$style_selector:" && pref=( -P "$style_selector:" ) - cword="$PREFIX$SUFFIX" - [[ $compstate[quote] != (\'|\") ]] && cword=${(Q)cword} - zstyle -s ":completion:${curcontext}:archive-files" path-style-selector path_style_selector || - path_style_selector='fm' - cword="$path_style_selector:$cword" - else - [[ -z $style_selector ]] && cword="$default_style_selector:$cword" - fi - qargs+=( ${(q)cword} ) - fi - - if zstyle -t ":completion:${curcontext}:archive-files" verbose; then - _call_program -p archive-file-descriptions ${(q)__borg_command:-borg} list $qargs 2>/dev/null | - while IFS= read -r -d $'\0' name && IFS= read -r -d $'\0' descr; do - archive_files+=( $name ) - descs+=( $descr ) - done - (( $pipestatus[1] )) && { _message "couldn't list archive: ${(Q)arch}"; return 1 } - disp=( -ld descs ) - else - archive_files=( ${(0)"$(_call_program -p archive-files ${(q)__borg_command:-borg} list $qargs 2>/dev/null)"} ) - (( $pipestatus[1] )) && { _message "couldn't list archive: ${(Q)arch}"; return 1 } - fi - - if (( $#archive_files )); then - if (( path_expected )); then - compstate[insert]='automenu' - else - compstate[insert]='' - compstate[list]='list force' - fi - fi - - compadd "$pref[@]" -U "$disp[@]" "$@" -a archive_files -} - -(( $+functions[__borg_choose_path_or_pattern] )) || -__borg_choose_path_or_pattern() { - local ss=$1 defss=$2 cword=$3 - shift 2 - [[ $ss == (pp|pf) || ( -z $ss && $defss == (pp|pf) ) ]] -} - -# transform borg exclude patterns into zsh ignore patterns and then complete files -(( $+functions[__borg_pattern_files] )) || -__borg_pattern_files() { - local -A opts - zparseopts -A opts -D -E e p f - - local paths_varname=$1 - shift - - local -a args - local -A style_selectors - __borg_setup_style_selectors - local pr_pat='[RP\+\-\!]' ss_pat="(${(j:|:)${(@kb)style_selectors}}):" - local prs_pat="$pr_pat #" - - if (( $+opts[-e] )); then - local -a borg_excludes exclude_options=( -e --exclude --pattern ) tmp - local k cword - local -i i - for k in $exclude_options; do - if [[ -n $opt_args[$k] ]]; then - IFS=: read -A tmp <<<$opt_args[$k] - tmp=( ${(Q)tmp} ) - # lstrip style selectors and pattern rules - [[ $+opts[-p] -gt 0 || $k == --pattern ]] && tmp=( ${tmp#$~prs_pat} ) - tmp=( ${tmp#$~ss_pat} ) - - # don't take into account the word under the cursor - cword="$PREFIX$SUFFIX" - [[ $compstate[quote] != (\'|\") ]] && cword=${(Q)cword} - [[ $+opts[-p] -gt 0 || $k == --pattern ]] && cword=${cword#$~prs_pat} - cword=${cword#$~ss_pat} - i=$tmp[(I)$cword] - (( i )) && tmp=( "${(@)tmp[1,i-1]}" "${(@)tmp[i+1,-1]}" ) - - borg_excludes+=( "$tmp[@]" ) - fi - done - [[ -n $borg_excludes ]] && args+=( -F borg_excludes ) - fi - - [[ -n ${(P)paths_varname} ]] && args+=( -W $paths_varname ) - - args+=( "$@" ) - - # lstrip style selectors and pattern rules - if (( $+opts[-p] )); then - if [[ $compstate[quote] != (\'|\") ]]; then - compset -P $pr_pat - compset -P '(\\ )#' - else - compset -P $prs_pat - fi - fi - compset -P $ss_pat - - compstate[insert]='' - compstate[list]='list force' - - _path_files "$args[@]" -} - -(( $+functions[__borg_setup_style_selectors] )) || -__borg_setup_style_selectors() { - typeset -gA style_selectors=( - fm 'Fnmatch' - sh 'Shell-style patterns' - re 'Regular expressions' - pp 'Path prefix' - pf 'Path full-match' - ) -} - -(( $+functions[__borg_skip_pattern_matching] )) || -__borg_skip_pattern_matching() { - # unset glob_complete - [[ $compstate[pattern_match] == '*' ]] && compstate[pattern_match]='' - # skip the _match completer - [[ -n $compstate[pattern_match] ]] && return 1 - return 0 -} - -# A simple prefix-oriented completion function for compressors. Can be improved by supporting the suffix. -(( $+functions[_borg_compression] )) || -_borg_compression() { - local -a nolvl=( - 'none:do not compress' - 'lz4:very high speed, very low compression' - ) - local -a havelvl=( - 'zstd:("zstandard")' - 'zlib:("gz") medium speed, medium compression' - 'lzma:("xz") low speed, high compression' - ) - local -a auto=( - 'auto:compress compressible, otherwise "none"' - ) - local -a match mbegin mend - # NOTE: Zsh's `-prefix` condition is confused by the leading parenthesis in the pattern. - # Fortunately, we simply need to show a message. - if compset -P '(#b)(|auto,)(zstd|zlib|lzma),'; then - local -i from to def - case $match[2] in - (zstd) from=1 to=22 def=3 ;; - (zlib|lzma) from=0 to=9 def=6 ;; - esac - _message -e "compression level (from $from to $to, default: $def)" - elif compset -P 'auto,'; then - _describe -t compression 'compression' nolvl -- havelvl -qS, - else - _describe -t compression 'compression' nolvl -- havelvl -qS, -- auto -S, - fi -} - -(( $+functions[_borg_chunker_params] )) || -_borg_chunker_params() { - if compset -P 'buzhash,'; then - if compset -P '*,*,*,'; then - _message -e 'HASH_WINDOW_SIZE' - elif compset -P '*,*,'; then - _message -e 'HASH_MASK_BITS (statistical medium chunk size ~= 2^HASH_MASK_BITS B)' - elif compset -P '*,'; then - _message -e 'CHUNK_MAX_EXP (maximum chunk size = 2^CHUNK_MAX_EXP B)' - else - _message -e 'CHUNK_MIN_EXP (minimum chunk size = 2^CHUNK_MIN_EXP B)' - fi - elif compset -P 'fixed,'; then - if compset -P '*,'; then - _message -e 'HEADER_SIZE (B)' - else - _message -e 'BLOCK_SIZE (B)' - fi - else - local -a algorithms=( - 'fixed:a simple, low cpu overhead, fixed blocksize chunker, optionally supporting a header block of different size' - 'buzhash:variable, content-defined blocksize, uses a rolling hash computed by the Buzhash algorithm' - ) - _describe -t algorithm 'ALGO' algorithms -S , - fi -} - -(( $+functions[_borg_chunker_params_examples] )) || -_borg_chunker_params_examples() { - local -a params=( - 'default:buzhash,19,23,21,4095' - 'buzhash,19,23,21,4095:small amount of chunks (default)' - 'buzhash,10,23,16,4095:big amount of chunks' - ) - params=( ${(q)params} ) - _alternative \ - 'chunker-params: :_borg_chunker_params' \ - "chunker-params-examples:chunker params examples:(($params))" -} - -(( $+functions[_borg_statuschars] )) || -_borg_statuschars() { - _values -s '' 'STATUSCHARS' \ - 'A[regular file, added]' \ - 'M[regular file, modified]' \ - 'U[regular file, unchanged]' \ - 'C[regular file, it changed while we backed it up]' \ - 'E[regular file, an error happened while accessing/reading this file]' \ - 'd[directory]' \ - 'b[block device]' \ - 'c[char device]' \ - 'h[regular file, hardlink (to already seen inodes)]' \ - 's[symlink]' \ - 'f[fifo]' \ - 'i[backup data was read from standard input (stdin)]' \ - '-[excluded]' \ - '+[included]' \ - '?[missing status code]' -} - -(( $+functions[_borg_quota_suffixes] )) || -_borg_quota_suffixes() { - if compset -P '[0-9]##'; then - local -a suffixes=( - 'K:10 ** 3 bytes' - 'M:10 ** 6 bytes' - 'G:10 ** 9 bytes' - 'T:10 ** 12 bytes' - 'P:10 ** 15 bytes' - ) - # NOTE: tag `suffixes` is already in use (file extensions) - _describe -t multiplier 'suffix' suffixes - else - _message -e 'QUOTA' - fi -} - -(( $+functions[_borg_timestamp] )) || -_borg_timestamp() { - _alternative \ - "dates:TIMESTAMP: _dates -f '%FT%T'" \ - 'files:reference:_files' -} - -(( $+functions[_borg_guard_unsigned_number] )) || -_borg_guard_unsigned_number() { - local -A opts - zparseopts -K -D -A opts M+: J+: V+: 1 2 o+: n F: x+: X+: - _guard '[0-9]#' ${1:-number} -} - -_borg() { - local -a match mbegin mend line state - local curcontext="$curcontext" state_descr - typeset -A opt_args - local -i ret=1 - - if [[ $service == 'borg' ]]; then - local __borg_command=$words[1] - - local -a common_options - __borg_setup_common_options - - _arguments -s -w -C : \ - '(- :)'{-V,--version}'[show version number and exit]' \ - $common_options \ - '(-): :->command' \ - '(-)*:: :->option-or-argument' && return - - case $state in - (command) - _borg_commands && ret=0 - ;; - (option-or-argument) - curcontext="${curcontext%:*:*}:borg-$words[1]:" - - if ! _call_function ret _borg-$words[1]; then - _default && ret=0 - fi - ;; - esac - elif [[ $service == (#b)-value-,BORG_(*),-default- ]]; then - _borg_parameters $match[1] && ret=0 - elif ! _call_function ret _$service; then - _default && ret=0 - fi - - return ret -} - -_borg "$@" diff --git a/src/borg/testsuite/archiver/completion_cmd_test.py b/src/borg/testsuite/archiver/completion_cmd_test.py index 21d308d02..ffa091199 100644 --- a/src/borg/testsuite/archiver/completion_cmd_test.py +++ b/src/borg/testsuite/archiver/completion_cmd_test.py @@ -1,21 +1,184 @@ -from . import cmd, generate_archiver_tests +import functools +import os +import subprocess +import tempfile + +import pytest + +from . import cmd, generate_archiver_tests, RK_ENCRYPTION pytest_generate_tests = lambda metafunc: generate_archiver_tests(metafunc, kinds="local") # NOQA -def test_bash_completion(archivers, request): - """Ensure the generated Bash completion includes our helper.""" +@functools.lru_cache +def cmd_available(cmd): + """Check if a shell command is available.""" + try: + subprocess.run(cmd.split(), capture_output=True, check=True) + return True + except (subprocess.SubprocessError, FileNotFoundError): + return False + + +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") + + +def _run_bash_completion_fn(completion_script, setup_code): + """Source the completion script in bash and run setup_code, return subprocess result.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=".bash", delete=False) as f: + f.write(completion_script) + script_path = f.name + try: + result = subprocess.run( + ["bash", "-c", f"source {script_path}\n{setup_code}"], capture_output=True, text=True, timeout=120 + ) + finally: + os.unlink(script_path) + return result + + +# -- output sanity checks ----------------------------------------------------- + + +def test_bash_completion_nontrivial(archivers, request): + """Verify the generated Bash completion is non-trivially sized.""" archiver = request.getfixturevalue(archivers) output = cmd(archiver, "completion", "bash") - assert "_borg_complete_archive() {" in output - assert "_borg_complete_sortby() {" in output - assert "_borg_complete_filescachemode() {" in output + assert len(output) > 5000, f"Bash completion suspiciously small: {len(output)} chars" + assert output.count("\n") > 100, f"Bash completion suspiciously few lines: {output.count(chr(10))}" -def test_zsh_completion(archivers, request): - """Ensure the generated Zsh completion includes our helper.""" +def test_zsh_completion_nontrivial(archivers, request): + """Verify the generated Zsh completion is non-trivially sized.""" archiver = request.getfixturevalue(archivers) output = cmd(archiver, "completion", "zsh") - assert "_borg_complete_archive() {" in output - assert "_borg_complete_sortby() {" in output - assert "_borg_complete_filescachemode() {" in output + assert len(output) > 5000, f"Zsh completion suspiciously small: {len(output)} chars" + assert output.count("\n") > 100, f"Zsh completion suspiciously few lines: {output.count(chr(10))}" + + +# -- syntax validation -------------------------------------------------------- + + +def _check_shell_syntax(script_content, shell, suffix): + """Write script_content to a temp file and verify syntax with ``shell -n``.""" + with tempfile.NamedTemporaryFile(mode="w", suffix=suffix, delete=False) as f: + f.write(script_content) + script_path = f.name + try: + result = subprocess.run([shell, "-n", script_path], capture_output=True) + finally: + os.unlink(script_path) + return result + + +@needs_bash +def test_bash_completion_syntax(archivers, request): + """Verify the generated Bash completion script has valid syntax.""" + archiver = request.getfixturevalue(archivers) + output = cmd(archiver, "completion", "bash") + result = _check_shell_syntax(output, "bash", ".bash") + assert result.returncode == 0, f"Generated Bash completion has syntax errors: {result.stderr.decode()}" + + +@needs_zsh +def test_zsh_completion_syntax(archivers, request): + """Verify the generated Zsh completion script has valid syntax.""" + archiver = request.getfixturevalue(archivers) + output = cmd(archiver, "completion", "zsh") + result = _check_shell_syntax(output, "zsh", ".zsh") + assert result.returncode == 0, f"Generated Zsh completion has syntax errors: {result.stderr.decode()}" + + +# -- borg-specific preamble function behavior (bash) -------------------------- + + +@needs_bash +def test_bash_sortby_dedup(archivers, request): + """_borg_complete_sortby should not re-offer already-selected sort keys.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "bash") + + # Simulate: user typed "borg repo-list --sort-by timestamp," + # The function should offer remaining keys but NOT "timestamp" again. + result = _run_bash_completion_fn( + script, 'COMP_WORDS=(borg repo-list --sort-by "timestamp,")\n' "COMP_CWORD=3\n" "_borg_complete_sortby\n" + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + lines = [line for line in result.stdout.strip().splitlines() if line.strip()] + # "timestamp" must not appear as a standalone completion candidate + bare_keys = [line.rsplit(",", 1)[-1] for line in lines] + assert "timestamp" not in bare_keys, f"timestamp was re-offered: {lines}" + # Other keys like "archive" should be offered + assert any("archive" in line for line in lines), f"expected 'archive' in completions: {lines}" + + +@needs_bash +def test_bash_filescachemode_exclusivity(archivers, request): + """_borg_complete_filescachemode should enforce ctime/mtime and disabled mutual exclusion.""" + archiver = request.getfixturevalue(archivers) + script = cmd(archiver, "completion", "bash") + + # After selecting "ctime,", mtime should not be offered + result = _run_bash_completion_fn( + script, 'COMP_WORDS=(borg create --files-cache "ctime,")\n' "COMP_CWORD=3\n" "_borg_complete_filescachemode\n" + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + bare_keys = [line.rsplit(",", 1)[-1] for line in result.stdout.strip().splitlines() if line.strip()] + assert "mtime" not in bare_keys, f"mtime offered after ctime: {bare_keys}" + assert "disabled" not in bare_keys, f"disabled offered after ctime: {bare_keys}" + + # After selecting "disabled,", nothing should be offered + result2 = _run_bash_completion_fn( + script, + 'COMP_WORDS=(borg create --files-cache "disabled,")\n' "COMP_CWORD=3\n" "_borg_complete_filescachemode\n", + ) + assert result2.returncode == 0 + assert result2.stdout.strip() == "", f"completions offered after disabled: {result2.stdout}" + + # After selecting "size,", disabled should not be offered + result3 = _run_bash_completion_fn( + script, 'COMP_WORDS=(borg create --files-cache "size,")\n' "COMP_CWORD=3\n" "_borg_complete_filescachemode\n" + ) + assert result3.returncode == 0 + bare_keys3 = [line.rsplit(",", 1)[-1] for line in result3.stdout.strip().splitlines() if line.strip()] + assert "disabled" not in bare_keys3, f"disabled offered after size: {bare_keys3}" + + +@needs_bash +def test_bash_archive_name_completion(archivers, request): + """_borg_complete_archive should complete archive names 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", "bash") + repo = archiver.repository_path + + result = _run_bash_completion_fn( + script, f'COMP_WORDS=(borg delete --repo "{repo}" "mybackup")\n' f"COMP_CWORD=4\n" f"_borg_complete_archive\n" + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + assert "mybackup-2024" in result.stdout, f"archive name missing: {result.stdout}" + assert "mybackup-2025" in result.stdout, f"archive name missing: {result.stdout}" + + +@needs_bash +def test_bash_archive_aid_completion(archivers, request): + """_borg_complete_archive should complete aid: prefixed archive IDs.""" + archiver = request.getfixturevalue(archivers) + cmd(archiver, "repo-create", RK_ENCRYPTION) + cmd(archiver, "create", "testarchive", archiver.input_path) + + script = cmd(archiver, "completion", "bash") + repo = archiver.repository_path + + result = _run_bash_completion_fn( + script, f'COMP_WORDS=(borg info --repo "{repo}" "aid:")\n' f"COMP_CWORD=4\n" f"_borg_complete_archive\n" + ) + assert result.returncode == 0, f"stderr: {result.stderr}" + lines = [line for line in result.stdout.strip().splitlines() if line.strip()] + 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}" diff --git a/src/borg/testsuite/shell_completions_test.py b/src/borg/testsuite/shell_completions_test.py index 24c5d426f..4fa26faa5 100644 --- a/src/borg/testsuite/shell_completions_test.py +++ b/src/borg/testsuite/shell_completions_test.py @@ -6,22 +6,6 @@ import pytest SHELL_COMPLETIONS_DIR = Path(__file__).parent / ".." / ".." / ".." / "scripts" / "shell_completions" -def test_bash_completion_is_valid(): - """Test that the Bash completion file is valid Bash syntax.""" - bash_completion_file = SHELL_COMPLETIONS_DIR / "bash" / "borg" - assert bash_completion_file.is_file() - - # Check if Bash is available - try: - subprocess.run(["bash", "--version"], capture_output=True, check=True) - except (subprocess.SubprocessError, FileNotFoundError): - pytest.skip("Bash not available") - - # Test whether the Bash completion file can be sourced without errors - result = subprocess.run(["bash", "-n", str(bash_completion_file)], capture_output=True) - assert result.returncode == 0, f"Bash completion file has syntax errors: {result.stderr.decode()}" - - 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" @@ -36,19 +20,3 @@ def test_fish_completion_is_valid(): # 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()}" - - -def test_zsh_completion_is_valid(): - """Test that the Zsh completion file is valid Zsh syntax.""" - zsh_completion_file = SHELL_COMPLETIONS_DIR / "zsh" / "_borg" - assert zsh_completion_file.is_file() - - # Check if Zsh is available - try: - subprocess.run(["zsh", "--version"], capture_output=True, check=True) - except (subprocess.SubprocessError, FileNotFoundError): - pytest.skip("Zsh not available") - - # Test whether the Zsh completion file can be sourced without errors - result = subprocess.run(["zsh", "-n", str(zsh_completion_file)], capture_output=True) - assert result.returncode == 0, f"Zsh completion file has syntax errors: {result.stderr.decode()}"