I find the bash commands complete and compgen overly mysterious, so I’m often playing with them to try to get a better grasp on all of the poorly-documented options. complete is a shell built-in, no man page, just help complete output. It’s vague.

I’ve detailed some of my other exploits with this in the past, one of my favorites being custom app alias completions. This time I wanted to go a lot simpler.

The afplay command comes default with OS X’s BSD installation. It’s the “Audio File Play” command used to play sound files in compatible formats. I usually use it in scripts to play the system sounds (Glass, Basso, etc.). So I wrote a quick function to make it easier to get to those:

play() {
	command afplay "/System/Library/Sounds/$1.aiff"
}

With that in place, I can just call play basso and it will play the sound. I don’t always remember the names of all the sounds, though, which means I have to ls /System/Library/Sounds to see them. A perfect job for shell completion, right?

So here’s the simple script that I source in ~/.bash_profile to give me tab completion of system sounds, listing them all if I haven’t started typing a word yet.

afplay.completion.bashraw
"
play() {
	command afplay "/System/Library/Sounds/$1.aiff"
}

_complete_system_sounds ()
{
	local cur
	local LC_ALL='C'

	COMPREPLY=()
	cur=${COMP_WORDS[COMP_CWORD]}

	# Turn case-insensitive matching on if needed
	local nocasematchWasOff=0
	shopt nocasematch >/dev/null || nocasematchWasOff=1
	(( nocasematchWasOff )) && shopt -s nocasematch

	local IFS="
"
	# Store list of sounds
	local sounds=( $(command ls /System/Library/Sounds/  2>/dev/null | xargs -I% basename % .aiff) )

	# Custom case-insensitive matching
	local s matches=()
	for s in ${sounds[@]}; do
	    if [[ "$s" == "$cur"* ]]; then
	    	matches+=("$s");
	    fi
	done

	# Unset 'nocasematch' option if it was unset before
	(( nocasematchWasOff )) && shopt -u nocasematch

	COMPREPLY=("${matches[@]}")

	return 0
}

complete -F _complete_system_sounds play

What it does is create an array from the result of listing the sounds directory and getting the base name of every file minus the .aiff extension. Then, rather than using compgen to do the matching, it uses a custom loop to handle case insensitive matching. This would normally work by default with compgen and shopt -s nocasematch, but for reasons I’m not clear on, it doesn’t when you’re providing a custom list.

The _complete_system_sounds function is used to case-insensitively complete the “play” function when I call it, so typing play f[TAB] will offer me “Frog” and “Funk.” afplay completion continues using the Bash default, only my custom function is affected.

Neat trick.

Ryan Irelan has produced a series of shell trick videos based on BrettTerpstra.com posts. Readers can get 10% off using the coupon code TERPSTRA.