From 96cf0a8347662865dd3dd5b250ffa44063c2ff0e Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Fri, 26 Jan 2024 10:32:04 +0000 Subject: [PATCH 1/3] make: use separate declarations for phony targets We currently have a single line that contains the declarations for all phony targets. As we add new makefile targets, this line becomes increasingly long. Using declarations on separate lines would be easier to maintain. Signed-off-by: Radostin Stoyanov --- Makefile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 1a6e484a..3638a5b6 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,10 @@ MIN_GO_MAJOR_VER = 1 MIN_GO_MINOR_VER = 20 GO_VALIDATION_ERR = Go version is not supported. Please update to at least $(MIN_GO_MAJOR_VER).$(MIN_GO_MINOR_VER) +.PHONY: all all: $(NAME) +.PHONY: check-go-version check-go-version: @if [ $(GO_MAJOR_VER) -gt $(MIN_GO_MAJOR_VER) ]; then \ exit 0 ;\ @@ -41,40 +43,50 @@ $(NAME).coverage: check-go-version $(GO_SRC) -o $@ \ -ldflags "-X main.name=$(NAME) -X main.version=${VERSION}" +.PHONY: release release: CGO_ENABLED=0 $(GO_BUILD) -o $(NAME) -ldflags "-X main.name=$(NAME) -X main.version=${VERSION}" +.PHONY: install install: $(NAME) @echo " INSTALL " $< @mkdir -p $(DESTDIR)$(BINDIR) @install -m0755 $< $(DESTDIR)$(BINDIR) @make -C docs install +.PHONY: uninstall uninstall: @make -C docs uninstall @echo " UNINSTALL" $(NAME) @$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME)) +.PHONY: clean clean: rm -f $(NAME) junit.xml $(NAME).coverage $(COVERAGE_PATH)/* if [ -d $(COVERAGE_PATH) ]; then rmdir $(COVERAGE_PATH); fi @make -C docs clean +.PHONY: golang-lint golang-lint: golangci-lint run +.PHONY: shellcheck shellcheck: shellcheck test/*bats +.PHONY: lint lint: golang-lint shellcheck +.PHONY: test test: $(NAME) $(GO) test -v ./... make -C test +.PHONY: test-junit test-junit: $(NAME) make -C test test-junit clean +.PHONY: coverage coverage: check-go-version $(NAME).coverage mkdir -p $(COVERAGE_PATH) COVERAGE_PATH=$(COVERAGE_PATH) COVERAGE=1 make -C test @@ -82,19 +94,23 @@ coverage: check-go-version $(NAME).coverage $(GO) tool covdata percent -i=${COVERAGE_PATH} $(GO) tool covdata textfmt -i=${COVERAGE_PATH} -o ${COVERAGE_PATH}/coverage.out +.PHONY: codecov codecov: curl -Os https://uploader.codecov.io/latest/linux/codecov chmod +x codecov ./codecov -f "$(COVERAGE_PATH)"/coverage.out +.PHONY: vendor vendor: go mod tidy go mod vendor go mod verify +.PHONY: docs docs: @make -C docs +.PHONY: help help: @echo "Usage: make " @echo " * clean - remove artifacts" @@ -111,5 +127,3 @@ help: @echo " * uninstall - remove the installed binary from $(BINDIR)" @echo " * release - build a static binary" @echo " * help - show help" - -.PHONY: clean docs install uninstall release lint golang-lint shellcheck vendor test help check-go-version test-junit From be4f035685c3811e25570ed9e2a497795d0a6287 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Fri, 26 Jan 2024 10:44:33 +0000 Subject: [PATCH 2/3] make: generate auto-completion files This patch extends the Makefile with targets to generate, install, and uninstall auto-completion files. Signed-off-by: Radostin Stoyanov --- Makefile | 40 +++- completions/README.md | 6 + completions/bash/checkpointctl | 338 ++++++++++++++++++++++++++++ completions/fish/checkpointctl.fish | 235 +++++++++++++++++++ completions/zsh/_checkpointctl | 212 +++++++++++++++++ 5 files changed, 829 insertions(+), 2 deletions(-) create mode 100644 completions/README.md create mode 100644 completions/bash/checkpointctl create mode 100644 completions/fish/checkpointctl.fish create mode 100644 completions/zsh/_checkpointctl diff --git a/Makefile b/Makefile index 3638a5b6..24d93da3 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ GO_SRC = $(shell find . -name \*.go) GO_BUILD = $(GO) build NAME = checkpointctl +BASHINSTALLDIR=${PREFIX}/share/bash-completion/completions +ZSHINSTALLDIR=${PREFIX}/share/zsh/site-functions +FISHINSTALLDIR=${PREFIX}/share/fish/vendor_completions.d + include Makefile.versions COVERAGE_PATH ?= $(shell pwd)/.coverage @@ -48,14 +52,14 @@ release: CGO_ENABLED=0 $(GO_BUILD) -o $(NAME) -ldflags "-X main.name=$(NAME) -X main.version=${VERSION}" .PHONY: install -install: $(NAME) +install: $(NAME) install.completions @echo " INSTALL " $< @mkdir -p $(DESTDIR)$(BINDIR) @install -m0755 $< $(DESTDIR)$(BINDIR) @make -C docs install .PHONY: uninstall -uninstall: +uninstall: uninstall.completions @make -C docs uninstall @echo " UNINSTALL" $(NAME) @$(RM) $(addprefix $(DESTDIR)$(BINDIR)/,$(NAME)) @@ -110,9 +114,41 @@ vendor: docs: @make -C docs +.PHONY: completions +completions: $(NAME) + declare -A outfiles=([bash]=%s [zsh]=_%s [fish]=%s.fish);\ + for shell in $${!outfiles[*]}; do \ + outfile=$$(printf "completions/$$shell/$${outfiles[$$shell]}" $(NAME)); \ + ./$(NAME) completion $$shell >| $$outfile; \ + done + +.PHONY: validate.completions +validate.completions: SHELL:=/usr/bin/env bash # Set shell to bash for this target +validate.completions: + # Check if the files can be loaded by the shell + . completions/bash/$(NAME) + if [ -x /bin/zsh ]; then /bin/zsh completions/zsh/_$(NAME); fi + if [ -x /bin/fish ]; then /bin/fish completions/fish/$(NAME).fish; fi + +.PHONY: install.completions +install.completions: + @install -d -m 755 ${DESTDIR}${BASHINSTALLDIR} + @install -m 644 completions/bash/$(NAME) ${DESTDIR}${BASHINSTALLDIR} + @install -d -m 755 ${DESTDIR}${ZSHINSTALLDIR} + @install -m 644 completions/zsh/_$(NAME) ${DESTDIR}${ZSHINSTALLDIR} + @install -d -m 755 ${DESTDIR}${FISHINSTALLDIR} + @install -m 644 completions/fish/$(NAME).fish ${DESTDIR}${FISHINSTALLDIR} + +.PHONY: uninstall.completions +uninstall.completions: + @$(RM) $(addprefix ${DESTDIR}${BASHINSTALLDIR}/,$(NAME)) + @$(RM) $(addprefix ${DESTDIR}${ZSHINSTALLDIR}/,_$(NAME)) + @$(RM) $(addprefix ${DESTDIR}${FISHINSTALLDIR}/,$(NAME).fish) + .PHONY: help help: @echo "Usage: make " + @echo " * completions - generate auto-completion files" @echo " * clean - remove artifacts" @echo " * docs - build man pages" @echo " * lint - verify the source code (shellcheck/golangci-lint)" diff --git a/completions/README.md b/completions/README.md new file mode 100644 index 00000000..57e046d7 --- /dev/null +++ b/completions/README.md @@ -0,0 +1,6 @@ +# Shell completion scripts + +checkpointctl offers shell completion scripts for bash, zsh and fish. These completion scripts are generated by `make completions`; do not +edit these files directly. To install them you can run `sudo make install.completions`. + +For information about these scripts see `checkpointctl completion --help`. diff --git a/completions/bash/checkpointctl b/completions/bash/checkpointctl new file mode 100644 index 00000000..9df5ab79 --- /dev/null +++ b/completions/bash/checkpointctl @@ -0,0 +1,338 @@ +# bash completion V2 for checkpointctl -*- shell-script -*- + +__checkpointctl_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Macs have bash3 for which the bash-completion package doesn't include +# _init_completion. This is a minimal version of that function. +__checkpointctl_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +# This function calls the checkpointctl program to obtain the completion +# results and the directive. It fills the 'out' and 'directive' vars. +__checkpointctl_get_completion_results() { + local requestComp lastParam lastChar args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly checkpointctl allows handling aliases + args=("${words[@]:1}") + requestComp="${words[0]} __complete ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __checkpointctl_debug "lastParam ${lastParam}, lastChar ${lastChar}" + + if [[ -z ${cur} && ${lastChar} != = ]]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __checkpointctl_debug "Adding extra empty parameter" + requestComp="${requestComp} ''" + fi + + # When completing a flag with an = (e.g., checkpointctl -n=) + # bash focuses on the part after the =, so we need to remove + # the flag part from $cur + if [[ ${cur} == -*=* ]]; then + cur="${cur#*=}" + fi + + __checkpointctl_debug "Calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [[ ${directive} == "${out}" ]]; then + # There is not directive specified + directive=0 + fi + __checkpointctl_debug "The completion directive is: ${directive}" + __checkpointctl_debug "The completions are: ${out}" +} + +__checkpointctl_process_completion_results() { + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + if (((directive & shellCompDirectiveError) != 0)); then + # Error code. No completion. + __checkpointctl_debug "Received error from custom completion go code" + return + else + if (((directive & shellCompDirectiveNoSpace) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __checkpointctl_debug "Activating no space" + compopt -o nospace + else + __checkpointctl_debug "No space directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveKeepOrder) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + # no sort isn't supported for bash less than < 4.4 + if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then + __checkpointctl_debug "No sort directive not supported in this version of bash" + else + __checkpointctl_debug "Activating keep order" + compopt -o nosort + fi + else + __checkpointctl_debug "No sort directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveNoFileComp) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __checkpointctl_debug "Activating no file completion" + compopt +o default + else + __checkpointctl_debug "No file completion directive not supported in this version of bash" + fi + fi + fi + + # Separate activeHelp from normal completions + local completions=() + local activeHelp=() + __checkpointctl_extract_activeHelp + + if (((directive & shellCompDirectiveFilterFileExt) != 0)); then + # File extension filtering + local fullFilter filter filteringCmd + + # Do not use quotes around the $completions variable or else newline + # characters will be kept. + for filter in ${completions[*]}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __checkpointctl_debug "File filtering command: $filteringCmd" + $filteringCmd + elif (((directive & shellCompDirectiveFilterDirs) != 0)); then + # File completion for directories only + + local subdir + subdir=${completions[0]} + if [[ -n $subdir ]]; then + __checkpointctl_debug "Listing directories in $subdir" + pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return + else + __checkpointctl_debug "Listing directories in ." + _filedir -d + fi + else + __checkpointctl_handle_completion_types + fi + + __checkpointctl_handle_special_char "$cur" : + __checkpointctl_handle_special_char "$cur" = + + # Print the activeHelp statements before we finish + if ((${#activeHelp[*]} != 0)); then + printf "\n"; + printf "%s\n" "${activeHelp[@]}" + printf "\n" + + # The prompt format is only available from bash 4.4. + # We test if it is available before using it. + if (x=${PS1@P}) 2> /dev/null; then + printf "%s" "${PS1@P}${COMP_LINE[@]}" + else + # Can't print the prompt. Just print the + # text the user had typed, it is workable enough. + printf "%s" "${COMP_LINE[@]}" + fi + fi +} + +# Separate activeHelp lines from real completions. +# Fills the $activeHelp and $completions arrays. +__checkpointctl_extract_activeHelp() { + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + + while IFS='' read -r comp; do + if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then + comp=${comp:endIndex} + __checkpointctl_debug "ActiveHelp found: $comp" + if [[ -n $comp ]]; then + activeHelp+=("$comp") + fi + else + # Not an activeHelp line but a normal completion + completions+=("$comp") + fi + done <<<"${out}" +} + +__checkpointctl_handle_completion_types() { + __checkpointctl_debug "__checkpointctl_handle_completion_types: COMP_TYPE is $COMP_TYPE" + + case $COMP_TYPE in + 37|42) + # Type: menu-complete/menu-complete-backward and insert-completions + # If the user requested inserting one completion at a time, or all + # completions at once on the command-line we must remove the descriptions. + # https://github.com/spf13/cobra/issues/1508 + local tab=$'\t' comp + while IFS='' read -r comp; do + [[ -z $comp ]] && continue + # Strip any description + comp=${comp%%$tab*} + # Only consider the completions that match + if [[ $comp == "$cur"* ]]; then + COMPREPLY+=("$comp") + fi + done < <(printf "%s\n" "${completions[@]}") + ;; + + *) + # Type: complete (normal completion) + __checkpointctl_handle_standard_completion_case + ;; + esac +} + +__checkpointctl_handle_standard_completion_case() { + local tab=$'\t' comp + + # Short circuit to optimize if we don't have descriptions + if [[ "${completions[*]}" != *$tab* ]]; then + IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") + return 0 + fi + + local longest=0 + local compline + # Look for the longest completion so that we can format things nicely + while IFS='' read -r compline; do + [[ -z $compline ]] && continue + # Strip any description before checking the length + comp=${compline%%$tab*} + # Only consider the completions that match + [[ $comp == "$cur"* ]] || continue + COMPREPLY+=("$compline") + if ((${#comp}>longest)); then + longest=${#comp} + fi + done < <(printf "%s\n" "${completions[@]}") + + # If there is a single completion left, remove the description text + if ((${#COMPREPLY[*]} == 1)); then + __checkpointctl_debug "COMPREPLY[0]: ${COMPREPLY[0]}" + comp="${COMPREPLY[0]%%$tab*}" + __checkpointctl_debug "Removed description from single completion, which is now: ${comp}" + COMPREPLY[0]=$comp + else # Format the descriptions + __checkpointctl_format_comp_descriptions $longest + fi +} + +__checkpointctl_handle_special_char() +{ + local comp="$1" + local char=$2 + if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then + local word=${comp%"${comp##*${char}}"} + local idx=${#COMPREPLY[*]} + while ((--idx >= 0)); do + COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} + done + fi +} + +__checkpointctl_format_comp_descriptions() +{ + local tab=$'\t' + local comp desc maxdesclength + local longest=$1 + + local i ci + for ci in ${!COMPREPLY[*]}; do + comp=${COMPREPLY[ci]} + # Properly format the description string which follows a tab character if there is one + if [[ "$comp" == *$tab* ]]; then + __checkpointctl_debug "Original comp: $comp" + desc=${comp#*$tab} + comp=${comp%%$tab*} + + # $COLUMNS stores the current shell width. + # Remove an extra 4 because we add 2 spaces and 2 parentheses. + maxdesclength=$(( COLUMNS - longest - 4 )) + + # Make sure we can fit a description of at least 8 characters + # if we are to align the descriptions. + if ((maxdesclength > 8)); then + # Add the proper number of spaces to align the descriptions + for ((i = ${#comp} ; i < longest ; i++)); do + comp+=" " + done + else + # Don't pad the descriptions so we can fit more text after the completion + maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + fi + + # If there is enough space for any description text, + # truncate the descriptions that are too long for the shell width + if ((maxdesclength > 0)); then + if ((${#desc} > maxdesclength)); then + desc=${desc:0:$(( maxdesclength - 1 ))} + desc+="…" + fi + comp+=" ($desc)" + fi + COMPREPLY[ci]=$comp + __checkpointctl_debug "Final comp: $comp" + fi + done +} + +__start_checkpointctl() +{ + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n =: || return + else + __checkpointctl_init_completion -n =: || return + fi + + __checkpointctl_debug + __checkpointctl_debug "========= starting completion logic ==========" + __checkpointctl_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __checkpointctl_debug "Truncated words[*]: ${words[*]}," + + local out directive + __checkpointctl_get_completion_results + __checkpointctl_process_completion_results +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_checkpointctl checkpointctl +else + complete -o default -o nospace -F __start_checkpointctl checkpointctl +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/completions/fish/checkpointctl.fish b/completions/fish/checkpointctl.fish new file mode 100644 index 00000000..4f0d183b --- /dev/null +++ b/completions/fish/checkpointctl.fish @@ -0,0 +1,235 @@ +# fish completion for checkpointctl -*- shell-script -*- + +function __checkpointctl_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __checkpointctl_perform_completion + __checkpointctl_debug "Starting __checkpointctl_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __checkpointctl_debug "args: $args" + __checkpointctl_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "CHECKPOINTCTL_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" + + __checkpointctl_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __checkpointctl_debug "Comps: $comps" + __checkpointctl_debug "DirectiveLine: $directiveLine" + __checkpointctl_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" +end + +# this function limits calls to __checkpointctl_perform_completion, by caching the result behind $__checkpointctl_perform_completion_once_result +function __checkpointctl_perform_completion_once + __checkpointctl_debug "Starting __checkpointctl_perform_completion_once" + + if test -n "$__checkpointctl_perform_completion_once_result" + __checkpointctl_debug "Seems like a valid result already exists, skipping __checkpointctl_perform_completion" + return 0 + end + + set --global __checkpointctl_perform_completion_once_result (__checkpointctl_perform_completion) + if test -z "$__checkpointctl_perform_completion_once_result" + __checkpointctl_debug "No completions, probably due to a failure" + return 1 + end + + __checkpointctl_debug "Performed completions and set __checkpointctl_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__checkpointctl_perform_completion_once_result variable after completions are run +function __checkpointctl_clear_perform_completion_once_result + __checkpointctl_debug "" + __checkpointctl_debug "========= clearing previously set __checkpointctl_perform_completion_once_result variable ==========" + set --erase __checkpointctl_perform_completion_once_result + __checkpointctl_debug "Successfully erased the variable __checkpointctl_perform_completion_once_result" +end + +function __checkpointctl_requires_order_preservation + __checkpointctl_debug "" + __checkpointctl_debug "========= checking if order preservation is required ==========" + + __checkpointctl_perform_completion_once + if test -z "$__checkpointctl_perform_completion_once_result" + __checkpointctl_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__checkpointctl_perform_completion_once_result[-1]) + __checkpointctl_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder 32 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) + __checkpointctl_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __checkpointctl_debug "This does require order preservation" + return 0 + end + + __checkpointctl_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __checkpointctl_comp_results +# - Return false if file completion should be performed +function __checkpointctl_prepare_completions + __checkpointctl_debug "" + __checkpointctl_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __checkpointctl_comp_results + + __checkpointctl_perform_completion_once + __checkpointctl_debug "Completion results: $__checkpointctl_perform_completion_once_result" + + if test -z "$__checkpointctl_perform_completion_once_result" + __checkpointctl_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__checkpointctl_perform_completion_once_result[-1]) + set --global __checkpointctl_comp_results $__checkpointctl_perform_completion_once_result[1..-2] + + __checkpointctl_debug "Completions are: $__checkpointctl_comp_results" + __checkpointctl_debug "Directive is: $directive" + + set -l shellCompDirectiveError 1 + set -l shellCompDirectiveNoSpace 2 + set -l shellCompDirectiveNoFileComp 4 + set -l shellCompDirectiveFilterFileExt 8 + set -l shellCompDirectiveFilterDirs 16 + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) + if test $compErr -eq 1 + __checkpointctl_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __checkpointctl_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) + + __checkpointctl_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __checkpointctl_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__checkpointctl_comp_results) + set --global __checkpointctl_comp_results $completions + __checkpointctl_debug "Filtered completions are: $__checkpointctl_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__checkpointctl_comp_results) + __checkpointctl_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__checkpointctl_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __checkpointctl_debug "Adding second completion to perform nospace directive" + set --global __checkpointctl_comp_results $split[1] $split[1]. + __checkpointctl_debug "Completions are now: $__checkpointctl_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __checkpointctl_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "checkpointctl" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "checkpointctl " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c checkpointctl -e + +# this will get called after the two calls below and clear the $__checkpointctl_perform_completion_once_result global +complete -c checkpointctl -n '__checkpointctl_clear_perform_completion_once_result' +# The call to __checkpointctl_prepare_completions will setup __checkpointctl_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c checkpointctl -n 'not __checkpointctl_requires_order_preservation && __checkpointctl_prepare_completions' -f -a '$__checkpointctl_comp_results' +# otherwise we use the -k flag +complete -k -c checkpointctl -n '__checkpointctl_requires_order_preservation && __checkpointctl_prepare_completions' -f -a '$__checkpointctl_comp_results' diff --git a/completions/zsh/_checkpointctl b/completions/zsh/_checkpointctl new file mode 100644 index 00000000..d8b05a7e --- /dev/null +++ b/completions/zsh/_checkpointctl @@ -0,0 +1,212 @@ +#compdef checkpointctl +compdef _checkpointctl checkpointctl + +# zsh completion for checkpointctl -*- shell-script -*- + +__checkpointctl_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_checkpointctl() +{ + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __checkpointctl_debug "\n========= starting completion logic ==========" + __checkpointctl_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __checkpointctl_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __checkpointctl_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., checkpointctl -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} __complete ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __checkpointctl_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __checkpointctl_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __checkpointctl_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%s\n" "${out[@]}") + __checkpointctl_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __checkpointctl_debug "No directive found. Setting do default" + directive=0 + fi + + __checkpointctl_debug "directive: ${directive}" + __checkpointctl_debug "completions: ${out}" + __checkpointctl_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __checkpointctl_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __checkpointctl_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __checkpointctl_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __checkpointctl_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __checkpointctl_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __checkpointctl_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __checkpointctl_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __checkpointctl_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __checkpointctl_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __checkpointctl_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __checkpointctl_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __checkpointctl_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __checkpointctl_debug "_describe did not find completions." + __checkpointctl_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __checkpointctl_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __checkpointctl_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_checkpointctl" ]; then + _checkpointctl +fi From b76a3902cabd28998c919a5f86e175697e8877b2 Mon Sep 17 00:00:00 2001 From: Radostin Stoyanov Date: Fri, 26 Jan 2024 11:00:49 +0000 Subject: [PATCH 3/3] ci: validate auto-completion files Signed-off-by: Radostin Stoyanov --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9700c571..67012da3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v3 - name: Install tools run: | - sudo dnf -y install ShellCheck bats golang criu asciidoctor iptables iproute kmod jq + sudo dnf -y install ShellCheck bats golang criu asciidoctor iptables iproute kmod jq bash bash-completion zsh fish sudo modprobe -va ip_tables ip6table_filter nf_conntrack nf_conntrack_netlink - name: Run make shellcheck run: make shellcheck @@ -33,6 +33,8 @@ jobs: path: test/junit.xml - name: Run make install/uninstall run: test/uninstall.sh + - name: Run make validate.completions + run: make validate.completions event_file: name: "Event File"