Shellkonfiguration – Übersicht
Ein sehr nützliches Shell-Feature (das die meisten Shell-Nutzer nur in der vorbereiteten Variante verwenden) ist tab completion / programmable completion. Dieses Feature zeigt mögliche Werte für das aktuelle Wort in der Kommandozeile und erspart einem Tipparbeit durch Vervollständigung. Es gibt em wesentlichen drei Ebenen von tab completion:
Die grundlegenden Funktionen (die sich kaum deaktivieren lassen, weil bei deaktivierter programmable completion immer noch das readline-Feature complete greift, sofern man nicht das key binding dafür löscht oder (mit bash --noediting
) readline komplett deaktiviert) beinhalten die Vervollständigung von Kommandonamen und die Vervollständigung von Dateinamen und Pfaden als Argumente für beliebige Kommandos.
Für einzelne Kommandos können Funktionen definiert werden, die die möglichen Parameter (Optionen und Argumente) des Kommandos kennen und dadurch anzeigen können. Dies wird im wesentlichen durch das Paket bash-completion bereitgestellt. Dafür muss man eine Datei per source ${pfad}
(oder . ${pfad}
) in einer der Shell-Startup-Dateien (~/.bashrc
) eingebunden werden.
Man kann sich selber Shellfunktionen schreiben, um die angebotene zu verbessern (siehe die Beispiele unten).
ec:0 20:32:03 hl@notebook:~ start cmd:> : /eTab ec:0 20:32:03 hl@notebook:~ start cmd:> : /etc/ ec:0 20:32:03 hl@notebook:~ start cmd:> : /etc/paTab pam.d/ paperspecs passwd passwd- ec:0 20:32:03 hl@notebook:~ start cmd:> : /etc/pasTab ec:0 20:32:03 hl@notebook:~ start cmd:> : /etc/passwd
ec:0 22:45:01 hl@notebook:~ start cmd:> ls --Tab --all --ignore-backups --almost-all --indicator-style= --author --inode --block-size= --kibibytes --classify --literal --color --no-group --color= --numeric-uid-gid --context --quote-name --dereference --quoting-style= --dereference-command-line --recursive --dereference-command-line-symlink-to-dir --reverse --directory --show-control-chars --dired --si --escape --size --file-type --sort --format= --sort= --full-time --tabsize= --group-directories-first --time --help --time= --hide= --time-style= --hide-control-chars --version --human-readable --width= --hyperlink --zero --ignore=
Der einfachste Fall einer Vervollständigungs-Funktion ist der für ein (neues) Kommando, das nur ein Argument hat (oder mehrere Argumente derselben Art).
Dies ist das Beispiel von der Seite zu Aliasen und Shellfunktionen:
_PROG_COMPL_ABBR_ARGS () { local keys=( e o a ) local DEFAULT_COMPLETIONS_EVAL_STRING='local -A PROG_COMPL_ABBR_ARGS=( [e]=/etc/pki/trust/anchors [o]=/usr/lib/os-probes/mounted/efi [a]=/usr/share/vim/vim91/autoload ['?']=help )' local word="${COMP_WORDS[$COMP_CWORD]}" if ! declare -p PROG_COMPL_ABBR_ARGS >/dev/null 2>&1; then eval "$DEFAULT_COMPLETIONS_EVAL_STRING" fi if [ -z "$word" ]; then COMPREPLY=( "${!PROG_COMPL_ABBR_ARGS[@]}" ) return 0 fi if [ "$word" = '?' ]; then COMPREPLY=( ) echo >&2 for key in "${keys[@]}"; do printf '%2s : %s\n' "${key}" "${PROG_COMPL_ABBR_ARGS["${key}"]}" >&2 done return 0 fi if [ "${PROG_COMPL_ABBR_ARGS["$word"]:+set}" = 'set' ]; then COMPREPLY=( "${PROG_COMPL_ABBR_ARGS["$word"]}" ) else COMPREPLY=( "$word" ) fi }
Da die Anzeige der verfügbaren Abkürzungen allein einem nicht wirklich weiterhilft, kann man sich mit ? Tab die Bedeutungen anzeigen lassen.
ec:0 17:45:54 hl@notebook:~ start cmd:> . completion.PROG_COMPL_ABBR_ARGS.sh ec:0 18:05:48 hl@notebook:~ start cmd:> complete -F _PROG_COMPL_ABBR_ARGS -o filenames -A file -- -l -c ec:0 18:05:55 hl@notebook:~ start cmd:>
Die Eingabe von - l Space e Tab ändert dann -l e
in -l /etc/pki/trust/anchors
.
Es wird schnell kompliziert, wenn man mit der verfügbaren Vervollständigung eines komplexen Kommandos nicht zufrieden ist.
Das komplexe Kommando, das ich am häufigsten verwende, ist systemctl
. So häufig, dass ich schon genervt bin, wenn ich nur z.B. status
in start
umschreiben muss. Deshalb habe ich mir eine Variante der Vervollständigungs-Funktion gebastelt, die mir die häufigsten Aktionen auf einen einzelnen Buchstaben legt. Dafür habe ich die Funktion aus dem bereits erwähnten Paket bash-completion erweitert (bei openSUSE liegt die Datei in /usr/share/bash-completion/completions/systemctl):
# von _systemctl() aufgerufene Funktionen, die ich nicht verändert habe # [...]_systemctl () { local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} local i verb comps mode cur_orig local -A OPTS=( [STANDALONE]='--all -a --reverse --after --before --defaults --force -f --full -l --global --help -h --no-ask-password --no-block --legend=no --no-pager --no-reload --no-wall --now --quiet -q --system --user --version --runtime --recursive -r --firmware-setup --show-types --plain --failed --value --fail --dry-run --wait --no-warn --with-dependencies --show-transaction -T --mkdir --marked --read-only' [ARG]='--host -H --kill-whom --property -p -P --signal -s --type -t --state --job-mode --root --preset-mode -n --lines -o --output -M --machine --message --timestamp --check-inhibitors --what --image --boot-loader-menu --boot-loader-entry --reboot-argument --drop-in' ) if __contains_word "--user" ${COMP_WORDS[*]}; then mode=--user elif __contains_word "--global" ${COMP_WORDS[*]}; then mode=--user else mode=--system fi if __contains_word "$prev" ${OPTS[ARG]}; then case $prev in --signal|-s) _signals return ;; --type|-t) comps=$(__systemctl $mode -t help) ;; --state) comps=$(__systemctl $mode --state=help) ;; --job-mode) comps='fail replace replace-irreversibly isolate ignore-dependencies ignore-requirements flush' ;; --kill-whom|--kill-who) comps='all control main' ;; --root) comps=$(compgen -A directory -- "$cur" ) compopt -o filenames ;; --host|-H) comps=$(compgen -A hostname) ;; --property|-p|-P) comps=$(__systemd_properties) ;; --preset-mode) comps='full enable-only disable-only' ;; --output|-o) comps=$( systemctl --output=help 2>/dev/null ) ;; --machine|-M) comps=$( __get_machines ) ;; --timestamp) comps='pretty us µs utc us+utc µs+utc' ;; --check-inhibitors) comps='auto yes no' ;; --what) comps='configuration state cache logs runtime fdstore all' ;; --image) comps=$(compgen -A file -- "$cur") compopt -o filenames ;; --boot-loader-entry) comps=$(systemctl --boot-loader-entry=help 2>/dev/null) ;; esac COMPREPLY=( $(compgen -W '$comps' -- "$cur") ) return 0 fi if [[ "$cur" = -* ]]; then COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") ) return 0 fi local -A VERBS=( [ALL_UNITS]='cat mask' [NONTEMPLATE_UNITS]='is-active is-failed is-enabled status show preset help list-dependencies edit set-property revert' [ENABLED_UNITS]='disable' [DISABLED_UNITS]='enable' [REENABLABLE_UNITS]='reenable' [FAILED_UNITS]='reset-failed' [STARTABLE_UNITS]='start' [STOPPABLE_UNITS]='stop condstop kill try-restart condrestart freeze thaw' [ISOLATABLE_UNITS]='isolate' [RELOADABLE_UNITS]='reload condreload try-reload-or-restart force-reload' [RESTARTABLE_UNITS]='restart reload-or-restart' [TARGET_AND_UNITS]='add-wants add-requires' [MASKED_UNITS]='unmask' [JOBS]='cancel' [ENVS]='set-environment unset-environment import-environment' [STANDALONE]='daemon-reexec daemon-reload default whoami emergency exit halt hibernate hybrid-sleep suspend-then-hibernate kexec soft-reboot list-jobs list-sockets list-timers list-units list-unit-files poweroff reboot rescue show-environment suspend get-default is-system-running preset-all list-automounts list-paths' [FILE]='link switch-root' [TARGETS]='set-default' [MACHINES]='list-machines' [LOG_LEVEL]='log-level' [LOG_TARGET]='log-target' [SERVICE_LOG_LEVEL]='service-log-level' [SERVICE_LOG_TARGET]='service-log-target' [SERVICE_WATCHDOGS]='service-watchdogs' [MOUNT]='bind mount-image' ) for ((i=0; i < COMP_CWORD; i++)); do if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} && ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then verb=${COMP_WORDS[i]} break fi done # When trying to match a unit name with certain special characters in its name (i.e # foo\x2dbar:01) they get (un)escaped by bash along the way, thus causing any possible # match to fail. # The following condition solves two cases: # 1) We're trying to complete an already escaped unit name part, # i.e foo\\x2dba. In this case we need to unescape the name, so it # gets properly matched with the systemctl output (i.e. foo\x2dba). # However, we need to keep the original escaped name as well for the # final match, as the completion machinery does the unescaping # automagically. # 2) We're trying to complete an unescaped (literal) unit name part, # i.e. foo\x2dba. That means we don't have to do the unescaping # required for correct matching with systemctl's output, however, # we need to escape the name for the final match, where the completion # expects the string to be escaped. cur_orig=$cur if [[ $cur =~ '\\' ]]; then cur="$(echo $cur | xargs echo)" else cur_orig="$(printf '%q' $cur)" fi if [[ -z ${verb-} ]]; then# BEGIN: add single character shortcuts for the most important actions if [[ "$cur" = [adeou] ]]; then case "$cur" in a) COMPREPLY=( 'start' ) return 0 ;; d) COMPREPLY=( 'daemon-reload' ) return 0 ;; e) COMPREPLY=( 'restart' ) return 0 ;; o) COMPREPLY=( 'stop' ) return 0 ;; u) COMPREPLY=( 'status' ) return 0 ;; esac fi # END: add single character shortcuts for the most important actionscomps="${VERBS[*]}" elif __contains_word "$verb" ${VERBS[ALL_UNITS]}; then comps=$( __get_all_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[NONTEMPLATE_UNITS]}; then comps=$( __get_non_template_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[ENABLED_UNITS]}; then comps=$( __get_enabled_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[DISABLED_UNITS]}; then comps=$( __get_disabled_units $mode "$cur"; __get_template_names $mode "$cur") compopt -o filenames elif __contains_word "$verb" ${VERBS[REENABLABLE_UNITS]}; then comps=$( __get_disabled_units $mode "$cur"; __get_enabled_units $mode "$cur"; __get_template_names $mode "$cur") compopt -o filenames elif __contains_word "$verb" ${VERBS[STARTABLE_UNITS]}; then comps=$( __get_startable_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[RESTARTABLE_UNITS]}; then comps=$( __get_restartable_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[STOPPABLE_UNITS]}; then comps=$( __get_stoppable_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[RELOADABLE_UNITS]}; then comps=$( __get_reloadable_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[ISOLATABLE_UNITS]}; then comps=$( __filter_units_by_properties $mode AllowIsolate=yes \ $( __get_non_template_units $mode "$cur" ) ) compopt -o filenames elif __contains_word "$verb" ${VERBS[FAILED_UNITS]}; then comps=$( __get_failed_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[MASKED_UNITS]}; then comps=$( __get_masked_units $mode "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[TARGET_AND_UNITS]}; then if __contains_word "$prev" ${VERBS[TARGET_AND_UNITS]} \ || __contains_word "$prev" ${OPTS[STANDALONE]}; then comps=$( __systemctl $mode list-unit-files --type target --all "$cur*" \ | { while read -r a b; do echo " $a"; done; } ) else comps=$( __get_all_unit_files $mode "$cur" ) fi compopt -o filenames elif __contains_word "$verb" ${VERBS[STANDALONE]}; then comps='' elif __contains_word "$verb" ${VERBS[JOBS]}; then comps=$( __systemctl $mode list-jobs | { while read -r a b; do echo " $a"; done; } ) elif [ "$verb" = 'unset-environment' ]; then comps=$( __systemctl $mode show-environment \ | while read -r line; do echo " ${line%%=*}"; done ) compopt -o nospace elif [ "$verb" = 'set-environment' ]; then comps=$( __systemctl $mode show-environment \ | while read -r line; do echo " ${line%%=*}="; done ) compopt -o nospace elif [ "$verb" = 'import-environment' ]; then COMPREPLY=( $(compgen -A variable -- "$cur_orig") ) return 0 elif __contains_word "$verb" ${VERBS[FILE]}; then comps=$( compgen -A file -- "$cur" ) compopt -o filenames elif __contains_word "$verb" ${VERBS[TARGETS]}; then comps=$( __systemctl $mode list-unit-files --type target --full --all "$cur*" \ | { while read -r a b; do echo " $a"; done; } ) elif __contains_word "$verb" ${VERBS[LOG_LEVEL]}; then comps='debug info notice warning err crit alert emerg' elif __contains_word "$verb" ${VERBS[LOG_TARGET]}; then comps='console journal kmsg journal-or-kmsg null' elif __contains_word "$verb" ${VERBS[SERVICE_LOG_LEVEL]}; then if __contains_word "$prev" ${VERBS[SERVICE_LOG_LEVEL]}; then comps=$( __get_all_unit_files $mode "$cur" ) elif __contains_word "$prev" debug info notice warning err crit alert emerg; then return 0 else comps='debug info notice warning err crit alert emerg' fi elif __contains_word "$verb" ${VERBS[SERVICE_LOG_TARGET]}; then if __contains_word "$prev" ${VERBS[SERVICE_LOG_TARGET]}; then comps=$( __get_all_unit_files $mode "$cur" ) elif __contains_word "$prev" console journal kmsg journal-or-kmsg null; then return 0 else comps='console journal kmsg journal-or-kmsg null' fi elif __contains_word "$verb" ${VERBS[SERVICE_WATCHDOGS]}; then comps='on off' elif __contains_word "$verb" ${VERBS[MOUNT]}; then if __contains_word "$prev" ${VERBS[MOUNT]}; then comps=$( __get_active_services $mode "$cur" ) elif [[ "$prev" =~ .service ]]; then comps=$( compgen -A file -- "$cur" ) compopt -o filenames else return 0 fi fi COMPREPLY=( $(compgen -o filenames -W '$comps' -- "$cur_orig") ) return 0 }
Man muss zuerst die Funktionen des Pakets laden
tmp_file='/usr/share/bash-completion/bash_completion' if [ -f "$tmp_file" ]; then source "$tmp_file" fi unset tmp_file
und dann dessen Funktionsdefinition mit der eigenen überschreiben.