From 6863fe2d357ae8dfa4f7e92098c872e67da448aa Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 18 May 2022 15:21:31 +0200 Subject: [PATCH 1/4] add completion command to generate shell completion scripts Use the cobra lib to automatically generate shell completion scripts. We can use the completion command which is automatically added, since it is not importent for most users we hide it. To test the new script on bash you can use `source <(bin/skopeo completion bash)` Signed-off-by: Paul Holzinger --- cmd/skopeo/main.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/skopeo/main.go b/cmd/skopeo/main.go index a6714ce2..3f8a9621 100644 --- a/cmd/skopeo/main.go +++ b/cmd/skopeo/main.go @@ -63,11 +63,8 @@ func createApp() (*cobra.Command, *globalOptions) { }, SilenceUsage: true, SilenceErrors: true, - // Currently, skopeo uses manually written completions. Cobra allows - // for auto-generating completions for various shells. Podman is - // already making us of that. If Skopeo decides to follow, please - // remove the line below (and hide the `completion` command). - CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, + // Hide the completion command which is provided by cobra + CompletionOptions: cobra.CompletionOptions{HiddenDefaultCmd: true}, // This is documented to parse "local" (non-PersistentFlags) flags of parent commands before // running subcommands and handling their options. We don't really run into such cases, // because all of our flags on rootCommand are in PersistentFlags, except for the deprecated --tls-verify; From 9bed0a9e9a77486160d80993444a17caa3ad8d79 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 18 May 2022 15:34:13 +0200 Subject: [PATCH 2/4] shell completion: add Makefile target Add target to generate the shell scripts and a target to install them. Signed-off-by: Paul Holzinger --- .gitignore | 2 +- Makefile | 26 ++- completions/bash/skopeo | 343 ---------------------------------------- 3 files changed, 22 insertions(+), 349 deletions(-) delete mode 100644 completions/bash/skopeo diff --git a/.gitignore b/.gitignore index bb25ec55..13f702b2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ /layers-* /skopeo result - +/completions/ # ignore JetBrains IDEs (GoLand) config folder .idea diff --git a/Makefile b/Makefile index d899c78a..5605edb0 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,10 @@ REGISTRIESDDIR ?= ${CONTAINERSCONFDIR}/registries.d SIGSTOREDIR ?= /var/lib/containers/sigstore BINDIR ?= ${PREFIX}/bin MANDIR ?= ${PREFIX}/share/man -BASHCOMPLETIONSDIR ?= ${PREFIX}/share/bash-completion/completions + +BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions +ZSHINSTALLDIR=${PREFIX}/share/zsh/site-functions +FISHINSTALLDIR=${PREFIX}/share/fish/vendor_completions.d GO ?= go GOBIN := $(shell $(GO) env GOBIN) @@ -156,8 +159,16 @@ docs: $(MANPAGES) docs-in-container: ${CONTAINER_RUN} $(MAKE) docs $(if $(DEBUG),DEBUG=$(DEBUG)) +.PHONY: completions +completions: bin/skopeo + install -d -m 755 completions/{bash,zsh,fish,powershell} + ./bin/skopeo completion bash >| completions/bash/skopeo + ./bin/skopeo completion zsh >| completions/zsh/_skopeo + ./bin/skopeo completion fish >| completions/fish/skopeo.fish + ./bin/skopeo completion powershell >| completions/powershell/skopeo.ps1 + clean: - rm -rf bin docs/*.1 + rm -rf bin docs/*.1 completions/ install: install-binary install-docs install-completions install -d -m 755 ${DESTDIR}${SIGSTOREDIR} @@ -176,9 +187,14 @@ ifneq ($(DISABLE_DOCS), 1) install -m 644 docs/*.1 ${DESTDIR}${MANDIR}/man1 endif -install-completions: - install -m 755 -d ${DESTDIR}${BASHCOMPLETIONSDIR} - install -m 644 completions/bash/skopeo ${DESTDIR}${BASHCOMPLETIONSDIR}/skopeo +install-completions: completions + install -d -m 755 ${DESTDIR}${BASHINSTALLDIR} + install -m 644 completions/bash/skopeo ${DESTDIR}${BASHINSTALLDIR} + install -d -m 755 ${DESTDIR}${ZSHINSTALLDIR} + install -m 644 completions/zsh/_skopeo ${DESTDIR}${ZSHINSTALLDIR} + install -d -m 755 ${DESTDIR}${FISHINSTALLDIR} + install -m 644 completions/fish/skopeo.fish ${DESTDIR}${FISHINSTALLDIR} + # There is no common location for powershell files so do not install them. Users have to source the file from their powershell profile. shell: $(CONTAINER_RUN) bash diff --git a/completions/bash/skopeo b/completions/bash/skopeo deleted file mode 100644 index 2b0ab634..00000000 --- a/completions/bash/skopeo +++ /dev/null @@ -1,343 +0,0 @@ -#! /bin/bash - -_complete_() { - local options_with_args=$1 - local boolean_options="$2 -h --help" - local transports=$3 - - local option_with_args - for option_with_args in $options_with_args $transports - do - if [ "$option_with_args" == "$prev" ] || [ "$option_with_args" == "$cur" ] - then - return - fi - done - - case "$cur" in - -*) - while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "$boolean_options $options_with_args" -- "$cur") - ;; - *) - if [ -n "$transports" ] - then - compopt -o nospace - while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "$transports" -- "$cur") - fi - ;; - esac -} - -_skopeo_supported_transports() { - local subcommand=$1 - - skopeo "$subcommand" --help | grep "Supported transports" -A 1 | tail -n 1 | sed -e 's/,/:/g' -e 's/$/:/' -} - -_skopeo_copy() { - local options_with_args=" - --authfile - --src-authfile - --dest-authfile - --format -f - --multi-arch - --sign-by - --sign-passphrase-file - --sign-identity - --src-creds --screds - --src-cert-dir - --src-tls-verify - --dest-creds --dcreds - --dest-cert-dir - --dest-tls-verify - --src-daemon-host - --dest-daemon-host - --src-registry-token - --dest-registry-token - --src-username - --src-password - --dest-username - --dest-password - " - - local boolean_options=" - --all - --dest-compress - --dest-decompress - --remove-signatures - --src-no-creds - --dest-no-creds - --dest-oci-accept-uncompressed-layers - --dest-precompute-digests - --preserve-digests - " - - local transports - transports=" - $(_skopeo_supported_transports "${FUNCNAME//"_skopeo_"/}") - " - - _complete_ "$options_with_args" "$boolean_options" "$transports" -} - -_skopeo_sync() { - local options_with_args=" - --authfile - --dest - --dest-authfile - --dest-cert- - --dest-creds - --dest-registry-token string - --format - --retry-times - --sign-by - --sign-passphrase-file - --src - --src-authfile - --src-cert-dir - --src-creds - --src-registry-token - --src-username - --src-password - --dest-username - --dest-password - " - - local boolean_options=" - --all - --dest-no-creds - --dest-tls-verify - --dry-run - --remove-signatures - --scoped - --src-no-creds - --src-tls-verify - --keep-going - --preserve-digests - " - - local transports - transports=" - $(_skopeo_supported_transports "${FUNCNAME//"_skopeo_"/}") - " - - _complete_ "$options_with_args" "$boolean_options" "$transports" -} - -_skopeo_inspect() { - local options_with_args=" - --authfile - --creds - --cert-dir - --format - --retry-times - --registry-token - --username - --password - " - local boolean_options=" - --config - --raw - --tls-verify - --no-creds - --no-tags -n - " - - local transports - transports=" - $(_skopeo_supported_transports "${FUNCNAME//"_skopeo_"/}") - " - - _complete_ "$options_with_args" "$boolean_options" "$transports" -} - -_skopeo_standalone_sign() { - local options_with_args=" - -o --output - --passphrase-file - " - local boolean_options=" - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_standalone_verify() { - local options_with_args=" - " - local boolean_options=" - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_manifest_digest() { - local options_with_args=" - " - local boolean_options=" - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_delete() { - local options_with_args=" - --authfile - --creds - --cert-dir - --registry-token - --username - --password - " - local boolean_options=" - --tls-verify - --no-creds - " - - local transports - transports=" - $(_skopeo_supported_transports "${FUNCNAME//"_skopeo_"/}") - " - - _complete_ "$options_with_args" "$boolean_options" "$transports" -} - -_skopeo_layers() { - local options_with_args=" - --authfile - --creds - --cert-dir - --registry-token - --username - --password - " - local boolean_options=" - --tls-verify - --no-creds - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_list_repository_tags() { - local options_with_args=" - --authfile - --creds - --cert-dir - --registry-token - --username - --password - " - - local boolean_options=" - --tls-verify - --no-creds - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_login() { - local options_with_args=" - --authfile - --cert-dir - --password -p - --username -u - " - - local boolean_options=" - --get-login - --tls-verify - --password-stdin - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_logout() { - local options_with_args=" - --authfile - " - - local boolean_options=" - --all -a - " - _complete_ "$options_with_args" "$boolean_options" -} - -_skopeo_skopeo() { - # XXX: Changes here need to be reflected in the manually expanded - # string in the `case` statement below as well. - local options_with_args=" - --policy - --registries.d - --override-arch - --override-os - --override-variant - --command-timeout - --tmpdir - " - local boolean_options=" - --insecure-policy - --debug - --version -v - --help -h - " - - local commands=( - copy - delete - inspect - list-tags - login - logout - manifest-digest - standalone-sign - standalone-verify - sync - help - h - ) - - case "$prev" in - # XXX: Changes here need to be reflected in $options_with_args as well. - --policy|--registries.d|--override-arch|--override-os|--override-variant|--command-timeout) - return - ;; - esac - - case "$cur" in - -*) - while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "$boolean_options $options_with_args" -- "$cur") - ;; - *) - while IFS='' read -r line; do COMPREPLY+=("$line"); done < <(compgen -W "${commands[*]} help" -- "$cur") - ;; - esac -} - -_cli_bash_autocomplete() { - local cur - - COMPREPLY=() - cur="${COMP_WORDS[COMP_CWORD]}" - COMPREPLY=() - local cur prev words cword - - _get_comp_words_by_ref -n : cur prev words cword - - local command="skopeo" cpos=0 - local counter=1 - while [ $counter -lt "$cword" ]; do - case "${words[$counter]}" in - skopeo|copy|sync|inspect|delete|manifest-digest|standalone-sign|standalone-verify|help|h|list-repository-tags) - command="${words[$counter]//-/_}" - cpos=$counter - (( cpos++ )) - break - ;; - esac - (( counter++ )) - done - - local completions_func=_skopeo_${command} - declare -F "$completions_func" >/dev/null && $completions_func - - return 0 -} - - complete -F _cli_bash_autocomplete skopeo From 6c2a415f6c06644282042bbd27202de70fba49d7 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 18 May 2022 16:24:24 +0200 Subject: [PATCH 3/4] shell completion: add install instructions docs Signed-off-by: Paul Holzinger --- install.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install.md b/install.md index 9f03890a..21b34007 100644 --- a/install.md +++ b/install.md @@ -212,6 +212,13 @@ Building in a container is simpler, but more restrictive: $ make binary ``` +### Shell completion scripts + +Skopeo has shell completion scripts for bash, zsh, fish and powershell. They are installed as part of `make install`. +You may have to restart your shell in order for them to take effect. + +For instructions to manually generate and load the scripts please see `skopeo completion --help`. + ### Installation Finally, after the binary and documentation is built: From d78bc8278206ed6a58d92582ea269c74dcf8549a Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Wed, 18 May 2022 15:56:57 +0200 Subject: [PATCH 4/4] shell completion: add completion for transports names Make sure skopeo copy/inspect/delete show the transport names when shell completion is used to not regress compared to the old bash completion script. In theory I would highly recommend to set completion functions for every flag and command. This can be ensured with a test like this: https://github.com/containers/podman/blob/main/cmd/podman/shell_completion_test.go But this is a lot of work to get right and I am neither a skopeo user or maintainer so I am missing a lot of context for most options. I think this would be better handled by a person who knows skopeo better. Normally options should either use AutocompleteNone() or AutocompleteDefault() from c/common/pkg/completion. Even better would be to add custom completion functions for arguments that only accept fixed values, see AutocompleteSupportedTransports() in this commit. Signed-off-by: Paul Holzinger --- cmd/skopeo/completions.go | 16 ++++++++++++++++ cmd/skopeo/copy.go | 5 +++-- cmd/skopeo/delete.go | 5 +++-- cmd/skopeo/inspect.go | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 cmd/skopeo/completions.go diff --git a/cmd/skopeo/completions.go b/cmd/skopeo/completions.go new file mode 100644 index 00000000..fc323a19 --- /dev/null +++ b/cmd/skopeo/completions.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/containers/image/v5/transports" + "github.com/spf13/cobra" +) + +// autocompleteSupportedTransports list all supported transports with the colon suffix. +func autocompleteSupportedTransports(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + tps := transports.ListNames() + suggestions := make([]string, 0, len(tps)) + for _, tp := range tps { + suggestions = append(suggestions, tp+":") + } + return suggestions, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index e6d29b67..84298fbd 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -64,8 +64,9 @@ Supported transports: See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - RunE: commandAction(opts.run), - Example: `skopeo copy docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest`, + RunE: commandAction(opts.run), + Example: `skopeo copy docker://quay.io/skopeo/stable:latest docker://registry.example.com/skopeo:latest`, + ValidArgsFunction: autocompleteSupportedTransports, } adjustUsage(cmd) flags := cmd.Flags() diff --git a/cmd/skopeo/delete.go b/cmd/skopeo/delete.go index b02fd9bf..f1dbeb4b 100644 --- a/cmd/skopeo/delete.go +++ b/cmd/skopeo/delete.go @@ -35,8 +35,9 @@ Supported transports: %s See skopeo(1) section "IMAGE NAMES" for the expected format `, strings.Join(transports.ListNames(), ", ")), - RunE: commandAction(opts.run), - Example: `skopeo delete docker://registry.example.com/example/pause:latest`, + RunE: commandAction(opts.run), + Example: `skopeo delete docker://registry.example.com/example/pause:latest`, + ValidArgsFunction: autocompleteSupportedTransports, } adjustUsage(cmd) flags := cmd.Flags() diff --git a/cmd/skopeo/inspect.go b/cmd/skopeo/inspect.go index d45b884e..e3c6e4d2 100644 --- a/cmd/skopeo/inspect.go +++ b/cmd/skopeo/inspect.go @@ -55,6 +55,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format Example: `skopeo inspect docker://registry.fedoraproject.org/fedora skopeo inspect --config docker://docker.io/alpine skopeo inspect --format "Name: {{.Name}} Digest: {{.Digest}}" docker://registry.access.redhat.com/ubi8`, + ValidArgsFunction: autocompleteSupportedTransports, } adjustUsage(cmd) flags := cmd.Flags()