Bash auto-complete for running applications

[Tweet]

I’m not up for doing a full Bash autocomplete tutorial tonight, but wanted to share something I figured out instead of eating lunch. Here it is with a modicum of explanation.

I’ve been using Jon Stovell’s quit script to stop running applications with various save options as part of tweaking a safer universal computing setup with Dropbox. For various reasons I found myself wanting to autocomplete application names, but only ones that were currently running. This technique could be used for any command line tool that runs on running, windowed applications. This version doesn’t do anything for faceless processes, but I’ll show you that in a minute.

A short version to start with. You can run a long one-liner in .bash_profile to return an array of running apps using osascript (AppleScript) and pass it to complete with the -W parameter to accept the variable instead of a function:

APP_COMPLETE=( $( echo "$(osascript -e "set AppleScript's text item delimiters to \"\n\"" -e "tell application \"System Events\" to return (displayed name of every application process whose (background only is false and displayed name is not \"Finder\")) as text" 2>/dev/null)"|sed -e 's/ /\\ /g' ) )

complete -o default -W "${APP_COMPLETE[*]}" quit

This works but isn’t case insensitive, and I’m never satisfied until things are as lazy as possible.

Case-insensitive completion of running app names

The script below is in ~/.app_completions along with my app name completion script for launching (as mentioned in the Bash version of my Get an App’s Icon series). You could just as easily add it to your .bash_profile, but it’s easier for me to keep track of if it’s external.

In .app_completions is this function and complete command:

_complete_running_apps ()
{
  local cur prev
  local LC_ALL='C'

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

  # do not attempt completion if we're specifying an option
  [[ "$cur" == -* ]] && return 0

  # Escape dots in paths for grep 
  cur=${cur//\./\\\.}

  local IFS="
"
  COMPREPLY=( $( echo "$(osascript -e "set AppleScript's text item delimiters to \"\n\"" -e "tell application \"System Events\" to return (displayed name of every application process whose (background only is false and displayed name is not \"Finder\")) as text" 2>/dev/null)"|grep -i "^$cur" | sed -e 's/ /\\ /g' ) )

  return 0
}

complete -o bashdefault -o default -o nospace -F _complete_running_apps quit 2>/dev/null || complete -o default -o nospace -F _complete_running_apps quit

It starts start by setting up a few variables to read what the completion command provides as partial text and preceding words. Then it runs the same AppleScript1 from before, except that by running it through a function we’re taking control of the matching process. Thus, we can use grep -i for case-insensitive matching. We set $COMPREPLY to the result and it’s read in by the auto-complete on the command line.

In the complete command at the end, note that it’s only currently setting up completion for the quit command. Just change quit to something else if you’re using it for something else.

Now, just add a line to .bash_profile to read in the script on login:

source ~/.app_completions

Bonus: kill ‘em all.

You can also do something similar with all running processes, handy for commands like killall. This could also be easily converted to a one-liner like the first script above, but just for the sake of laziness:

_complete_running_processes ()
{
    local cur prev
    local LC_ALL='C'

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

    # do not attempt completion if we're specifying an option
    [[ "$cur" == -* ]] && return 0

    # Escape dots in paths for grep
    cur=${cur//\./\\\.}

    local IFS="
"
    COMPREPLY=( $(ps axc|awk '{ print $5 }'|sort -u|grep -v "^[\-\(]"|grep -i "^$cur") )

    return 0
}

complete -o bashdefault -o default -o nospace -F _complete_running_processes killall 2>/dev/null || complete -o default -o nospace -F _complete_running_processes killall

The ps axc reports all running processes with just their executable name. The awk after it grabs the executable names from each line. They’re passed as a list to sort -u which removes duplicates and sorts them in alphabetical order. The first grep removes processes beginning with “(“ or “-“ so we can exclude private and terminal commands. There are still plenty of lines autocompleting that you really don’t want to kill, but hey, the command line can be a dangerous place. The last grep does our case-insensitive match for anything that begins with the partial string that we’re completing off of.

So that’s my lunch break. It will ease my hunger pains if someone else finds any use for this. Be sure to let me know!

  1. There’s probably a way to do this with ps and some string manipulation, but it was lunchtime and I was hungry.