Bash Completion with Debian

The tab completion for bash provided by the bash-completion package in Debian annoys me. I think it’s bloated and it behaves quite unexpectedly. I could always uninstall it, but what if I don’t have root access or if there’s other users on the machine who wants the package installed?

Easy – insert some basic commands into your user’s bash initialization file (such as ~/.bashrc) that cleans out all of the predefined functions, variables, and completion settings:

  1. Unset any and all bash functions that may exist.
    unset -f $(set | grep -E '^[^ ]+ \(\)' | cut -d' ' -f1 | xargs)

  2. Unset variables that begins with a character that is not a capital letter.
    unset $(set | grep -E '^[^A-Z][^=]*=' | cut -d= -f1 | xargs)

  3. Remove any and all defined completion rules.
    complete -r

All done!

There’s usually some stuff that I actually like to have tab completed though, such as the sudo command.

Without any custom tab completion configured, doing sudo <TAB> will obviously complete the command line into files, directories, etc. that are found in your current working directory.

I’d rather see the second command line argument being completed into the the executables found in the $PATH. And how about getting it to show the same executables as the root user would see, while we’re at it?

Create a bash function that temporarily, within the function itself, appends the sbin directories to the current $PATH and then generates a tab completion reply based on the executables found in the modified $PATH:

  if [ "$COMP_CWORD" -ge '2' ]; then
    compopt -o filenames
    COMPREPLY=( $(compgen -f -- "$cur") )
    local PATH=$PATH:/usr/local/sbin:/usr/sbin:/sbin
    COMPREPLY=( $(compgen -c -- "$2") )

The if/else/fi statement makes sure that only the second command line argument is completed into the available executables. The rest of the arguments (if specified) should still be completed like usual (into files etc.).

Finally, make sure that the completion is actually activated for sudo:
complete -F _complete_sudo sudo

It should be noted that this tab completion function won’t be any good in case any options are supplied to the sudo executable itself, e.g. running sudo -u user command. It’s also pretty pointless changing the $PATH this way for a non-root user.

What about tab completing into the commands that the user is actually allowed to execute through sudo? Not impossible. Secure? Probably not.

Another tab completion feature that I’ve always appreciated is using the built-in completion function called dirnames for the basic cd command:

complete -o dirnames cd

This will simply make sure that the tab completion for cd will result in actual directories, meaning no files etc. It will, however, still show symlinks in case the symlink points to a directory.

UPDATE 2013-01-21:
I made a new completion function for sudo that is a bit more… functional. This is really the problem with using completion functions. You will always find something that’s missing. Even though this one does the job better, it still won’t complete correctly in case you run sudo with arguments to sudo itself etc. (like -u user). See the comments in the function below for more information.

  local cur f

  # get current word

  # is this the first word?
  if [ "$COMP_CWORD" == '1' ]; then # 1st word = i.e. executable
    # word begins with slash or dot? treat as executable
    if [ "${cur:0:1}" == '/' -o "${cur:0:1}" == '.' ]; then
      compopt -o filenames
      # only include executable files
      # (and dirs to be able to traverse the tree)
      for f in $(compgen -f -- "$cur"); do
        [ -x "$f" ] || continue
    else # treat it as a normal command found in su PATH otherwise
      local PATH=$PATH:/usr/local/sbin:/usr/sbin:/sbin
      COMPREPLY=( $(compgen -c -- "$cur") )
  else # not 1st word, i.e. arguments; complete like usual files
    compopt -o filenames
    COMPREPLY=( $(compgen -f -- "$cur") )