From 7d6fe8b1886f902f8ffbec2a9985fae9f91121cb Mon Sep 17 00:00:00 2001 From: Tom Ryder Date: Sun, 2 Dec 2018 17:59:29 +1300 Subject: Overhaul Bash completion scripts Some general changes: * Apply case sensitivity switching in more contexts, using a dynamically loaded helper function * Use array counters for appending to COMPREPLY where possible * Lots more short-circuiting to limit structural depth These changes are expansive and there will definitely be bugs. --- bash/bash_completion.d/_abook_addresses.bash | 31 +++---- .../bash_completion.d/_completion_ignore_case.bash | 12 +++ bash/bash_completion.d/_ssh_config_hosts.bash | 33 ++----- bash/bash_completion.d/bd.bash | 39 ++++---- bash/bash_completion.d/eds.bash | 52 +++++------ bash/bash_completion.d/find.bash | 64 +++---------- bash/bash_completion.d/gpg.bash | 21 +++-- bash/bash_completion.d/keep.bash | 45 ++++----- bash/bash_completion.d/mail.bash | 4 +- bash/bash_completion.d/make.bash | 93 +++++++++++-------- bash/bash_completion.d/man.bash | 95 ++++++++++--------- bash/bash_completion.d/mex.bash | 60 +++++++++--- bash/bash_completion.d/openssl.bash | 44 +++++---- bash/bash_completion.d/pass.bash | 62 +++++++------ bash/bash_completion.d/path.bash | 101 +++++++++++---------- bash/bash_completion.d/sd.bash | 60 ++++-------- bash/bash_completion.d/td.bash | 51 +++++------ 17 files changed, 437 insertions(+), 430 deletions(-) create mode 100644 bash/bash_completion.d/_completion_ignore_case.bash (limited to 'bash') diff --git a/bash/bash_completion.d/_abook_addresses.bash b/bash/bash_completion.d/_abook_addresses.bash index e1a94bc7..6f7e226f 100644 --- a/bash/bash_completion.d/_abook_addresses.bash +++ b/bash/bash_completion.d/_abook_addresses.bash @@ -1,32 +1,29 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Email addresses from abook(1) _abook_addresses() { # Needs abook(1) hash abook 2>/dev/null || return - # Iterate through words produced by subshell - local word - while read -r word ; do - [[ -n $word ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$word + # Iterate through completions produced by subshell + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp done < <( - # Set case-insensitive matching if appropriate - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocasematch 2>/dev/null - break - ;; - esac - done < <(bind -v) + # Make matches behave appropriately + if _completion_ignore_case ; then + shopt -s nocasematch 2>/dev/null + fi # Generate list of email addresses from abook(1) while IFS=$'\t' read -r address _ ; do case $address in - ("$2"*) - printf '%s\n' "$address" - ;; + ("$2"*) printf '%s\n' "$address" ;; esac done < <(abook --mutt-query \@) ) diff --git a/bash/bash_completion.d/_completion_ignore_case.bash b/bash/bash_completion.d/_completion_ignore_case.bash new file mode 100644 index 00000000..fe8208fc --- /dev/null +++ b/bash/bash_completion.d/_completion_ignore_case.bash @@ -0,0 +1,12 @@ +# Return whether to ignore case for filename completion +_completion_ignore_case() { + + # Check Readline settings for case-insensitive matching + while read -r _ set ; do + [[ $set == 'completion-ignore-case on' ]] || continue + return 0 + done < <(bind -v) + + # Didn't find it, stay case-sensitive + return 1 +} diff --git a/bash/bash_completion.d/_ssh_config_hosts.bash b/bash/bash_completion.d/_ssh_config_hosts.bash index 0c1eb379..3f937a2a 100644 --- a/bash/bash_completion.d/_ssh_config_hosts.bash +++ b/bash/bash_completion.d/_ssh_config_hosts.bash @@ -1,45 +1,28 @@ # Complete ssh_config(5) hostnames _ssh_config_hosts() { - # Don't complete anything that wouldn't be in a valid hostname - case $2 in - *[!a-zA-Z0-9.-]*) return 1 ;; - esac - # Iterate through words from a subshell - while read -r word ; do - [[ -n $word ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$word + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp done < <( - # Check bind settings to see if we should match case insensitively - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocasematch 2>/dev/null - break - ;; - esac - done < <(bind -v) - # Iterate through SSH client config paths for config in "$HOME"/.ssh/config /etc/ssh/ssh_config ; do [[ -e $config ]] || continue - # Read Host options and their first value from file + # Read 'Host' options and their first value from file while read -r option value _ ; do [[ $option == Host ]] || continue # Check host value case $value in - - # Don't complete with wildcard characters + # No empties + ('') ;; + # No wildcards (*'*'*) ;; - # Found a match; print it - ("$2"*) - printf '%s\n' "$value" - ;; + ("$2"*) printf '%s\n' "$value" ;; esac done < "$config" diff --git a/bash/bash_completion.d/bd.bash b/bash/bash_completion.d/bd.bash index 59f4718f..09134e6a 100644 --- a/bash/bash_completion.d/bd.bash +++ b/bash/bash_completion.d/bd.bash @@ -1,15 +1,15 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Completion setup for bd() _bd() { - # The function accepts only one argument, so it doesn't make sense to - # complete anywhere else - ((COMP_CWORD == 1)) || return - - # Iterate through slash-delimited matching parent path elements, as piped - # in by the subshell - local ci word - while IFS= read -rd/ word ; do - COMPREPLY[ci++]=$word + # Iterate through completions produced by subshell + local ci comp + while IFS= read -d / -r comp ; do + COMPREPLY[ci++]=$comp done < <( # Build an array of path nodes, leaf to root @@ -22,29 +22,22 @@ _bd() { done # Continue if we have at least two nodes, counting the leaf - ((${#nodes} > 1)) || return + ((${#nodes[@]} > 1)) || return - # Shift off the leaf, since it's not meaningful to go "back to" the + # Shift off the leaf, since it is not meaningful to go "back to" the # current directory nodes=("${nodes[@]:1}") - # Turn on case-insensitive matching, if configured to do so - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocasematch 2>/dev/null - break - ;; - esac - done < <(bind -v) + # Make matching behave appropriately + if _completion_ignore_case ; then + shopt -s nocasematch 2>/dev/null + fi # Iterate through the nodes and print the ones that match the word # being completed, with a trailing slash as terminator for node in "${nodes[@]}" ; do case $node in - ("$2"*) - printf '%s/' "$node" - ;; + ("$2"*) printf '%s/' "$node" ;; esac done ) diff --git a/bash/bash_completion.d/eds.bash b/bash/bash_completion.d/eds.bash index da6bb879..371962ca 100644 --- a/bash/bash_completion.d/eds.bash +++ b/bash/bash_completion.d/eds.bash @@ -1,38 +1,34 @@ -# Complete args to eds(1df) with existing executables in $EDSPATH, defaulting -# to ~/.local/bin +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + +# Complete args to eds(1df) _eds() { - local edspath - edspath=${EDSPATH:-"$HOME"/.local/bin} - [[ -d $edspath ]] || return - local executable - while IFS= read -rd '' executable ; do - [[ -n $executable ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$executable + + # Iterate through completions produced by subshell + local ci comp + while IFS= read -d / -r comp ; do + COMPREPLY[ci++]=$comp done < <( - shopt -s dotglob nullglob - # Make globbing case-insensitive if appropriate - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocaseglob - break - ;; - esac - done < <(bind -v) + # Make globs expand appropriately + shopt -u dotglob + shopt -s nullglob + if _completion_ignore_case ; then + shopt -s nocaseglob + fi - declare -a files - files=("${EDSPATH:-"$HOME"/.local/bin}"/"$2"*) - declare -a executables - for file in "${files[@]}" ; do + # Iterate through files in local binaries directory + edspath=${EDSPATH:-"$HOME"/.local/bin} + for file in "$edspath"/"$2"* ; do + # Skip directories ! [[ -d $file ]] || continue - [[ -e $file ]] || continue + # Skip non-executable files [[ -x $file ]] || continue - executables[${#executables[@]}]=${file##*/} + # Print entry, null-terminated + printf '%q\0' "${file##*/}" done - - # Print quoted entries, null-delimited - printf '%q\0' "${executables[@]}" ) } complete -F _eds eds diff --git a/bash/bash_completion.d/find.bash b/bash/bash_completion.d/find.bash index 7593c594..f87029e7 100644 --- a/bash/bash_completion.d/find.bash +++ b/bash/bash_completion.d/find.bash @@ -1,41 +1,13 @@ -# compopt requires Bash >=4.0, and I don't think it's worth making a compatible -# version -((BASH_VERSINFO[0] >= 4)) || return - # Semi-intelligent completion for find(1); nothing too crazy _find() { - # Backtrack through words so far; if none of them look like options, we're - # still completing directory names - local i - local -i opts - for ((i = COMP_CWORD; i >= 0; i--)) ; do - case ${COMP_WORDS[i]} in - -*) - opts=1 - break - ;; - esac - done - if ! ((opts)) ; then - compopt -o dirnames - return - fi - - # For the rest of this, if we end up with an empty COMPREPLY, we should - # just do what Bash would normally do - compopt -o bashdefault -o default - - # Iterate through whatever the subshell gives us; don't add blank items, though - local item - while read -r item ; do - [[ -n $item ]] || continue - COMPREPLY+=("$item") + # Iterate through completions produced by subshell + local ci comp + while IFS= read -r comp ; do + COMPREPLY[ci++]=$comp done < <( - # If the word being completed starts with a dash, just complete it as - # an option; crude, but simple, and will be right the vast majority of - # the time + # Complete POSIX-specified options case $2 in (-*) compgen -W ' @@ -59,33 +31,27 @@ _find() { -user -xdev ' -- "$2" + return ;; esac - # Otherwise, look at the word *before* this one to figure out what to + # Look at the word *before* this one to figure out what to # complete case $3 in # Args to -exec and -execdir should be commands - (-exec|-execdir) - compgen -A command -- "$2" - ;; - - # Args to -group should complete group names - (-group) - compgen -A group -- "$2" - ;; + (-exec|-execdir) compgen -A command -- "$2" ;; # Legal POSIX flags for -type - (-type) - compgen -W 'b c d f l p s' -- "$2" - ;; + (-type) compgen -W 'b c d f l p s' -- "$2" ;; + + # Args to -group should complete group names + (-group) compgen -A group -- "$2" ;; # Args to -user should complete usernames - (-user) - compgen -A user -- "$2" - ;; + (-user) compgen -A user -- "$2" ;; + esac ) } -complete -F _find find +complete -F _find -o bashdefault -o default find diff --git a/bash/bash_completion.d/gpg.bash b/bash/bash_completion.d/gpg.bash index bfa2d1a9..c6f92676 100644 --- a/bash/bash_completion.d/gpg.bash +++ b/bash/bash_completion.d/gpg.bash @@ -1,7 +1,7 @@ # Completion for gpg(1) with long options _gpg() { - # Bail if no gpg(1) + # Needs gpg(1) hash gpg 2>/dev/null || return # Bail if not completing an option @@ -11,13 +11,16 @@ _gpg() { esac # Generate completion reply from gpg(1) options - local option - while read -r option ; do - case $option in - "$2"*) - COMPREPLY[${#COMPREPLY[@]}]=$option - ;; - esac - done < <(gpg --dump-options 2>/dev/null) + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp + done < <( + gpg --dump-options 2>/dev/null | + while read -r option ; do + case $option in + ("$2"*) printf '%s\n' "$option" ;; + esac + done + ) } complete -F _gpg -o bashdefault -o default gpg diff --git a/bash/bash_completion.d/keep.bash b/bash/bash_completion.d/keep.bash index 6829db9c..c7144684 100644 --- a/bash/bash_completion.d/keep.bash +++ b/bash/bash_completion.d/keep.bash @@ -1,3 +1,8 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Complete calls to keep() with variables and functions, or if -d is given with # stuff that's already kept _keep() { @@ -7,16 +12,17 @@ _keep() { mode=keep if ((COMP_CWORD > 1)) ; then case ${COMP_WORDS[1]} in + # Help; no completion -h) return 1 ;; + # Deleting; change mode -d) mode=delete ;; esac fi # Collect words from an appropriate type of completion - local word - while read -r word ; do - [[ -n $word ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$word + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp done < <( # Switch on second word; is it a -d option? @@ -24,32 +30,27 @@ _keep() { # Keepable names: all functions and variables (keep) - compgen -A function -A variable \ - -- "$2" + compgen -A function -A variable -- "$2" ;; # Kept names: .bash-suffixed names in keep dir (delete) + # Make globs behave correctly + shopt -u dotglob shopt -s nullglob - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocaseglob - break - ;; - esac - done < <(bind -v) + if _completion_ignore_case ; then + shopt -s nocaseglob + fi # Build list of kept names - dir=${BASHKEEP:-"$HOME"/.bashkeep.d} - cword=$2 - kept=("$dir"/"$cword"*.bash) - kept=("${kept[@]##*/}") - kept=("${kept[@]%.bash}") - - # Print kept names - printf '%s\n' "${kept[@]}" + bashkeep=${BASHKEEP:-"$HOME"/.bashkeep.d} + for keep in "$bashkeep"/"$2"*.bash ; do + ! [[ -d $keep ]] || continue + keep=${keep##*/} + keep=${keep%.bash} + printf '%s\n' "$keep" + done ;; esac ) diff --git a/bash/bash_completion.d/mail.bash b/bash/bash_completion.d/mail.bash index 5d1cdec0..0f6d60d4 100644 --- a/bash/bash_completion.d/mail.bash +++ b/bash/bash_completion.d/mail.bash @@ -1,5 +1,5 @@ # Completion for mail(1) with abook(1) email addresses -if ! declare -F _text_filenames >/dev/null ; then - source "$HOME"/.bash_completion.d/_text_filenames.bash +if ! declare -F _abook_addresses >/dev/null ; then + source "$HOME"/.bash_completion.d/_abook_addresses.bash fi complete -F _abook_addresses -o bashdefault -o default mail diff --git a/bash/bash_completion.d/make.bash b/bash/bash_completion.d/make.bash index 8ca36529..2527d145 100644 --- a/bash/bash_completion.d/make.bash +++ b/bash/bash_completion.d/make.bash @@ -1,3 +1,8 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Completion setup for Make, completing targets _make() { @@ -5,48 +10,56 @@ _make() { # first, then "Makefile"). You may want to add "GNU-makefile" after this. local mf for mf in makefile Makefile '' ; do - [[ -e $mf ]] || continue - break + ! [[ -e $mf ]] || break done [[ -n $mf ]] || return - # Iterate through the Makefile, line by line - local line - while IFS= read -r line ; do - case $line in - - # We're looking for targets but not variable assignments - \#*) ;; - $'\t'*) ;; - *:=*) ;; - *:*) - - # Break the target up with space delimiters - local -a targets - IFS=' ' read -rd '' -a targets < \ - <(printf '%s\0' "${line%%:*}") - - # Iterate through the targets and add suitable ones - local target - for target in "${targets[@]}" ; do - case $target in - - # Don't complete special targets beginning with a - # period - .*) ;; - - # Don't complete targets with names that have - # characters outside of the POSIX spec (plus slashes) - *[^[:word:]./-]*) ;; - - # Add targets that match what we're completing - "$2"*) - COMPREPLY[${#COMPREPLY[@]}]=$target - ;; - esac - done - ;; - esac - done < "$mf" + # Iterate through completions produced by subshell + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp + done < <( + while IFS= read -r line ; do + + # Match expected format + case $line in + # Has no equals sign anywhere + (*=*) continue ;; + # First char not a tab + ($'\t'*) continue ;; + # Has a colon on the line + (*:*) ;; + # Skip anything else + (*) continue ;; + esac + + # Break the target up with space delimiters + local -a targets + IFS=' ' read -a targets -r \ + < <(printf '%s\n' "${line%%:*}") + + # Short-circuit if there are no targets + ((${#targets[@]})) || exit + + # Make matches behave correctly + if _completion_ignore_case ; then + shopt -s nocasematch 2>/dev/null + fi + + # Examine each target for completion suitability + local target + for target in "${targets[@]}" ; do + case $target in + # Not .PHONY, .POSIX etc + (.*) ;; + # Nothing with metacharacters + (*[^[:word:]./-]*) ;; + # Match! + ("$2"*) printf '%s\n' "$target" ;; + esac + done + + done < "$mf" + ) } complete -F _make -o bashdefault -o default make diff --git a/bash/bash_completion.d/man.bash b/bash/bash_completion.d/man.bash index b5ecaa3e..274f663a 100644 --- a/bash/bash_completion.d/man.bash +++ b/bash/bash_completion.d/man.bash @@ -1,39 +1,32 @@ # Autocompletion for man(1) _man() { - # Don't even bother if we don't have manpath(1) - hash manpath 2>/dev/null || return - - # Don't bother if the word has slashes in it, the user is probably trying - # to complete an actual path + # Don't interfere with a user typing a path case $2 in */*) return 1 ;; esac - # If this is the second word, and the previous word started with a number, - # we'll assume that's the section to search - local section subdir - if ((COMP_CWORD > 1)) ; then - case $3 in - [0-9]*) - section=$3 - subdir=man${section%%[^0-9]*} - ;; - esac - fi + # If previous word started with a number, we'll assume that's a section to + # search + case $3 in + [0-9]*) sec=$3 ;; + esac + + # Cut completion short if we have neither section nor word; there will + # probably be too many results + [[ -n $sec ]] || [[ -n $2 ]] || return # Read completion results from a subshell and add them to the COMPREPLY # array individually - local page - while IFS= read -rd '' page ; do - [[ -n $page ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$page + local ci comp + while IFS= read -d '' -r comp ; do + COMPREPLY[ci++]=$comp done < <( # Do not return dotfiles, give us extended globbing, and expand empty # globs to just nothing shopt -u dotglob - shopt -s extglob nullglob + shopt -s nullglob # Make globbing case-insensitive if appropriate while read -r _ setting ; do @@ -45,34 +38,54 @@ _man() { esac done < <(bind -v) - # Break manpath(1) output into an array of paths - declare -a manpaths - IFS=: read -a manpaths -r < <(manpath 2>/dev/null) - - # Iterate through the manual page paths and add every manual page we find - declare -a pages - for manpath in "${manpaths[@]}" ; do - [[ -n $manpath ]] || continue - if [[ -n $section ]] ; then - for page in \ - "$manpath"/"$subdir"/"$2"*."$section"?(.[glx]z|.bz2|.lzma|.Z) - do - pages[${#pages[@]}]=$page + # Figure out the manual paths to search + if hash amanpath 2>/dev/null ; then + + # manpath(1) exists, run it to find what to search + IFS=: read -a manpaths -r \ + < <(manpath 2>/dev/null) + else + + # Fall back on some typical paths + manpaths=( \ + "$HOME"/.local/man \ + "$HOME"/.local/share/man \ + /usr/man \ + /usr/share/man \ + /usr/local/man \ + /usr/local/share/man \ + ) + fi + + # Add pages from each manual directory + local pages pi + for mp in "${manpaths[@]}" ; do + [[ -n $mp ]] || continue + + # Which pattern? Depends on section specification + if [[ -n $sec ]] ; then + + # Section requested; quoted value in glob + for page in "$mp"/man"${sec%%[!0-9]*}"/"$2"*."$sec"* ; do + pages[pi++]=${page##*/} done else - for page in "$manpath"/man[0-9]*/"$2"*.* ; do - pages[${#pages[@]}]=$page + + # No section; + for page in "$mp"/man[0-9]*/"$2"*.[0-9]* ; do + pages[pi++]=${page##*/} done fi done - # Strip paths, .gz suffixes, and finally .
suffixes - pages=("${pages[@]##*/}") - pages=("${pages[@]%.@([glx]z|bz2|lzma|Z)}") + # Bail if there are no pages + ((pi)) || exit + + # Strip section suffixes pages=("${pages[@]%.[0-9]*}") - # Print quoted entries, null-delimited - printf '%q\0' "${pages[@]}" + # Print entries, null-delimited + printf '%s\0' "${pages[@]}" ) } complete -F _man -o bashdefault -o default man diff --git a/bash/bash_completion.d/mex.bash b/bash/bash_completion.d/mex.bash index bc3d2c7b..b1e0e1a7 100644 --- a/bash/bash_completion.d/mex.bash +++ b/bash/bash_completion.d/mex.bash @@ -1,16 +1,54 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Completion setup for mex(1df), completing non-executable files in $PATH _mex() { - local -a path - IFS=: read -ra path < <(printf '%s\n' "$PATH") - local dir name - for dir in "${path[@]}" ; do - [[ -d $dir ]] || continue - for name in "$dir"/* ; do - [[ -e $name ]] || continue - ! [[ -d $name ]] || continue - ! [[ -x $name ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=${name##*/} + + # Iterate through completions produced by subshell + local ci comp + while IFS= read -d / -r comp ; do + COMPREPLY[ci++]=$comp + done < <( + + # Make globs expand appropriately + shopt -u dotglob + shopt -s nullglob + if _completion_ignore_case ; then + shopt -s nocaseglob + fi + + # Break $PATH up into an array + declare -a paths + IFS=: read -a paths -r \ + < <(printf '%s\n' "$PATH") + + # Iterate through each path, collecting non-executable filenames + for path in "${paths[@]}" ; do + for name in "$path"/"$2"* ; do + + # Skip anything that is not a plain file + [[ -f $name ]] || continue + # Skip files that are already executable + ! [[ -x $name ]] || continue + + # Chop off leading path + name=${name##*/} + + # Skip certain filename patterns + case $name in + # DOS batch file + (*.bat) continue ;; + # README files + (README*) continue ;; + esac + + # Print name of the file + printf '%s/' "${name##*/}" + + done done - done + ) } complete -F _mex mex diff --git a/bash/bash_completion.d/openssl.bash b/bash/bash_completion.d/openssl.bash index 3396eb9f..1cb4bd07 100644 --- a/bash/bash_completion.d/openssl.bash +++ b/bash/bash_completion.d/openssl.bash @@ -1,26 +1,32 @@ # Some simple completion for openssl(1ssl) _openssl() { + # Needs openssl(1ssl) + hash openssl 2>/dev/null || return + # Only complete the first word: OpenSSL subcommands - case $COMP_CWORD in - 1) - while read -r subcmd ; do - case $subcmd in - '') ;; - "$2"*) - COMPREPLY[${#COMPREPLY[@]}]=$subcmd - ;; - esac - done < <( - for arg in \ - list-cipher-commands \ - list-standard-commands \ - list-message-digest-commands ; do - printf '%s\n' "$arg" - openssl "$arg" + ((COMP_CWORD == 1)) || return + + # Iterate through completions produced by subshell + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp + done < <( + + # Run each of the command-listing commands; read each line into an + # array of subcommands (they are printed as a table) + for list in commands digest-commands cipher-commands ; do + openssl list -"$list" + done | { + declare -a subcmds + while read -a subcmds -r ; do + for subcmd in "${subcmds[@]}" ; do + case $subcmd in + ("$2"*) printf '%s\n' "$subcmd" ;; + esac done - ) - ;; - esac + done + } + ) } complete -F _openssl -o bashdefault -o default openssl diff --git a/bash/bash_completion.d/pass.bash b/bash/bash_completion.d/pass.bash index af7926d9..5a6e0b6c 100644 --- a/bash/bash_completion.d/pass.bash +++ b/bash/bash_completion.d/pass.bash @@ -1,11 +1,11 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Custom completion for pass(1), because I don't like the one included with the # distribution -_pass() -{ - # If we can't read the password directory, just bail - local passdir - passdir=${PASSWORD_STORE_DIR:-"$HOME"/.password-store} - [[ -r $passdir ]] || return +_pass() { # Iterate through completions produced by subshell local ci comp @@ -13,35 +13,43 @@ _pass() COMPREPLY[ci++]=$comp done < <( - # Set shell options to expand globs the way we expect + # Make globs expand appropriately shopt -u dotglob shopt -s nullglob + if _completion_ignore_case ; then + shopt -s nocaseglob + fi + + # Set password store path + pass_dir=${PASSWORD_STORE_DIR:-"$HOME"/.password-store} - # Check Readline settings for case-insensitive matching - while read -r _ setting ; do - if [[ $setting == 'completion-ignore-case on' ]] ; then - shopt -s nocaseglob - break - fi - done < <(bind -v) + # Gather the entries + for entry in "$pass_dir"/"$2"*.gpg ; do + entries[ei++]=$entry + done - # Gather the entries, use ** for depth search if we can - entries=("$passdir"/"$2"*.gpg) + # Try to iterate into subdirs, use depth search with ** if available if shopt -s globstar 2>/dev/null ; then - entries=("${entries[@]}" "$passdir"/"$2"**/*.gpg) + for entry in "$pass_dir"/"$2"**/*.gpg ; do + entries[ei++]=$entry + done else - entries=("${entries[@]}" "$passdir"/"$2"*/*.gpg) + for entry in "$pass_dir"/"$2"*/*.gpg ; do + entries[ei++]=$entry + done fi - # Bail out if there are no entries - ((${#entries[@]})) || exit - - # Strip leading path and .gpg suffix from entry names - entries=("${entries[@]#"$passdir"/}") - entries=("${entries[@]%.gpg}") - - # Print entries, quoted and null-delimited - printf '%q\0' "${entries[@]}" + # Iterate through entries + for entry in "${entries[@]}" ; do + # Skip directories + ! [[ -d $entry ]] || continue + # Strip leading path + entry=${entry#"$pass_dir"/} + # Strip .gpg suffix + entry=${entry%.gpg} + # Print shell-quoted entry, null terminated + printf '%q\0' "$entry" + done ) } complete -F _pass pass diff --git a/bash/bash_completion.d/path.bash b/bash/bash_completion.d/path.bash index 8db6a74a..9234f132 100644 --- a/bash/bash_completion.d/path.bash +++ b/bash/bash_completion.d/path.bash @@ -1,3 +1,8 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Completion for path _path() { @@ -5,9 +10,9 @@ _path() { if ((COMP_CWORD == 1)) ; then # Complete operation as first word - local cmd - while read -r cmd ; do - COMPREPLY[${#COMPREPLY[@]}]=$cmd + local ci comp + while read -r comp ; do + COMPREPLY[ci++]=$comp done < <(compgen -W ' append check @@ -18,62 +23,60 @@ _path() { remove shift ' -- "$2") + return + fi # Complete with either directories or $PATH entries as all other words - else - case ${COMP_WORDS[1]} in + case ${COMP_WORDS[1]} in - # Complete with a directory - insert|append|check) - local dirname - while IFS= read -rd '' dirname ; do - [[ -n $dirname ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$dirname - done < <( + # Complete with a directory + insert|append|check) + local ci comp + while IFS= read -d '' -r comp ; do + COMPREPLY[ci++]=$comp + done < <( - # Set options to glob correctly - shopt -s dotglob nullglob + # Make globs expand appropriately + shopt -s dotglob nullglob + if _completion_ignore_case ; then + shopt -s nocaseglob + fi - # Make globbing case-insensitive if appropriate - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocaseglob - break - ;; - esac - done < <(bind -v) + # Print shell-quoted matching directories, null-terminated + for dir in "$2"*/ ; do + printf '%q\0' "${dir%/}" + done + ) + ;; - # Collect directory names, strip trailing slash - local -a dirnames - dirnames=("$2"*/) - dirnames=("${dirnames[@]%/}") + # Complete with directories from PATH + remove) + local ci comp + while IFS= read -d '' -r comp ; do + COMPREPLY[ci++]=$comp + done < <( - # Print quoted entries, null-delimited - printf '%q\0' "${dirnames[@]}" - ) - ;; + # Make matches work appropriately + if _completion_ignore_case ; then + shopt -s nocasematch 2>/dev/null + fi - # Complete with directories from PATH - remove) - local -a promptarr - IFS=: read -rd '' -a promptarr < \ - <(printf '%s\0' "$PATH") - local part - for part in "${promptarr[@]}" ; do - case $part in - "$2"*) - COMPREPLY[${#COMPREPLY[@]}]=$(printf '%q' "$part") - ;; + # Break PATH into parts + declare -a paths + IFS=: read -a paths -d '' -r \ + < <(printf '%s\0' "$PATH") + + # Print shell-quoted matching parts, null-terminated + for path in "${paths[@]}" ; do + case $path in + ("$2"*) printf '%q\0' "$path" ;; esac done - ;; + ) + ;; - # No completion - *) - return 1 - ;; - esac - fi + # No completion + *) return 1 ;; + esac } complete -F _path path diff --git a/bash/bash_completion.d/sd.bash b/bash/bash_completion.d/sd.bash index d6e93c78..66dea73b 100644 --- a/bash/bash_completion.d/sd.bash +++ b/bash/bash_completion.d/sd.bash @@ -1,52 +1,32 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Completion function for sd; any sibling directories, excluding the self _sd() { - # Only makes sense for the first argument - ((COMP_CWORD == 1)) || return - - # Current directory can't be root directory - case $PWD in - /) return 1 ;; - esac - # Build list of matching sibling directories - local dirname - while IFS= read -rd '' dirname ; do - [[ -n $dirname ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$dirname + local ci comp + while IFS= read -d / -r comp ; do + COMPREPLY[ci++]=$comp done < <( - # Set options to glob correctly + # Make globs expand appropriately shopt -s dotglob nullglob - - # Make globbing case-insensitive if appropriate - while read -r _ setting ; do - case $setting in - ('completion-ignore-case on') - shopt -s nocaseglob - break - ;; - esac - done < <(bind -v) - - # Collect directory names, strip leading ../ and trailing / - local -a dirnames - dirnames=(../"$2"*/) - dirnames=("${dirnames[@]#../}") - dirnames=("${dirnames[@]%/}") - - # Iterate again, but exclude the current directory this time - local -a sibs - local dirname - for dirname in "${dirnames[@]}" ; do - case $dirname in - "${PWD##*/}") ;; - *) sibs[${#sibs[@]}]=$dirname ;; + if _completion_ignore_case ; then + shopt -s nocaseglob + fi + + # Print matching sibling dirs that are not the current dir + for sibling in ../"$2"*/ ; do + sibling=${sibling%/} + sibling=${sibling#../} + case $sibling in + ("${PWD##*/}") ;; + (*) printf '%q/' "${sibling}" ;; esac done - - # Print quoted sibling directories, null-delimited - printf '%q\0' "${sibs[@]}" ) } complete -F _sd sd diff --git a/bash/bash_completion.d/td.bash b/bash/bash_completion.d/td.bash index 38dc51a3..f3735691 100644 --- a/bash/bash_completion.d/td.bash +++ b/bash/bash_completion.d/td.bash @@ -1,36 +1,31 @@ +# Load _completion_ignore_case helper function +if ! declare -F _completion_ignore_case >/dev/null ; then + source "$HOME"/.bash_completion.d/_completion_ignore_case.bash +fi + # Complete filenames for td(1df) _td() { - local dir - dir=${TODO_DIR:-"$HOME"/Todo} - local fn - while IFS= read -rd '' fn ; do - [[ -n $fn ]] || continue - COMPREPLY[${#COMPREPLY[@]}]=$fn - done < <( - shopt -s extglob nullglob - shopt -u dotglob - # Make globbing case-insensitive if appropriate; is there a cleaner way - # to find this value? - while read -r _ option value ; do - case $option in - (completion-ignore-case) - case $value in - (on) - shopt -s nocaseglob - break - ;; - esac - ;; - esac - done < <(bind -v) + # Iterate through completions produced by subshell + local ci comp + while IFS= read -d / -r comp ; do + COMPREPLY[ci++]=$comp + done < <( - declare -a fns - fns=("$dir"/"$2"*) - fns=("${fns[@]#"$dir"/}") + # Make globs expand appropriately + shopt -u dotglob + shopt -s nullglob + if _completion_ignore_case ; then + shopt -s nocaseglob + fi - # Print quoted entries, null-delimited - printf '%q\0' "${fns[@]}" + # Find and print matching file entries + for list in "${TODO_DIR:-"$HOME"/Todo}"/"$2"* ; do + # Skip directories + ! [[ -d $list ]] || continue + # Print entry, slash-terminated + printf '%q/' "${list##*/}" + done ) } complete -F _td td -- cgit v1.2.3