I've had this use case come up for a couple of different scripts I've written or modified. Essentially, I want bash completion for option '-x' to complete executables on the PATH. This is sort of two questions wrapped in one.
So far I've had troubles because bash doesn't easily distinguish between aliases, builtins, functions, etc and executable files on the PATH. The _commands
wrapper function in /usr/share/bash-completion/bash_completion completes on all of the above but I have no use for working with aliases, builtins, functions, etc and only want to complete on the commands that happen to be executables on the PATH.
So for example... If I enter scriptname -x bas[TAB]
, it should complete with base64, bash, basename, bashbug.
This is what my completion script looks like now:
_have pygsparkle && {
_pygsparkle(){
local cur prev
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
case $prev in
-x|--executable)
# _command
executables=$({ compgen -c; compgen -abkA function; } | sort | uniq -u)
COMPREPLY=( $( compgen -W "$executables" -- "$cur" ) )
return 0
;;
esac
if [[ $cur = -* ]]; then
COMPREPLY=( $( compgen -W '--executable -h --help -x' -- "$cur" ) )
fi
}
complete -F _pygsparkle pygsparkle
}
It seems to work as expected but { compgen -c; compgen -abkA function; } | sort | uniq -u
is a pretty dirty hack. In zsh you can get a sorted list of executables on PATH running print -rl -- ${(ko)commands}
. So it appears I'm missing at least 60+ execs, likely because uniq -u
is dumping execs with that same name as aliases or functions.
Is there a better way to do this? Either a better command for getting all executables on PATH or a pre-existing completion function that will serve the same ends?
Update: Ok so the following function executes in under 1/6 sec and looks like the best option. Unless there are any other suggestions I'll probably just close the question.
_executables(){
while read -d $'\0' path; do
echo "${path##*/}"
done < <(echo -n "$PATH" | xargs -d: -n1 -I% -- find -L '%' -maxdepth 1 -mindepth 1 -type f -executable -print0 2>/dev/null) | sort -u
}
There doesn't seem to be a simple answer to the question of how to list all the executables available on a user's PATH. Alas, I have searched far and wide for an answer.
compgen
may at first seem like the right direction but it lacks an option to show only executables
compgen -c
will show all commands (this includes aliases, builtins, executables, functions, and keywords)
compgen -abkA function
will show all commands except executables
So an approximation of the executables available can be surmised by 'diffing' the two, i.e. { compgen -c; compgen -abkA function; } | sort | uniq -u
, but that clearly has some issues.
NOTE: To confirm that compgen -c
will not work, all you have to do is scan through the results and identify many non-executable entries. You can also try diff -u <(compgen -A function -abck | sort -u) <(compgen -c | sort -u)
and see that the commands are equivalent (once duplicates have been removed of course).
So it seems the best option is to scan through every directory on the path.
In summary, the following is the best option on a modern system. It has a speedy runtime (~0.05 seconds) comparable to compgen -c
and suitable for completions.
executables(){
echo -n "$PATH" | xargs -d: -I{} -r -- find -L {} -maxdepth 1 -mindepth 1 -type f -executable -printf '%P\n' 2>/dev/null | sort -u
}
If you are using ancient versions of find/xargs (i.e. busybox or BSD) and/or using mksh (Android's default shell) which doesn't support process substitution, you will want the following. Not so speedy (~3.5 seconds).
executables(){
find ${PATH//:/ } -follow -maxdepth 1 -type f -exec test -x '{}' \; -print0 2>/dev/null \
| while read -d $'\0' path; do
echo "${path##*/}"
done \
| sort -u
}
If you are using the aforementioned setup and have spaces in your PATH for some reason, then this should work.
executables(){
echo "$PATH" \
| tr ':' '\n' \
| while IFS= read path; do
find "$path" -follow -maxdepth 1 -type f -exec test -x '{}' \; -print0 2>/dev/null
done \
| while read -d $'\0' path; do
echo "${path##*/}"
done \
| sort -u
}