TransWikia.com

How to get tab completion when using curly braces in Bash

Unix & Linux Asked by Weston Ganger on December 17, 2021

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

5 Answers

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.

Answered by Craig Hicks on December 17, 2021

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}

Answered by user3325563 on December 17, 2021

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:

$' tn"'@><=;|&(:'

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.

Answered by mikeserv on December 17, 2021

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.

Answered by mr.spuratic on December 17, 2021

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.

Answered by Joe Sewell on December 17, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP