Having fun with bash completion
I have a hard time navigating through directory structure in some projects. I usualy know the leaf directory name exactly, but not the intermediate path.
Some examples are:
- linux.git: i915 -> drivers/gpu/drm/i915
- gentoo.git: xmms2 -> media-sound/xmms2
- nixpkgs.git: re2c -> pkgs/development/tools/parsing/re2c
First two I somehow tolerated for a while and was able to get away with an equivalent of cd */xmms2. But for nixpkgs became too much to type.
First, I wrote a one-liner to cd right into a subdir if it’s a unique directory across all subdirectories:
cdc() {
if [[ -d $1 ]]; then
cd "$1"
return
fi
local candidates=()
# Be careful to handle directories with whitespace
# and special characters that could break tokenization
while IFS= read -r -d $'\0' d; do
[[ $d == '.' ]] && continue
candidates+=("${d#./}")
done < <(find -path "*/$2" -type d -print0 2>/dev/null)
if [[ ${#candidates[@]} -eq 1 ]]; then
cd "${candidates[@]}"
return
fi
echo "ERROR: cdc: '$1' is ambiguous (${#candidates[@]}) entries. Can't cd."
return 1
}
Example usage session:
~ $ cdc re2c
ERROR: cdc: 're2c' is ambiguous (8) entries. Can't cd.
~ $ cd portage/gentoo
~/portage/gentoo $ cdc re2c
~/portage/gentoo/dev-util/re2c $
Looks straightforward. Then I thought of hooking up bash completion to avoid typing full intermediate directory. And to see interactively what these ambiguities are.
For example, in case of pkgs/development/python-modules/importlib-metadata I’d like to avoid typing importlib-metadata while being able to get to it quicker.
Apparently, bash does not require bash completion to be an exact prefix for something and allows for any arbitrary substitution!
Here is a silly example to get basics of bash completion:
# complete-hia.bash
# $1 - 'hia'
# $2 - the word being completed
# $3 - the word before completion
_complete_hia() {
# generic completion results:
COMPREPLY=(
# just generate 5 random entries
$RANDOM
$RANDOM
$RANDOM
$RANDOM
$RANDOM
# and a fancy output
"Hia! Here is your full arg list: '$*'"
)
# let's do something special on exact match
if [[ $2 == secret ]]; then
COMPREPLY=( "YOU GOT IT!")
fi
}
complete -F _complete_hia hia
Example session:
$ source complete-hia.bash
$ hia a<TAB>
1476 29762
14984 31708
15184 Hia! Here is your full arg list: 'hia a hia'
$ hia a<TAB>
22726 3483
24271 8982
32492 Hia! Here is your full arg list: 'hia a hia'
$ hia is it secret<TAB>
$ hia is it YOU GOT IT!
There are many minor caveats like automatic prefix expansion when all alternatives match (make sure to check compgen).
Let’s try arbitrary directory completion for cdc command introduced above.
_cdc() {
local d candidates=()
while IFS= read -r -d $'\0' d; do
[[ $d == '.' ]] && continue
candidates+=("${d#./}")
done < <(find -path "*/$2" -type d -print0 2>/dev/null)
COMPREPLY=(
# multiple candidates, don't match on prefix. Just dump all.
# Also always quote output
"${candidates[@]@Q}"
)
if [[ ${#candidates[@]} -gt 1 ]]; then
# If there is ambiguity do not mangle original argument
COMPREPLY+=( "${2}" )
fi
}
complete -F _cdc cdc
Here is the example session:
~ $ cdc *rtlib-*
'dev/git/nixpkgs/pkgs/development/python-modules/importlib-metadata'
'dev/git/nixpkgs/pkgs/development/python-modules/importlib-resources'
*rtlib-*
~ $ cdc *rtlib-m*<TAB>
~ $ cdc 'dev/git/nixpkgs/pkgs/development/python-modules/importlib-metadata'
~ $ cdc curseofwar<TAB>
curseofwar
'dev/git/nixpkgs/pkgs/games/curseofwar'
'portage/slyfox-gentoo/games-strategy/curseofwar'
I made trailing globs to be clunky to use on purpose as I use exact match most of the time. One could cook a version with many enhancements like find’s case-insensitive match or do something smarter around completion quoting.
More info is at Programmable Completion Builtins.
Have fun!