Bash – How to get tab completion when using curly braces in Bash

autocompletebash

I want to have tab completion when using curly braces in a command in bash.

For example:

cp ~/html/{foo bar.txt whatever} /var/www/html/

I want the tab completion for the files stated in the curly braces

Best Answer

There are two ways to interpret the question: you want the completion while you are typing the names before the closing } (in effect performing completion of files in another directory); or, you want the completion to expand and substitute the (valid) names after the closing }. { } expansion expands all the options, unlike globbing which expands existing file names, though using "," rather than space.

With bash, the expected way to use {} to create a list of words is:

cp ~/html/{foo,bar.txt,whatever}  ...

There are two options: associating a readline key binding with a shell function, or programmable completion.

The readline approach gives you a free hand to completely rewrite the current command, but the problem is that it doesn't tokenise the current command line, so you have a non-trivial parsing problem.

Programmable completion does tokenise the command line, but you can only modify/replace the current word, which means you can't (easily) preserve the path/{ prefix during editing.

The only related native bash capability is the readline function shell-expand-line for bash purports to do "all of the shell word expansions", this is bound by default to \M-\C-e (Esc \C-e). One might reasonably expect this expansion to be all of the seven types of expansion indicated in the man page (bash-4.3):

Expansion is performed on the command line after it has been split into
words.   There are seven kinds of expansion performed: brace expansion,
tilde expansion, parameter and variable  expansion,  command  substitu‐
tion, arithmetic expansion, word splitting, and pathname expansion.

You can experiment with by typing (without hitting Return)

echo {1..2} ~root $0 $LANG `echo foo` $((1+2) "a b" /etc/p[aeiou]*

And then MetaCtrlE (or ESC + CtrlE if you are using a Meta-impaired keyboard).

Neither the first nor last (brace and path) expansions work for me, so the documentation and implementation don't quite match IMHO.

What follows is a work-around, rather than complete solution using tab for expansion. You need to make sure to use the correct syntax for brace expansion though, specifically with a comma to separate the items, not a space.

function _expand() {
   [[ -z "${READLINE_LINE}" ]] && return
   eval local aa=( ${READLINE_LINE} )       # not-quoted, eval needed
   [[ ${#aa} -eq 0 ]] && return             # parse problem
   printf -v READLINE_LINE "%s " "${aa[@]}"
   READLINE_POINT=${#READLINE_LINE}         # eol, predictable at least
}

bind -x '"\C-_":_expand'

This binds Ctrl_ to a bash function which uses eval to populate an array, causing all the normal expansions (as above, and not including history) to occur, and then rebuilds the command line.

Start typing, hit Ctrl_ when you want something expanded, and Return as normal when ready.

This is more powerful than tab, but less precise in that it expands the complete input line. There are a few cases where this won't expand as hoped, specifically where various shell metacharacters confuse the simple eval logic.

Rather than over-engineer a bash solution, Gille's suggestion of zsh is worth a look.

Related Question