With the plethora of new command-line options, it is starting to get difficult to remember them all. This commit introduces shell completions for bash and zsh for the convenience of the user.
Instead of writing the completion files by hand (a tedious task), the files are generated automatically from a Python spec using shtab [1]. To ease the integration into the main Makefile (automake), the generated completions are stored in git as well. This has the advantage that 'make install' will install the completion files. A CI job ensures that the generated files are in sync with the spec (i.e. python files). [1] https://github.com/iterative/shtab Signed-off-by: Michael Adler <[email protected]> --- .github/workflows/main.yaml | 9 ++ .gitignore | 4 + Makefile.am | 6 + share/completion/.gitignore | 2 + share/completion/Makefile | 38 +++++ share/completion/bg_printenv/cli.py | 31 ++++ share/completion/bg_printenv/common.py | 1 + share/completion/bg_setenv/cli.py | 47 ++++++ share/completion/bg_setenv/common.py | 1 + share/completion/common.py | 23 +++ .../generated/bash/bg_printenv.bash | 139 +++++++++++++++++ .../completion/generated/bash/bg_setenv.bash | 142 ++++++++++++++++++ share/completion/generated/zsh/_bg_printenv | 37 +++++ share/completion/generated/zsh/_bg_setenv | 43 ++++++ 14 files changed, 523 insertions(+) create mode 100644 share/completion/.gitignore create mode 100644 share/completion/Makefile create mode 100644 share/completion/bg_printenv/cli.py create mode 120000 share/completion/bg_printenv/common.py create mode 100644 share/completion/bg_setenv/cli.py create mode 120000 share/completion/bg_setenv/common.py create mode 100644 share/completion/common.py create mode 100644 share/completion/generated/bash/bg_printenv.bash create mode 100644 share/completion/generated/bash/bg_setenv.bash create mode 100644 share/completion/generated/zsh/_bg_printenv create mode 100644 share/completion/generated/zsh/_bg_setenv diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 10b2a2d..78f6ce4 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -160,3 +160,12 @@ jobs: # files are provided. Compare 'cppcheck --help'. cppcheck -f -q --error-exitcode=2 $enable $suppress $ignore \ $cpp_conf $includes . + + - name: Generate shell completions + if: ${{ matrix.target == 'amd64' }} + run: | + sudo apt-get install -y --no-install-recommends make python3 pip git + sudo pip install shtab + rm -rf share/completion/generated + make -C share/completion + git diff --exit-code share/completion diff --git a/.gitignore b/.gitignore index 1943fe0..f472f03 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,7 @@ test_ebgenv_api test_ebgenv_api_internal test_probe_config_file test_probe_config_partitions + +### Python ### +__pycache__ +*.pyc diff --git a/Makefile.am b/Makefile.am index 8081839..9351488 100644 --- a/Makefile.am +++ b/Makefile.am @@ -218,6 +218,12 @@ efibootguard_DATA = $(efi_loadername) CLEANFILES += $(efi_objects) $(efi_solib) $(efi_loadername) EXTRA_DIST += $(efi_sources) +bashcompletiondir = ${datarootdir}/efibootguard/completion/bash +bashcompletion_DATA = share/completion/generated/bash/bg_setenv.bash share/completion/generated/bash/bg_printenv.bash + +zshcompletiondir = ${datarootdir}/efibootguard/completion/zsh +zshcompletion_DATA = share/completion/generated/zsh/_bg_setenv share/completion/generated/zsh/_bg_printenv + $(top_builddir)/%.o: $(top_srcdir)/%.c @$(MKDIR_P) $(shell dirname $@)/ $(AM_V_CC)$(GNUEFI_CC) $(efi_cppflags) $(efi_cflags) -c $< -o $@ diff --git a/share/completion/.gitignore b/share/completion/.gitignore new file mode 100644 index 0000000..b731cd1 --- /dev/null +++ b/share/completion/.gitignore @@ -0,0 +1,2 @@ +!bg_printenv +!bg_setenv diff --git a/share/completion/Makefile b/share/completion/Makefile new file mode 100644 index 0000000..21105bb --- /dev/null +++ b/share/completion/Makefile @@ -0,0 +1,38 @@ +# Copyright (c) Siemens AG, 2021 +# +# Authors: +# Michael Adler <[email protected]> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# +# SPDX-License-Identifier: GPL-2.0 +PYTHON ?= python3 + +all: bash-completion zsh-completion + +bash-completion: generated/bash/bg_setenv.bash generated/bash/bg_printenv.bash + +zsh-completion: generated/zsh/_bg_setenv generated/zsh/_bg_printenv + +generated/bash/bg_setenv.bash: bg_setenv/cli.py + @echo "Generating $@" + @mkdir -p $(@D) + @$(PYTHON) -m shtab --shell=bash -u "bg_setenv.cli.bg_setenv" >$@ + +generated/bash/bg_printenv.bash: bg_printenv/cli.py + @echo "Generating $@" + @mkdir -p $(@D) + @-$(PYTHON) -m shtab --shell=bash -u "bg_printenv.cli.bg_printenv" >$@ + +generated/zsh/_bg_setenv: bg_setenv/cli.py + @echo "Generating $@" + @mkdir -p $(@D) + @-$(PYTHON) -m shtab --shell=zsh -u "bg_setenv.cli.bg_setenv" >$@ + +generated/zsh/_bg_printenv: bg_printenv/cli.py + @echo "Generating $@" + @mkdir -p $(@D) + @-$(PYTHON) -m shtab --shell=zsh -u "bg_printenv.cli.bg_printenv" >$@ + +.PHONY: all bash-completion zsh-completion diff --git a/share/completion/bg_printenv/cli.py b/share/completion/bg_printenv/cli.py new file mode 100644 index 0000000..570faf9 --- /dev/null +++ b/share/completion/bg_printenv/cli.py @@ -0,0 +1,31 @@ +# +# Copyright (c) Siemens AG, 2021 +# +# Authors: +# Michael Adler <[email protected]> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# +# SPDX-License-Identifier: GPL-2.0 + +import argparse + +from .common import add_common_opts + + +def bg_printenv(): + parser = argparse.ArgumentParser(prog="bg_printenv", add_help=False) + add_common_opts(parser) + parser.add_argument("-c", "--current", action="store_true", help="Only print values from the current environment") + parser.add_argument( + "-o", + "--output", + choices=["in_progress", "revision", "kernel", "kernelargs", "watchdog_timeout", "ustate", "user"], + help="Comma-separated list of fields which are printed", + ) + parser.add_argument("-r", "--raw", action="store_true", help="Raw output mode") + parser.add_argument("--usage", action="store_true", help="Give a short usage message") + return parser + + diff --git a/share/completion/bg_printenv/common.py b/share/completion/bg_printenv/common.py new file mode 120000 index 0000000..a11703e --- /dev/null +++ b/share/completion/bg_printenv/common.py @@ -0,0 +1 @@ +../common.py \ No newline at end of file diff --git a/share/completion/bg_setenv/cli.py b/share/completion/bg_setenv/cli.py new file mode 100644 index 0000000..9698882 --- /dev/null +++ b/share/completion/bg_setenv/cli.py @@ -0,0 +1,47 @@ +# +# Copyright (c) Siemens AG, 2021 +# +# Authors: +# Michael Adler <[email protected]> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# +# SPDX-License-Identifier: GPL-2.0 + +import argparse + +from .common import add_common_opts + + +def bg_setenv(): + parser = argparse.ArgumentParser(prog="bg_setenv", add_help=False) + add_common_opts(parser) + parser.add_argument("-P", "--preserve", action="store_true", help="Preserve existing entries") + parser.add_argument("-k", "--kernel", metavar="KERNEL", help="Set kernel to load") + parser.add_argument("-a", "--args", metavar="KERNEL_ARGS", help="Set kernel arguments") + parser.add_argument("-r", "--revision", metavar="REVISION", help="Set revision value") + parser.add_argument( + "-s", + "--ustate", + choices=["OK", "INSTALLED", "TESTING", "FAILED", "UNKNOWN"], + metavar="USTATE", + help="Set update status for environment", + ) + parser.add_argument("-w", "--watchdog", metavar="WATCHDOG_TIMEOUT", help="Watchdog timeout in seconds") + parser.add_argument("-c", "--confirm", action="store_true", help="Confirm working environment") + parser.add_argument("-u", "--update", action="store_true", help="Automatically update oldest revision") + parser.add_argument( + "-x", + "--uservar", + metavar="KEY=VAL", + help="Set user-defined string variable. For setting multiple variables, use this option multiple times.", + ) + parser.add_argument( + "-i", + "--in_progress", + metavar="IN_PROGRESS", + choices=["0", "1"], + help="Set in_progress variable to simulate a running update process.", + ) + return parser diff --git a/share/completion/bg_setenv/common.py b/share/completion/bg_setenv/common.py new file mode 120000 index 0000000..a11703e --- /dev/null +++ b/share/completion/bg_setenv/common.py @@ -0,0 +1 @@ +../common.py \ No newline at end of file diff --git a/share/completion/common.py b/share/completion/common.py new file mode 100644 index 0000000..8134aef --- /dev/null +++ b/share/completion/common.py @@ -0,0 +1,23 @@ +# +# Copyright (c) Siemens AG, 2021 +# +# Authors: +# Michael Adler <[email protected]> +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# +# SPDX-License-Identifier: GPL-2.0 + +import shtab + + +def add_common_opts(parser): + parser.add_argument( + "-f", "--filepath", metavar="ENVFILE", help="Environment to use. Expects a file name, usually called BGENV.DAT." + ).complete = shtab.FILE + parser.add_argument("-p", "--part", metavar="ENV_PART", type=int, help="Set environment partition to update") + parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose") + parser.add_argument("-V", "--version", action="store_true", help="Print version") + # there is a bug in shtab which currently prohibits "-?" + parser.add_argument("--help", action="store_true", help="Show help") diff --git a/share/completion/generated/bash/bg_printenv.bash b/share/completion/generated/bash/bg_printenv.bash new file mode 100644 index 0000000..3c6dd6c --- /dev/null +++ b/share/completion/generated/bash/bg_printenv.bash @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +# AUTOMATCALLY GENERATED by `shtab` + + + +_shtab_bg_printenv_option_strings=('-f' '--filepath' '-p' '--part' '-v' '--verbose' '-V' '--version' '--help' '-c' '--current' '-o' '--output' '-r' '--raw' '--usage') + +_shtab_bg_printenv__f_COMPGEN=_shtab_compgen_files +_shtab_bg_printenv___filepath_COMPGEN=_shtab_compgen_files + +_shtab_bg_printenv__o_choices='in_progress revision kernel kernelargs watchdog_timeout ustate user' +_shtab_bg_printenv___output_choices='in_progress revision kernel kernelargs watchdog_timeout ustate user' + +_shtab_bg_printenv__v_nargs=0 +_shtab_bg_printenv___verbose_nargs=0 +_shtab_bg_printenv__V_nargs=0 +_shtab_bg_printenv___version_nargs=0 +_shtab_bg_printenv___help_nargs=0 +_shtab_bg_printenv__c_nargs=0 +_shtab_bg_printenv___current_nargs=0 +_shtab_bg_printenv__r_nargs=0 +_shtab_bg_printenv___raw_nargs=0 +_shtab_bg_printenv___usage_nargs=0 + + +# $1=COMP_WORDS[1] +_shtab_compgen_files() { + compgen -f -- $1 # files +} + +# $1=COMP_WORDS[1] +_shtab_compgen_dirs() { + compgen -d -- $1 # recurse into subdirs +} + +# $1=COMP_WORDS[1] +_shtab_replace_nonword() { + echo "${1//[^[:word:]]/_}" +} + +# set default values (called for the initial parser & any subparsers) +_set_parser_defaults() { + local subparsers_var="${prefix}_subparsers[@]" + sub_parsers=${!subparsers_var} + + local current_option_strings_var="${prefix}_option_strings[@]" + current_option_strings=${!current_option_strings_var} + + completed_positional_actions=0 + + _set_new_action "pos_${completed_positional_actions}" true +} + +# $1=action identifier +# $2=positional action (bool) +# set all identifiers for an action's parameters +_set_new_action() { + current_action="${prefix}_$(_shtab_replace_nonword $1)" + + local current_action_compgen_var=${current_action}_COMPGEN + current_action_compgen="${!current_action_compgen_var}" + + local current_action_choices_var="${current_action}_choices" + current_action_choices="${!current_action_choices_var}" + + local current_action_nargs_var="${current_action}_nargs" + if [ -n "${!current_action_nargs_var}" ]; then + current_action_nargs="${!current_action_nargs_var}" + else + current_action_nargs=1 + fi + + current_action_args_start_index=$(( $word_index + 1 )) + + current_action_is_positional=$2 +} + +# Notes: +# `COMPREPLY`: what will be rendered after completion is triggered +# `completing_word`: currently typed word to generate completions for +# `${!var}`: evaluates the content of `var` and expand its content as a variable +# hello="world" +# x="hello" +# ${!x} -> ${hello} -> "world" +_shtab_bg_printenv() { + local completing_word="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=() + + prefix=_shtab_bg_printenv + word_index=0 + _set_parser_defaults + word_index=1 + + # determine what arguments are appropriate for the current state + # of the arg parser + while [ $word_index -ne $COMP_CWORD ]; do + local this_word="${COMP_WORDS[$word_index]}" + + if [[ -n $sub_parsers && " ${sub_parsers[@]} " =~ " ${this_word} " ]]; then + # valid subcommand: add it to the prefix & reset the current action + prefix="${prefix}_$(_shtab_replace_nonword $this_word)" + _set_parser_defaults + fi + + if [[ " ${current_option_strings[@]} " =~ " ${this_word} " ]]; then + # a new action should be acquired (due to recognised option string or + # no more input expected from current action); + # the next positional action can fill in here + _set_new_action $this_word false + fi + + if [[ "$current_action_nargs" != "*" ]] && \ + [[ "$current_action_nargs" != "+" ]] && \ + [[ "$current_action_nargs" != *"..." ]] && \ + (( $word_index + 1 - $current_action_args_start_index >= \ + $current_action_nargs )); then + $current_action_is_positional && let "completed_positional_actions += 1" + _set_new_action "pos_${completed_positional_actions}" true + fi + + let "word_index+=1" + done + + # Generate the completions + + if [[ "${completing_word}" == -* ]]; then + # optional argument started: use option strings + COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) + else + # use choices & compgen + COMPREPLY=( $(compgen -W "${current_action_choices}" -- "${completing_word}"; \ + [ -n "${current_action_compgen}" ] \ + && "${current_action_compgen}" "${completing_word}") ) + fi + + return 0 +} + +complete -o filenames -F _shtab_bg_printenv bg_printenv diff --git a/share/completion/generated/bash/bg_setenv.bash b/share/completion/generated/bash/bg_setenv.bash new file mode 100644 index 0000000..66a7332 --- /dev/null +++ b/share/completion/generated/bash/bg_setenv.bash @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# AUTOMATCALLY GENERATED by `shtab` + + + +_shtab_bg_setenv_option_strings=('-f' '--filepath' '-p' '--part' '-v' '--verbose' '-V' '--version' '--help' '-P' '--preserve' '-k' '--kernel' '-a' '--args' '-r' '--revision' '-s' '--ustate' '-w' '--watchdog' '-c' '--confirm' '-u' '--update' '-x' '--uservar' '-i' '--in_progress') + +_shtab_bg_setenv__f_COMPGEN=_shtab_compgen_files +_shtab_bg_setenv___filepath_COMPGEN=_shtab_compgen_files + +_shtab_bg_setenv__s_choices='OK INSTALLED TESTING FAILED UNKNOWN' +_shtab_bg_setenv___ustate_choices='OK INSTALLED TESTING FAILED UNKNOWN' +_shtab_bg_setenv__i_choices='0 1' +_shtab_bg_setenv___in_progress_choices='0 1' + +_shtab_bg_setenv__v_nargs=0 +_shtab_bg_setenv___verbose_nargs=0 +_shtab_bg_setenv__V_nargs=0 +_shtab_bg_setenv___version_nargs=0 +_shtab_bg_setenv___help_nargs=0 +_shtab_bg_setenv__P_nargs=0 +_shtab_bg_setenv___preserve_nargs=0 +_shtab_bg_setenv__c_nargs=0 +_shtab_bg_setenv___confirm_nargs=0 +_shtab_bg_setenv__u_nargs=0 +_shtab_bg_setenv___update_nargs=0 + + +# $1=COMP_WORDS[1] +_shtab_compgen_files() { + compgen -f -- $1 # files +} + +# $1=COMP_WORDS[1] +_shtab_compgen_dirs() { + compgen -d -- $1 # recurse into subdirs +} + +# $1=COMP_WORDS[1] +_shtab_replace_nonword() { + echo "${1//[^[:word:]]/_}" +} + +# set default values (called for the initial parser & any subparsers) +_set_parser_defaults() { + local subparsers_var="${prefix}_subparsers[@]" + sub_parsers=${!subparsers_var} + + local current_option_strings_var="${prefix}_option_strings[@]" + current_option_strings=${!current_option_strings_var} + + completed_positional_actions=0 + + _set_new_action "pos_${completed_positional_actions}" true +} + +# $1=action identifier +# $2=positional action (bool) +# set all identifiers for an action's parameters +_set_new_action() { + current_action="${prefix}_$(_shtab_replace_nonword $1)" + + local current_action_compgen_var=${current_action}_COMPGEN + current_action_compgen="${!current_action_compgen_var}" + + local current_action_choices_var="${current_action}_choices" + current_action_choices="${!current_action_choices_var}" + + local current_action_nargs_var="${current_action}_nargs" + if [ -n "${!current_action_nargs_var}" ]; then + current_action_nargs="${!current_action_nargs_var}" + else + current_action_nargs=1 + fi + + current_action_args_start_index=$(( $word_index + 1 )) + + current_action_is_positional=$2 +} + +# Notes: +# `COMPREPLY`: what will be rendered after completion is triggered +# `completing_word`: currently typed word to generate completions for +# `${!var}`: evaluates the content of `var` and expand its content as a variable +# hello="world" +# x="hello" +# ${!x} -> ${hello} -> "world" +_shtab_bg_setenv() { + local completing_word="${COMP_WORDS[COMP_CWORD]}" + COMPREPLY=() + + prefix=_shtab_bg_setenv + word_index=0 + _set_parser_defaults + word_index=1 + + # determine what arguments are appropriate for the current state + # of the arg parser + while [ $word_index -ne $COMP_CWORD ]; do + local this_word="${COMP_WORDS[$word_index]}" + + if [[ -n $sub_parsers && " ${sub_parsers[@]} " =~ " ${this_word} " ]]; then + # valid subcommand: add it to the prefix & reset the current action + prefix="${prefix}_$(_shtab_replace_nonword $this_word)" + _set_parser_defaults + fi + + if [[ " ${current_option_strings[@]} " =~ " ${this_word} " ]]; then + # a new action should be acquired (due to recognised option string or + # no more input expected from current action); + # the next positional action can fill in here + _set_new_action $this_word false + fi + + if [[ "$current_action_nargs" != "*" ]] && \ + [[ "$current_action_nargs" != "+" ]] && \ + [[ "$current_action_nargs" != *"..." ]] && \ + (( $word_index + 1 - $current_action_args_start_index >= \ + $current_action_nargs )); then + $current_action_is_positional && let "completed_positional_actions += 1" + _set_new_action "pos_${completed_positional_actions}" true + fi + + let "word_index+=1" + done + + # Generate the completions + + if [[ "${completing_word}" == -* ]]; then + # optional argument started: use option strings + COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) + else + # use choices & compgen + COMPREPLY=( $(compgen -W "${current_action_choices}" -- "${completing_word}"; \ + [ -n "${current_action_compgen}" ] \ + && "${current_action_compgen}" "${completing_word}") ) + fi + + return 0 +} + +complete -o filenames -F _shtab_bg_setenv bg_setenv diff --git a/share/completion/generated/zsh/_bg_printenv b/share/completion/generated/zsh/_bg_printenv new file mode 100644 index 0000000..9545483 --- /dev/null +++ b/share/completion/generated/zsh/_bg_printenv @@ -0,0 +1,37 @@ +#compdef bg_printenv + +# AUTOMATCALLY GENERATED by `shtab` + +_shtab_bg_printenv_options_=( + {-f,--filepath}"[Environment to use. Expects a file name, usually called BGENV.DAT.]:filepath:_files" + {-p,--part}"[Set environment partition to update]:part:" + {-v,--verbose}"[Be verbose]" + {-V,--version}"[Print version]" + "--help[Show help]" + {-c,--current}"[Only print values from the current environment]" + {-o,--output}"[Comma-separated list of fields which are printed]:output:(in_progress revision kernel kernelargs watchdog_timeout ustate user)" + {-r,--raw}"[Raw output mode]" + "--usage[Give a short usage message]" +) + +_shtab_bg_printenv_commands_() { + local _commands=( + + ) + + _describe 'bg_printenv commands' _commands +} + + +typeset -A opt_args +local context state line curcontext="$curcontext" + +_arguments \ + $_shtab_bg_printenv_options_ \ + \ + ': :_shtab_bg_printenv_commands_' \ + '*::args:->args' + +case $words[1] in + +esac diff --git a/share/completion/generated/zsh/_bg_setenv b/share/completion/generated/zsh/_bg_setenv new file mode 100644 index 0000000..1f3af92 --- /dev/null +++ b/share/completion/generated/zsh/_bg_setenv @@ -0,0 +1,43 @@ +#compdef bg_setenv + +# AUTOMATCALLY GENERATED by `shtab` + +_shtab_bg_setenv_options_=( + {-f,--filepath}"[Environment to use. Expects a file name, usually called BGENV.DAT.]:filepath:_files" + {-p,--part}"[Set environment partition to update]:part:" + {-v,--verbose}"[Be verbose]" + {-V,--version}"[Print version]" + "--help[Show help]" + {-P,--preserve}"[Preserve existing entries]" + {-k,--kernel}"[Set kernel to load]:kernel:" + {-a,--args}"[Set kernel arguments]:args:" + {-r,--revision}"[Set revision value]:revision:" + {-s,--ustate}"[Set update status for environment]:ustate:(OK INSTALLED TESTING FAILED UNKNOWN)" + {-w,--watchdog}"[Watchdog timeout in seconds]:watchdog:" + {-c,--confirm}"[Confirm working environment]" + {-u,--update}"[Automatically update oldest revision]" + {-x,--uservar}"[Set user-defined string variable. For setting multiple variables, use this option multiple times.]:uservar:" + {-i,--in_progress}"[Set in_progress variable to simulate a running update process.]:in_progress:(0 1)" +) + +_shtab_bg_setenv_commands_() { + local _commands=( + + ) + + _describe 'bg_setenv commands' _commands +} + + +typeset -A opt_args +local context state line curcontext="$curcontext" + +_arguments \ + $_shtab_bg_setenv_options_ \ + \ + ': :_shtab_bg_setenv_commands_' \ + '*::args:->args' + +case $words[1] in + +esac -- 2.33.1 -- You received this message because you are subscribed to the Google Groups "EFI Boot Guard" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion on the web visit https://groups.google.com/d/msgid/efibootguard-dev/20211111105914.410825-2-michael.adler%40siemens.com.
