3

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
}
Six
  • 5,122
  • 3
  • 29
  • 38
  • The sed command would work if you make the `:` optional at the end: `sed -r 's/([^:]+):*/find "\1" -maxdepth 1 -type f -executable -exec basename "{}" \\;\n/eg' <<< $PATH | sort | uniq` ... However, your bash attempt looked good. What is wrong with that? The performance? – hek2mgl Apr 03 '15 at 14:15
  • Running that extensive of a find command for completion didn't really seem to work well in practice, freezing up the terminal for a few seconds for each [TAB]. Just hoping theres already a completion function or option to do it better otherwise I may just have to scrap it. – Six Apr 03 '15 at 14:28
  • I don't really get what is wrong with `compgen -A function -abck` ? – hek2mgl Apr 03 '15 at 15:00
  • `compgen -A function -abck` includes all sorts of commands (aliases, builtins, functions, keywords, executables on PATH). I just want executables on path. For example, one of the scripts is a wrapper for pygmentize. I want to easily be able to do syntax highlighting on installed scripts. Having it complete to aliases or keywords would be counterproductive. Anyways that's just one example but I've had it come up in a number of situations. – Six Apr 03 '15 at 15:19
  • Then use only `compgen -c`.. – hek2mgl Apr 03 '15 at 15:22
  • Yes as far as I know `compgen -c` is the same as `compgen -A function -abck`. I don't think there is a compgen option to only list executables but it would be really nice if there was. – Six Apr 03 '15 at 15:31
  • Check this: http://stackoverflow.com/questions/948008/linux-command-to-list-all-available-commands-and-aliases .. Also try `diff -u <(compgen -A function -abck) <(compgen -c)` – hek2mgl Apr 03 '15 at 16:24
  • They only appear to be different because `compgen -A function -abck` shows a bunch of duplicates. Try this to remove all the dupes: `diff -u <(compgen -A function -abck | sort -u) <(compgen -c | sort -u)`. That answer at http://stackoverflow.com/questions/948008/linux-command-to-list-all-available-commands-and-aliases was a little misleading I think because it should be `compgen -c` for all of the above... Right? – Six Apr 03 '15 at 16:40

3 Answers3

4

I have a run-in-background script. To get autocomplete for it I use bash's builtin complete:

> complete -c <script-name>
> <script-name> ech[TAB]
> <script-name> echo 

From the docs:

command
   Command names. May also be specified as -c.

To deal with non-executables, my first thought was to filter with which or type -P but that's terribly slow. A better way is to only check the unknowns. Use @Six's solution or some other to find items in compgen -c but not compgen -abkA function. For everything in both lists, check with type -P. For example:

function _executables {
    local exclude=$(compgen -abkA function | sort)
    local executables=$(
        comm -23 <(compgen -c) <(echo $exclude)
        type -tP $( comm -12 <(compgen -c) <(echo $exclude) )
    )
    COMPREPLY=( $(compgen -W "$executables" -- ${COMP_WORDS[COMP_CWORD]}) )
}
complete -F _executables <script-name>

I've tried a few times but introducing -F to complete just seems to be a slow way to do thing. Not to mention now having to handle absolute/relative path completion to executables, the horribly fiddly complete-the-directory-but-don't-add-a-space bit and correctly handling spaces in paths.

Community
  • 1
  • 1
jozxyqk
  • 16,424
  • 12
  • 91
  • 180
  • `complete -c` gives the same completions as `compgen -c`, which includes all of the aforementioned undesirables like aliases, builtins, functions, and keywords. To give you a real-world example, just last week I was writing some wrapper scripts, one to enable XFCE's presentation mode (which inhibits screensaver and suspend) while running some command and other to only inhibit suspend. Were I to use `complete -c presentation-wrapper`, I'd be getting a bunch of undesired completions like `command`, `declare`, `done`, and `while`. I imagine they're also problematic for your script. – Six Feb 07 '17 at 15:44
  • @Six Aah right, thanks for being patient with me! I've had a go but there's still a decent delay of 0.1-0.2 seconds and lots of edge cases to handle. – jozxyqk Feb 10 '17 at 04:16
1

Looks like you are looking for compgen -c

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • I tried the above command and it's actually getting some permission errors where there shouldn't be any. Running this command `sed -r 's/([^:]+):/find "\1" -maxdepth 1 -type f -executable -exec basename {} \\;\n/eg' <<< $PATH | sort | uniq | wc -l` results in 2929. The other command I considered using is `find ${PATH//:/ } -maxdepth 1 -executable -exec basename '{}' \; 2>/dev/null | sort -u | wc -l` and I get 3701 which is the correct number. Both of those commands take about 3-4 seconds, so not great but seems accurate at least. – Six Apr 03 '15 at 14:00
  • Hmm I see, same on my machine. (Btw, your bash command is way better than my `sed` approach! Looks like I play too much with `sed`) – hek2mgl Apr 03 '15 at 14:05
0

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
}
Six
  • 5,122
  • 3
  • 29
  • 38