From dcfc80aaa112498498d93bae3ae0eb2088244c47 Mon Sep 17 00:00:00 2001 From: Tom Ryder Date: Wed, 21 Oct 2015 13:08:09 +1300 Subject: Tidy up completion considerably; no more compgen * Remove all instances of compgen; for filename completion it's quite broken as it relies on implicit wordsplitting in array context, and doesn't have an option to print with a null delimiter; replaced with manual for/while loops instead * Add IFS= to while/read loops over filenames * Use "dirname/s" instead of "dir/s" variables to avoid keyword collisions and for clarity * Remove some unnecessary variables * Use shorter syntax for loop exit conditions * Move completion options into functions where applicable rather than having them on the completion definition itself --- bash/bashrc.d/apf.bash | 2 +- bash/bashrc.d/bd.bash | 56 ++++++++------- bash/bashrc.d/cf.bash | 20 +++--- bash/bashrc.d/fnl.bash | 8 +-- bash/bashrc.d/ftp.bash | 25 ++++--- bash/bashrc.d/git.bash | 21 +++--- bash/bashrc.d/gpg.bash | 27 ++++---- bash/bashrc.d/make.bash | 20 +++--- bash/bashrc.d/mysql.bash | 53 +++++++++----- bash/bashrc.d/pass.bash | 33 ++++----- bash/bashrc.d/path.bash | 177 ++++++++++++++++++++++++++--------------------- bash/bashrc.d/sd.bash | 46 +++++++++--- bash/bashrc.d/ssh.bash | 25 ++++--- bash/bashrc.d/ud.bash | 51 +++++++++----- bash/bashrc.d/vr.bash | 4 +- 15 files changed, 325 insertions(+), 243 deletions(-) (limited to 'bash/bashrc.d') diff --git a/bash/bashrc.d/apf.bash b/bash/bashrc.d/apf.bash index af759577..01b38437 100644 --- a/bash/bashrc.d/apf.bash +++ b/bash/bashrc.d/apf.bash @@ -85,7 +85,7 @@ apf() { # Read all the null-delimited arguments from the file local -a args local arg - while read -d '' -r arg ; do + while IFS= read -d '' -r arg ; do args=("${args[@]}" "$arg") done < "$argfile" diff --git a/bash/bashrc.d/bd.bash b/bash/bashrc.d/bd.bash index 110fca06..e4a6738e 100644 --- a/bash/bashrc.d/bd.bash +++ b/bash/bashrc.d/bd.bash @@ -34,24 +34,24 @@ bd() { [[ $req != / ]] || req=${req%/} # What to do now depends on the request - local dir + local dirname case $req in # If no argument at all, just go up one level '') - dir=.. + dirname=.. ;; # Just go straight to the root or dot directories if asked /|.|..) - dir=$req + dirname=$req ;; # Anything else with a leading / needs to anchor to the start of the # path /*) - dir=$req - if [[ $PWD != "$dir"/* ]] ; then + dirname=$req + if [[ $PWD != "$dirname"/* ]] ; then printf 'bash: %s: Directory name not in path\n' \ "$FUNCNAME" >&2 return 1 @@ -59,13 +59,13 @@ bd() { ;; # In all other cases, iterate through the directory tree to find a - # match, or whittle the dir down to an empty string trying + # match, or whittle the dirname down to an empty string trying *) - dir=${PWD%/*} - while [[ -n $dir && $dir != */"$req" ]] ; do - dir=${dir%/*} + dirname=${PWD%/*} + while [[ -n $dirname && $dirname != */"$req" ]] ; do + dirname=${dirname%/*} done - if [[ -z $dir ]] ; then + if [[ -z $dirname ]] ; then printf 'bash: %s: Directory name not in path\n' \ "$FUNCNAME" >&2 return 1 @@ -74,26 +74,32 @@ bd() { esac # Try to change into the determined directory - builtin cd "${opts[@]}" -- "$dir" + builtin cd "${opts[@]}" -- "$dirname" } # Completion setup for bd _bd() { - local word - word=${COMP_WORDS[COMP_CWORD]} - - # Build a list of dirs in $PWD - local -a dirs - while read -d / -r dir ; do - if [[ -n $dir ]] ; then - dirs=("${dirs[@]}" "$dir") - fi - done < <(printf %s "$PWD") - - # Complete with matching dirs + + # The completions given are filenames and may require escaping compopt -o filenames - local IFS=$'\n' - COMPREPLY=( $(compgen -W "${dirs[*]}" -- "$word") ) + + # Only makes sense for the first argument + ((COMP_CWORD == 1)) || return 1 + + # Build a list of dirnames in $PWD + local -a dirnames + IFS=/ read -d '' -a dirnames < <(printf '%s\0' "${PWD#/}") + + # Remove the last element in the array (the current directory) + ((${#dirnames[@]})) || return 1 + dirnames=("${dirnames[@]:0:$((${#dirnames[@]}-1))}") + + # Add the matching dirnames to the reply + local dirname + for dirname in "${dirnames[@]}" ; do + [[ $dirname == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$dirname") + done } complete -F _bd bd diff --git a/bash/bashrc.d/cf.bash b/bash/bashrc.d/cf.bash index be66ecea..4109fb18 100644 --- a/bash/bashrc.d/cf.bash +++ b/bash/bashrc.d/cf.bash @@ -1,30 +1,30 @@ # Count files cf() { - local dir + local dirname # Specify directory to check - dir=${1:-$PWD} + dirname=${1:-$PWD} # Error conditions - if [[ ! -e $dir ]] ; then + if [[ ! -e $dirname ]] ; then printf 'bash: %s: %s does not exist\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 - elif [[ ! -d $dir ]] ; then + elif [[ ! -d $dirname ]] ; then printf 'bash: %s: %s is not a directory\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 - elif [[ ! -r $dir ]] ; then + elif [[ ! -r $dirname ]] ; then printf 'bash: %s: %s is not readable\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi # Count files and print; use a subshell so options are unaffected ( shopt -s dotglob nullglob - declare -a files=("$dir"/*) - printf '%d\t%s\n' "${#files[@]}" "$dir" + declare -a files=("$dirname"/*) + printf '%d\t%s\n' "${#files[@]}" "$dirname" ) } complete -A directory cf diff --git a/bash/bashrc.d/fnl.bash b/bash/bashrc.d/fnl.bash index 9f894654..efdaa1c5 100644 --- a/bash/bashrc.d/fnl.bash +++ b/bash/bashrc.d/fnl.bash @@ -23,20 +23,20 @@ fnl() { fi # Create a temporary directory or bail - local template dir + local template dirname template=$FUNCNAME.$1.XXXXX - if ! dir=$(mktemp -dt -- "$template") ; then + if ! dirname=$(mktemp -dt -- "$template") ; then return fi # Run the command and save its exit status local ret - "$@" >"$dir"/stdout 2>"$dir"/stderr + "$@" >"$dirname"/stdout 2>"$dirname"/stderr ret=$? # Note these are *not* local variables # shellcheck disable=SC2034 - fnl_stdout=$dir/stdout fnl_stderr=$dir/stderr + fnl_stdout=$dirname/stdout fnl_stderr=$dirname/stderr declare -p fnl_std{out,err} # Return the exit status of the command, not the declare builtin diff --git a/bash/bashrc.d/ftp.bash b/bash/bashrc.d/ftp.bash index b8e8c55e..f046a683 100644 --- a/bash/bashrc.d/ftp.bash +++ b/bash/bashrc.d/ftp.bash @@ -1,14 +1,13 @@ # Completion for ftp with .netrc machines _ftp() { - local word - word=${COMP_WORDS[COMP_CWORD]} + + # Do default completion if no results + compopt -o default # Bail if the .netrc file is illegible local netrc netrc=$HOME/.netrc - if [[ ! -r $netrc ]] ; then - return 1 - fi + [[ -r $netrc ]] || return 1 # Tokenize the file local -a tokens @@ -16,19 +15,23 @@ _ftp() { # Iterate through tokens and collect machine names local -a machines - local -i machine + local -i nxm local token for token in "${tokens[@]}" ; do - if ((machine)) ; then + if ((nxm)) ; then machines=("${machines[@]}" "$token") - machine=0 + nxm=0 elif [[ $token == machine ]] ; then - machine=1 + nxm=1 fi done # Generate completion reply - COMPREPLY=( $(compgen -W "${machines[*]}" -- "$word") ) + local machine + for machine in "${machines[@]}" ; do + [[ $machine == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$machine") + done } -complete -F _ftp -o default ftp +complete -F _ftp ftp diff --git a/bash/bashrc.d/git.bash b/bash/bashrc.d/git.bash index 5965607c..182cef46 100644 --- a/bash/bashrc.d/git.bash +++ b/bash/bashrc.d/git.bash @@ -1,27 +1,22 @@ # Completion for git local branch names _git() { - # Bail if not a git repo (or no git!) - if ! git rev-parse --git-dir >/dev/null 2>&1 ; then - return 1 - fi + # Use default completion if no matches + compopt -o default - # Get current and previous word - local word first - word=${COMP_WORDS[COMP_CWORD]} - first=${COMP_WORDS[1]} + # Bail if not a git repo (or no git!) + git rev-parse --git-dir >/dev/null 2>&1 || return 1 # Switch on the previous word - case $first in + case ${COMP_WORDS[1]} in # If the first word is appropriate, complete with branch/tag names checkout|merge|rebase) - local -a branches local branch while read -r branch ; do - branches=("${branches[@]}" "${branch##*/}") + [[ $branch == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$branch") done < <(git for-each-ref refs/{heads,tags} 2>/dev/null) - COMPREPLY=( $(compgen -W "${branches[*]}" -- "$word") ) return ;; @@ -31,5 +26,5 @@ _git() { ;; esac } -complete -F _git -o default git +complete -F _git git diff --git a/bash/bashrc.d/gpg.bash b/bash/bashrc.d/gpg.bash index 83bda3d7..3f01db64 100644 --- a/bash/bashrc.d/gpg.bash +++ b/bash/bashrc.d/gpg.bash @@ -14,23 +14,22 @@ gpg() { # Completion for gpg with long options _gpg() { - local word - word=${COMP_WORDS[COMP_CWORD]} + + # Complete with directories/files if no matches + compopt -o default # Bail if no gpg(1) - if ! hash gpg 2>/dev/null ; then - return 1 - fi + hash gpg 2>/dev/null || return 1 - # Bail if word doesn't start with two dashes - if [[ $word != --* ]] ; then - return 1 - fi + # Bail if not completing an option + [[ ${COMP_WORDS[COMP_CWORD]} == --* ]] || return 1 - # Generate completion reply - COMPREPLY=( $(compgen -W \ - "$(gpg --dump-options 2>/dev/null)" \ - -- "$word") ) + # Generate completion reply from gpg(1) options + local option + while read -r option ; do + [[ $option == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$option") + done < <(gpg --dump-options 2>/dev/null) } -complete -F _gpg -o default gpg +complete -F _gpg gpg diff --git a/bash/bashrc.d/make.bash b/bash/bashrc.d/make.bash index df4c0f56..216894fd 100644 --- a/bash/bashrc.d/make.bash +++ b/bash/bashrc.d/make.bash @@ -1,17 +1,16 @@ # Completion setup for Make, completing targets _make() { - local word - word=${COMP_WORDS[COMP_CWORD]} - # Quietly bail if no legible Makefile - if [[ ! -r Makefile ]] ; then - return 1 - fi + # Do default completion if no matches + compopt -o default + + # Bail if no legible Makefile + [[ ! -r Makefile ]] || return 1 # Build a list of targets by parsing the Makefile local -a targets tokens local target line - while read -r line ; do + while read -r line ; do if [[ $line == *:* ]] ; then target=$line target=${target%%:*} @@ -25,7 +24,10 @@ _make() { done < Makefile # Complete with matching targets - COMPREPLY=( $(compgen -W "${targets[*]}" -- "$word") ) + for target in "${targets[@]}" ; do + [[ $target == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$target") + done } -complete -F _make -o default make +complete -F _make make diff --git a/bash/bashrc.d/mysql.bash b/bash/bashrc.d/mysql.bash index 5ef6f67c..33231c13 100644 --- a/bash/bashrc.d/mysql.bash +++ b/bash/bashrc.d/mysql.bash @@ -23,26 +23,41 @@ mysql() { # Completion setup for MySQL for configured databases _mysql() { - local word - word=${COMP_WORDS[COMP_CWORD]} - - # Check directory exists and has at least one .cnf file - local dir - dir=$HOME/.mysql - if [[ ! -d $dir ]] || ( - shopt -s nullglob dotglob - declare -a files=("$dir"/*.cnf) - ((! ${#files[@]})) - ) ; then - return 1 - fi + + # The completed results are filenames, and if there are no matches, do + # default completion + compopt -o filenames -o default + + # Only makes sense for first argument + ((COMP_CWORD == 1)) || return 1 + + # Bail if directory doesn't exist + local dirname + dirname=$HOME/.mysql + [[ -d $dirname ]] || return 1 # Return the names of the .cnf files sans prefix as completions - local -a items - items=("$dir"/*.cnf) - items=("${items[@]##*/}") - items=("${items[@]%%.cnf}") - COMPREPLY=( $(compgen -W "${items[*]}" -- "$word") ) + local db + while IFS= read -d '' -r db ; do + [[ $db == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$db") + done < <( + + # Set options so that globs expand correctly + shopt -s dotglob nullglob + + # Collect all the config file names, strip off leading path and .cnf + local -a cnfs + cnfs=("$dirname"/*.cnf) + cnfs=("${cnfs[@]#$dirname/}") + cnfs=("${cnfs[@]%.cnf}") + + # Bail if no files to prevent empty output + ((${#cnfs[@]})) || exit 1 + + # Print the conf names, null-delimited + printf '%s\0' "${cnfs[@]}" + ) } -complete -F _mysql -o default mysql +complete -F _mysql mysql diff --git a/bash/bashrc.d/pass.bash b/bash/bashrc.d/pass.bash index 8513e5dd..e8748118 100644 --- a/bash/bashrc.d/pass.bash +++ b/bash/bashrc.d/pass.bash @@ -1,7 +1,5 @@ -# Requires Bash >= 4.0 for dotglob, nullglob, and globstar -if ((BASH_VERSINFO[0] < 4)) ; then - return -fi +# Requires Bash >= 4.0 for globstar +((BASH_VERSINFO[0] >= 4)) || return # Custom completion for pass(1), because I don't like the one included with the # distribution @@ -10,25 +8,23 @@ _pass() # If we can't read the password directory, just bail local passdir passdir=${PASSWORD_STORE_DIR:-$HOME/.password-store} - if [[ ! -r $passdir ]] ; then - return 1 - fi + [[ -r $passdir ]] || return 1 # Iterate through list of .gpg paths, extension stripped, null-delimited, # and filter them down to the ones matching the completing word (compgen # doesn't seem to do this properly with a null delimiter) - local word entry - word=${COMP_WORDS[COMP_CWORD]} - while read -d '' -r entry ; do - if [[ $entry == "$word"* ]] ; then - COMPREPLY=("${COMPREPLY[@]}" "$entry") - fi + local entry + while IFS= read -d '' -r entry ; do + [[ $entry == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + + # We have to use printf %q here to quote the entry, as it may include + # spaces or newlines, just like any filename + COMPREPLY=("${COMPREPLY[@]}" "$(printf %q "$entry")") done < <( # Set shell options to expand globs the way we expect shopt -u dotglob - shopt -s nullglob - shopt -s globstar + shopt -s globstar nullglob # Change into password directory, or bail cd -- "$passdir" 2>/dev/null || exit @@ -38,10 +34,11 @@ _pass() entries=(**/*.gpg) entries=("${entries[@]%.gpg}") + # Bail if no entries to prevent empty output + ((${#entries[@]})) || exit 1 + # Print all the entries, null-delimited - if ((${#entries[@]})) ; then - printf '%s\0' "${entries[@]}" - fi + printf '%s\0' "${entries[@]}" ) } complete -F _pass pass diff --git a/bash/bashrc.d/path.bash b/bash/bashrc.d/path.bash index e73e6b2f..c3e02627 100644 --- a/bash/bashrc.d/path.bash +++ b/bash/bashrc.d/path.bash @@ -22,7 +22,7 @@ USAGE: $FUNCNAME h[elp] Print this help message (also done if command not found) $FUNCNAME l[ist] - Print the current dirs in PATH, one per line (default command) + Print the current directories in PATH, one per line (default command) $FUNCNAME i[nsert] DIR Add a directory to the front of PATH, checking for existence and uniqueness $FUNCNAME a[ppend] DIR @@ -52,30 +52,30 @@ EOF insert|i) local -a patharr IFS=: read -a patharr < <(printf '%s\n' "$PATH") - local dir - dir=$1 - [[ $dir == / ]] || dir=${dir%/} - if [[ -z $dir ]] ; then + local dirname + dirname=$1 + [[ $dirname == / ]] || dirname=${dirname%/} + if [[ -z $dirname ]] ; then printf 'bash: %s: need a directory path to insert\n' \ "$FUNCNAME" >&2 return 1 fi - if [[ ! -d $dir ]] ; then + if [[ ! -d $dirname ]] ; then printf 'bash: %s: %s not a directory\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - if [[ $dir == *:* ]] ; then + if [[ $dirname == *:* ]] ; then printf 'bash: %s: Cannot add insert directory %s with colon in name\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - if path check "$dir" ; then + if path check "$dirname" ; then printf 'bash: %s: %s already in PATH\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - patharr=("$dir" "${patharr[@]}") + patharr=("$dirname" "${patharr[@]}") path set "${patharr[@]}" ;; @@ -83,30 +83,30 @@ EOF append|add|a) local -a patharr IFS=: read -a patharr < <(printf '%s\n' "$PATH") - local dir - dir=$1 - [[ $dir == / ]] || dir=${dir%/} - if [[ -z $dir ]] ; then + local dirname + dirname=$1 + [[ $dirname == / ]] || dirname=${dirname%/} + if [[ -z $dirname ]] ; then printf 'bash: %s: need a directory path to append\n' \ "$FUNCNAME" >&2 return 1 fi - if [[ ! -d $dir ]] ; then + if [[ ! -d $dirname ]] ; then printf 'bash: %s: %s not a directory\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - if [[ $dir == *:* ]] ; then + if [[ $dirname == *:* ]] ; then printf 'bash: %s: Cannot append directory %s with colon in name\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - if path check "$dir" ; then + if path check "$dirname" ; then printf 'bash: %s: %s already in PATH\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi - patharr=("${patharr[@]}" "$dir") + patharr=("${patharr[@]}" "$dirname") path set "${patharr[@]}" ;; @@ -114,23 +114,23 @@ EOF remove|rm|r) local -a patharr IFS=: read -a patharr < <(printf '%s\n' "$PATH") - local dir - dir=$1 - [[ $dir == / ]] || dir=${dir%/} - if [[ -z $dir ]] ; then + local dirname + dirname=$1 + [[ $dirname == / ]] || dirname=${dirname%/} + if [[ -z $dirname ]] ; then printf 'bash: %s: need a directory path to remove\n' \ "$FUNCNAME" >&2 return 1 fi - if ! path check "$dir" ; then + if ! path check "$dirname" ; then printf 'bash: %s: %s not in PATH\n' \ - "$FUNCNAME" "$dir" >&2 + "$FUNCNAME" "$dirname" >&2 return 1 fi local -a newpatharr local part for part in "${patharr[@]}" ; do - [[ $dir == "$part" ]] && continue + [[ $dirname == "$part" ]] && continue newpatharr=("${newpatharr[@]}" "$part") done path set "${newpatharr[@]}" @@ -139,9 +139,9 @@ EOF # Set the PATH to the given directories without checking existence or uniqueness set|s) local -a newpatharr - local dir - for dir ; do - newpatharr=("${newpatharr[@]}" "$dir") + local dirname + for dirname ; do + newpatharr=("${newpatharr[@]}" "$dirname") done PATH=$(IFS=: ; printf '%s' "${newpatharr[*]}") ;; @@ -150,17 +150,17 @@ EOF check|c) local -a patharr IFS=: read -a patharr < <(printf '%s\n' "$PATH") - local dir - dir=$1 - [[ $dir == / ]] || dir=${dir%/} - if [[ -z $dir ]] ; then + local dirname + dirname=$1 + [[ $dirname == / ]] || dirname=${dirname%/} + if [[ -z $dirname ]] ; then printf 'bash: %s: need a directory path to check\n' \ "$FUNCNAME" >&2 return 1 fi local part for part in "${patharr[@]}" ; do - if [[ $dir == "$part" ]] ; then + if [[ $dirname == "$part" ]] ; then return 0 fi done @@ -179,51 +179,68 @@ EOF # Completion for path _path() { - local word - word=${COMP_WORDS[COMP_CWORD]} - - # Complete operation as first word - if ((COMP_CWORD == 1)) ; then - COMPREPLY=( $(compgen -W \ - 'help list insert append remove set check' \ - -- "$word") ) - else - case ${COMP_WORDS[1]} in - # Complete with one directory - insert|i|append|add|a|check|c) - if ((COMP_CWORD == 2)) ; then + # What to do depends on which word we're completing + case $COMP_CWORD in + + # Complete operation as first word + 1) + for cmd in help list insert append remove set check ; do + [[ $cmd == "${COMP_REPLY[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$cmd") + done + ;; + + # Complete with either directories or $PATH entries as all other words + *) + case ${COMP_WORDS[1]} in + + # Complete with a directory + insert|i|append|add|a|check|c|set|s) compopt -o filenames - local IFS=$'\n' - COMPREPLY=( $(compgen -A directory -- "$word") ) - fi - ;; - - # Complete with any number of directories - set|s) - compopt -o filenames - local IFS=$'\n' - COMPREPLY=( $(compgen -A directory -- "$word") ) - ;; - - # Complete with directories from PATH - remove|rm|r) - local -a promptarr - IFS=: read -a promptarr < <(printf '%s\n' "$PATH") - local part - for part in "${promptarr[@]}" ; do - if [[ $part && $part == "$word"* ]] ; then + local dirname + while IFS= read -d '' -r dirname ; do + [[ $dirname == "${COMP_WORDS[COMP_CWORD]}"/* ]] \ + || continue + COMPREPLY=("${COMPREPLY[@]}" "$dirname") + done < <( + + # Set options to glob correctly + shopt -s dotglob nullglob + + # Collect directory names, strip trailing slash + local -a dirnames + dirnames=(*/) + dirnames=("${dirnames[@]%/}") + + # Bail if no results to prevent empty output + ((${#dirnames[@]})) || exit 1 + + # Print results, null-delimited + printf '%s\0' "${dirnames[@]}" + ) + ;; + + # Complete with directories from PATH + remove|rm|r) + compopt -o filenames + local -a promptarr + IFS=: read -d '' -a promptarr < <(printf '%s\0' "$PATH") + local part + for part in "${promptarr[@]}" ; do + [[ $part == "${COMP_WORDS[COMP_CWORD]}"* ]] \ + || continue COMPREPLY=("${COMPREPLY[@]}" "$part") - fi - done - ;; - - # No completion - *) - return 1 - ;; - esac - fi + done + ;; + + # No completion + *) + return 1 + ;; + esac + ;; + esac } complete -F _path path diff --git a/bash/bashrc.d/sd.bash b/bash/bashrc.d/sd.bash index d4ba49b3..ccc4aec7 100644 --- a/bash/bashrc.d/sd.bash +++ b/bash/bashrc.d/sd.bash @@ -55,11 +55,11 @@ sd() { # Set up local variable for the sibling to which we'll attempt to move, # assuming we find one - local dir + local dirname # If we have one argument, it's easy, we just try to move to that one if (($# == 1)) ; then - dir=$1 + dirname=$1 # If no argument, the user is lazy; if there's only one sibling, we'll do # what they mean and switch to it @@ -68,7 +68,7 @@ sd() { # This subshell switches on globbing functions to try to find all the # current directory's siblings; it exits non-zero if it found anything # other than one - if ! dir=$( + if ! dirname=$( shopt -s dotglob extglob nullglob local -a siblings @@ -92,7 +92,7 @@ sd() { # This block is run if the subshell fails due to there not being a # single sibling ) ; then - printf 'bash: %s: No single sibling dir\n' \ + printf 'bash: %s: No single sibling directory\n' \ "$FUNCNAME" >&2 return 1 fi @@ -105,17 +105,43 @@ sd() { fi # Try to change into the determine directory - builtin cd "${opts[@]}" -- ../"$dir" + builtin cd "${opts[@]}" -- ../"$dirname" } # Completion function for sd; any sibling directories, excluding the self _sd() { - local word curdir - word=${COMP_WORDS[COMP_CWORD]} - curdir=${PWD##*/} + + # The completed results are filenames compopt -o filenames - local IFS=$'\n' - COMPREPLY=( $(cd .. && compgen -d -X "$curdir" -- "$word") ) + + # Only makes sense for the first argument + ((COMP_CWORD == 1)) || return 1 + + # Current directory can't be root directory + [[ $PWD != / ]] || return 1 + + # Build list of matching sibiling directories + while IFS= read -d '' -r dirname ; do + [[ $dirname == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$dirname") + done < <( + + # Set options to glob correctly + shopt -s dotglob extglob nullglob + + # Collect directory names, exclude current directory, strip leading ../ + # and trailing / + local -a dirnames + dirnames=(../!("${PWD##*/}")/) + dirnames=("${dirnames[@]#../}") + dirnames=("${dirnames[@]%/}") + + # Bail if no results to prevent empty output + ((${#dirnames[@]})) || exit 1 + + # Print results, null-delimited + printf '%s\0' "${dirnames[@]}" + ) } complete -F _sd sd diff --git a/bash/bashrc.d/ssh.bash b/bash/bashrc.d/ssh.bash index ea3c70a6..223aace8 100644 --- a/bash/bashrc.d/ssh.bash +++ b/bash/bashrc.d/ssh.bash @@ -1,23 +1,26 @@ # Completion for ssh/sftp/ssh-copy-id with config hostnames _ssh() { - local word - word=${COMP_WORDS[COMP_CWORD]} + + # Use default completion if no matches + compopt -o default # Read hostnames from existent config files, no asterisks local -a hosts local config option value for config in "$HOME"/.ssh/config /etc/ssh/ssh_config ; do - if [[ -e $config ]] ; then - while read -r option value _ ; do - if [[ $option == Host && $value != *'*'* ]] ; then - hosts=("${hosts[@]}" "$value") - fi - done < "$config" - fi + [[ -e $config ]] || continue + while read -r option value _ ; do + [[ $option == Host ]] || continue + [[ $value != *'*'* ]] || continue + hosts=("${hosts[@]}" "$value") + done < "$config" done # Generate completion reply - COMPREPLY=( $(compgen -W "${hosts[*]}" -- "$word") ) + for host in "${hosts[@]}" ; do + [[ $host == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$host") + done } -complete -F _ssh -o default ssh sftp ssh-copy-id +complete -F _ssh ssh sftp ssh-copy-id diff --git a/bash/bashrc.d/ud.bash b/bash/bashrc.d/ud.bash index b5279004..7a715a52 100644 --- a/bash/bashrc.d/ud.bash +++ b/bash/bashrc.d/ud.bash @@ -32,34 +32,53 @@ ud() { # Check and save optional second argument, target directory; default to # $PWD (typical usage case) - local dir - dir=${2:-$PWD} - if [[ ! -e $dir ]] ; then - printf 'bash: %s: Target dir %s does not exist\n' "$FUNCNAME" "$2" >&2 + local dirname + dirname=${2:-$PWD} + if [[ ! -e $dirname ]] ; then + printf 'bash: %s: Target directory %s does not exist\n' "$FUNCNAME" "$2" >&2 return 1 fi # Append /.. to the target the specified number of times local -i i for (( i = 0 ; i < steps ; i++ )) ; do - dir=${dir%/}/.. + dirname=${dirname%/}/.. done # Try to change into it - cd "${opts[@]}" -- "$dir" + cd "${opts[@]}" -- "$dirname" } -# Completion is only useful for the second argument +# Completion setup for ud _ud() { - if ((COMP_CWORD == 2)) ; then - local word - word=${COMP_WORDS[COMP_CWORD]} - compopt -o filenames - local IFS=$'\n' - COMPREPLY=( $(compgen -A directory -- "$word" ) ) - else - return 1 - fi + + # The completions given are filenames and may require escaping + compopt -o filenames + + # Only makes sense for the second argument + ((COMP_CWORD == 2)) || return 1 + + # Iterate through directories, null-separated, add them to completions + local dirname + while IFS= read -d '' -r dirname ; do + [[ $dirname == "${COMP_WORDS[COMP_CWORD]}"* ]] || continue + COMPREPLY=("${COMPREPLY[@]}" "$dirname") + done < <( + + # Set options to glob correctly + shopt -s dotglob nullglob + + # Collect directory names, strip trailing slashes + local -a dirnames + dirnames=(*/) + dirnames=("${dirnames[@]%/}") + + # Bail if no results to prevent empty output + ((${#dirnames[@]})) || exit 1 + + # Print results null-delimited + printf '%s\0' "${dirnames[@]}" + ) } complete -F _ud ud diff --git a/bash/bashrc.d/vr.bash b/bash/bashrc.d/vr.bash index a8d836a3..3eb1fbdd 100644 --- a/bash/bashrc.d/vr.bash +++ b/bash/bashrc.d/vr.bash @@ -37,8 +37,8 @@ vr() { return fi - # If we have a .svn dir, iterate upwards until we find an ancestor that - # doesn't; hopefully that's the root + # If we have a .svn directory, iterate upwards until we find an ancestor + # that doesn't; hopefully that's the root if [[ -d $path/.svn ]] ; then local search search=$path -- cgit v1.2.3