4

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

  • I don't know if bash can do that. Zsh does (it just works), you might consider switching. – Gilles 'SO- stop being evil' Jan 26 '15 at 21:54
1

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.

| improve this answer | |
  • 1
    Your eval is at risk of executing arbitrary code - $READLINE_LINE need only contain a ) and your user is in serious trouble. You could do a much safer job of this with $IFS probably. – mikeserv Jan 27 '15 at 19:17
  • 1
    @mikeserv that's absolutely correct, eval is not something to invoke lightly (or possibly at all). I can't think of a scenario where the input isn't something the user typed/pasted into the shell command line however. Suggestions for a safer alternative way to expand the troublesome {} welcome... – mr.spuratic Jan 27 '15 at 21:01
  • 1
    mr. spuratic - eval can be safely invoked given parameter validation. And nevermind the IFS thing - because you're already using it actually - and globbing as well. So the ) statement I made before was incorrect - your $READLINE_LINE need not contain a ) to offer serious risk to operational security - it need only contain a glob that matches a filename containing a ), in fact. That or any backquotes or $(...) or any number of other shell tokens that could result in code execution. – mikeserv Jan 28 '15 at 2:27
2

In a Ubuntu (16.04) terminal you can do Ctrl+{ and then { and it autocompletes the curly brackets with the files in the directory. It uses commas instead of spaces to separate the files. For example, a directory containing these files: file.cpp file.h file.i file.py file_wrap.cxx

... would auto complete to this: $ cp file{.{cpp,h,i,py},_wrap.cxx}

| improve this answer | |
  • 1
    Hah. I did this by accident one time and was like "WTF just happened??" – Archie Sep 26 '19 at 16:13
  • 1
    Yep, I think that's kind of how I found about it too ... plus a few hours afterwards web searching for it. Perhaps that's how I end up writing this answer. – user3325563 Sep 26 '19 at 18:40
  • 1
    The code that implements this makes for some C programming fun: opensource.apple.com/source/bash/bash-86.1/bash-3.2/… – Archie Sep 27 '19 at 1:30
0

Braces (which, by definition, are "curly") in this context refer to a list of fragments of text, separated by commas. While it's most often used for file names, it gets expanded before file name expansions are handled. Therefore, there is no definitive expansion capability, because bash cannot know if you're actually typing a file name or not.

| improve this answer | |
0

From man bash:

COMP_WORDBREAKS
       The  set  of  characters  that  the readline
       library treats as word separators when  per‐
       forming word completion.  If COMP_WORDBREAKS
       is unset, it loses its  special  properties,
       even if it is subsequently reset.

I thought that was interesting, so I did:

printf %q\\n "${COMP_WORDBREAKS}"

...to have a look at its default value. This got me:

$' \t\n"\'@><=;|&(:'

So I figured, why not...?

$ COMP_WORDBREAKS="{}$COMP_WORDBREAKS"
$ {/<TAB><TAB>

...where the <TAB> part represents my actually pressing the <TAB> key, and I saw:

$ {/
bin/      esp/      lib/      opt/      run/      sys/      usr/      
boot/     etc/      lib64/    proc/     sbin/     testuser/ var/      
dev/      home/     mnt/      root/     srv/      tmp/

So that was neat. I then did:

$ COMP_WORDBREAKS=${COMP_WORDBREAKS#??}
$ {/<TAB><TAB>

...and nothing happened.

| improve this answer | |
0

I provide diagnostics tool which enables the OP to directly observe the behavior of line completion after tab, and for comparison, the behavior of parsing after return. After observation and analysis, we may conclude that while writing a line completion function to complete curly brackets function might be possible - it would be pointless due to bash's limitation of suppressing any further line completion functionality after the curly brackets.

The following bash script enables the diagnostics:

#!/bin/bash

confirm_args(){
    logger -t "confirm_args" -- "#=${#}"
    for index in `seq 0 ${#}` ; do
        eval item=\$$index
        logger -t "confirm_args" -- "[$index] ${item}"
    done    
}

_foo_completion(){
    logger -t "_foo_completion" -- "COMP_CWORD=${COMP_CWORD}" 
    logger -t "_foo_completion" -- "#COMP_WORDS=${#COMP_WORDS[@]}"
    for index in `seq 0 $((${#COMP_WORDS[@]}-1))` ; do
        logger -t "_foo_completion" -- "[$index] ${COMP_WORDS[$index]}"
    done
}
declare -f _foo_completion
complete -F _foo_completion "foo"
alias foo=confirm_args

I have made it available as a gist foo-completion.sh.

Copy and paste it into a file /tmp/foo-completion.sh.

Create two dummy files

touch /tmp/a.{1,2}

In order for the diagnostics to function correctly, logger must write to the system log. Then the OP will be able to view the diagnostic output by following the system log file. If the OP is running systemd it can viewed by opening a new window and entering

sudo journalctl -f

Activate the line completion and diagnostic output:

source /tmp/foo-completion.sh

Note that the _foo_completion does NOT perform any completion, thus no candidates are return to the user. All it does is show what data is passed to the line completion function.

Test some inputs and observe the outputs:

INPUT 1

foo a.*[SPACE][TAB]

OUTPUT

Jul 23 12:10:34 ub18 _foo_completion[17672]: COMP_CWORD=2
Jul 23 12:10:34 ub18 _foo_completion[17673]: #COMP_WORDS=3
Jul 23 12:10:34 ub18 _foo_completion[17675]: [0] foo
Jul 23 12:10:34 ub18 _foo_completion[17676]: [1] /tmp/a.*
Jul 23 12:10:34 ub18 _foo_completion[17677]: [2]

INPUT 2

foo a.*[SPACE][RETURN]

OUTPUT

Jul 23 12:19:43 ub18 confirm_args[18487]: #=2
Jul 23 12:19:43 ub18 confirm_args[18489]: [0] bash
Jul 23 12:19:43 ub18 confirm_args[18490]: [1] /tmp/a.1
Jul 23 12:19:43 ub18 confirm_args[18491]: [2] /tmp/a.2

INPUT 3

foo a.{1,2}[SPACE][TAB]

OUTPUT

NOTE - no output!

INPUT 4

foo a.{1,2}[SPACE][RETURN]

OUTPUT

Jul 23 12:28:42 ub18 confirm_args[19098]: #=2
Jul 23 12:28:42 ub18 confirm_args[19100]: [0] bash
Jul 23 12:28:42 ub18 confirm_args[19101]: [1] /tmp/a.1
Jul 23 12:28:42 ub18 confirm_args[19102]: [2] /tmp/a.2

Analysis

In "INPUT 1" the * is passed to the line completion function as a literal character '*'. However, in "INPUT 3" the line completion function is not even called. Apparently the curly brackets cause bash to suppress any further calls to the line completion function.

On the other hand, in the post [RETURN] parsing cases INPUT 2 and INPUT 4, bash expands both cases a.* and a.{1,2} to the same result.

One more test case:

INPUT 5

foo a.{[TAB]

OUTPUT

Jul 23 12:56:52 ub18 _foo_completion[21059]: COMP_CWORD=1
Jul 23 12:56:52 ub18 _foo_completion[21060]: #COMP_WORDS=2
Jul 23 12:56:52 ub18 _foo_completion[21062]: [0] foo
Jul 23 12:56:52 ub18 _foo_completion[21063]: [1] /tmp/a.{

Analysis

In "INPUT 5", with no space before the tab, bash DOES call line completion. So a line completion could theoretically return a single candidate

/tmp/a.{1,2}

which should result in auto-completion (although I haven't tested).

The problem with that would be that further files could not be added with line completion, because the curly brackets suppress line completion.

Testing with a preexisting line completion function, e.g.,

INPUT CHECK

ls /tmp/a.{[TAB][TAB][TAB][TAB][TAB][TAB]

OUTPUT

(nothing)

seems to support the thesis that it's not worth supporting curlies in a line completion function until/unless bash doesn't suppress line completion after curlies.

EDIT: After playing around with the [CTRL]{{ method described in this answer, I found it complete and was (sometimes) able to be followed by the default completion function to add more files. However, there was no way to continue with the original _foo_completion function after that, which may or may not be a limitation depending on the usage case.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.