From ea69a51bdb003dc948d8ce3a054fd418a42cfc44 Mon Sep 17 00:00:00 2001 From: Andre Noll Date: Sat, 19 Apr 2025 13:39:26 +0200 Subject: [PATCH] Revamp bash_completion. This rewrite of the bash_completion script improves completion for the para_client and para_audioc commands significantly. One flaw which is fixed by this patch is related to the fact that by default readline creates COMP_WORDS by splitting at characters that separate a shell command, including "=", but for paraslash commands we only want to split at spaces. So we modify the completer to return the special code 124 to instruct readline to try split again with space as the only delimiter. --- bash_completion | 123 +++++++++++++++++++++++++----------------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/bash_completion b/bash_completion index 5135101b..c3de2181 100644 --- a/bash_completion +++ b/bash_completion @@ -1,12 +1,11 @@ # Copyright (C) 2007 Andre Noll , see file COPYING. + _para_complete() { - local prg="$1" # the program to execute - local cur=${COMP_WORDS[$COMP_CWORD]} - local line="$COMP_LINE" OLD_IFS="$IFS" - local opts n - - # This extracts short and long options from the help output + local prg=$1 # the program to execute, either para_client or para_audioc + local cur OLD_IFS list opt + local -i n old_extglob=1 ddpos=-1 # position of the double-dash arg + # This awk script extracts short and long options from the help output. local script='{ if ($1 ~ "^-[a-zA-Z]," && $2 ~ "^--[a-zA-Z]") { print substr($1, 0, 2); @@ -18,77 +17,83 @@ _para_complete() } }' - if [[ "$cur" == -* ]]; then # option - # Depending on whether '--' is one of the previous words we - # complete either on local options, i.e. those of the program - # to execute, or call the program to print possible completions - # (to a subcommand). - local_opts=true - for ((i=0; i < $COMP_CWORD; i++)); do - [[ "${COMP_WORDS[$i]}" != '--' ]] && continue - local_opts=false - break - done - if [[ "$local_opts" == "true" ]]; then - result="-- $($prg --help | awk "$script")" - COMPREPLY=($(compgen -W "$result" -- $cur)) + # Ask readline to recreate COMP_WORDS using space as the only delimiter. + if [[ "$COMP_WORDBREAKS" != ' ' ]]; then + COMP_WORDBREAKS=' ' + return 124 + fi + n=$COMP_POINT + cur= + while ((n > 0)); do + let n--; + [[ "${COMP_LINE:$n:1}" == ' ' ]] && break + cur="${COMP_LINE:$n:1}$cur" + done + for ((n = 0; n < $COMP_CWORD; n++)); do + [[ "${COMP_WORDS[$n]}" != '--' ]] && continue + ddpos=$n + break + done + # Figure out whether we need to complete according to options of the + # given program or according to its subcommands. The 'para' shortcut + # is expected to be aliased to 'para_client --' and we always complete + # on subcommands in this case. + if [[ "$cur" == -* ]] && [[ "${COMP_WORDS[0]}" != 'para' ]]; then + # If '--' is none of the previous words, we complete on options + # of the given program. + if ((ddpos < 0 || COMP_CWORD < ddpos)); then + list=$($prg --help | awk "$script") + COMPREPLY=($(compgen -W "$list" -- "$cur")) return fi fi - # We need to call the program with --complete to get the possible - # completions. Before that, all local options must be discarded. - IFS=' ' - n=0 - for word in $line; do - ((n > 0)) && ! [[ "$word" == -* ]] && break - line="${line##*( )}" # remove leading whitespace - line="${line##+([^ ])}" - line="${line##*( )}" - let n++ - [[ "$word" == '--' ]] && break - done - IFS="$OLD_IFS" - s=$((${#COMP_LINE} - ${#line})) # how many characters have been cut - if (($COMP_POINT > $s)); then - COMP_POINT=$(($COMP_POINT - $s)) + # Rebuild the command line with the first word and anything up to + # "--" stripped off. Then call the given program with --complete on + # the remainder. Treat the 'para' shortcut as if no double-dash had + # been given. i.e. only strip off the first word. + if [[ "${COMP_WORDS[0]}" == 'para' ]] || ((ddpos < 0)); then + n=0 else - COMP_POINT=0 + n=$ddpos fi - COMP_LINE="$line" - #echo "line: $COMP_LINE, point: $COMP_POINT" - export COMP_LINE COMP_POINT - result=($($prg --complete)) - - # the last line of the output contains the options for compopt, - # prefixed with '-o='. - n=${#result[@]} - (($n == 0)) && return # oops, $prg did not write any output - let n-- - opts="${result[$n]}" - result[$n]= - opts="${opts#-o=}" + shopt -pq extglob || old_extglob=0 + shopt -s extglob + list=${COMP_LINE:0:$COMP_POINT} + while ((n >= 0)); do + list=${list##+([^ ])} # remove argument + list=${list##*( )} # remove leading whitespace + let n-- + done + ((!old_extglob)) && shopt -u extglob + # prg relies on COMP_POINT and COMP_LINE, so adjust these + export COMP_POINT=${#list} + export COMP_LINE=$list + COMPREPLY=($($prg --complete 2>/dev/null)) + ((${#COMPREPLY[@]} == 0)) && return # oops, $prg did not write any output + # The last line of the output contains the options for compopt, prefixed + # with '-o='. + n=$((${#COMPREPLY[@]} - 1)) + OLD_IFS=$IFS IFS=',' - compopt +o nospace - for opt in $opts; do - #echo "opt: $opt" + for opt in ${COMPREPLY[$n]#-o=}; do case "$opt" in - filenames) compopt -o filenames;; + filenames) compopt -o default;; nospace) compopt -o nospace;; esac done - IFS="$OLD_IFS" - COMPREPLY=(${result[@]}) + IFS=$OLD_IFS + unset COMPREPLY[$n] + COMPREPLY=($(compgen -W "${COMPREPLY[*]}" -- "$cur")) } _para_audioc() { _para_complete para_audioc } -complete -F _para_audioc para_audioc _para_client() { _para_complete para_client } -complete -o default -o nospace -F _para_client para_client -complete -o default -o nospace -F _para_client para +complete -F _para_audioc para_audioc +complete -F _para_client para_client para -- 2.39.5