How does bash differentiate between brace expansion and command grouping?
A simplified reason is the existence of one character: space.
Brace expansions do not process (un-quoted) spaces.
A {...}
list needs (un-quoted) spaces.
The more detailed answer is how the shell parses a command line.
The first step to parse (understand) a command line is to divide it into parts.
These parts (usually called words or tokens) result from dividing a command line at each meta-character from the link:
- Splits the command into tokens that are separated by the fixed set of meta-characters: SPACE, TAB, NEWLINE, ;, (, ), <, >, |, and &. Types of tokens include words, keywords, I/O redirectors, and semicolons.
Meta-characters: spacetabenter;,<>| and &.
After splitting, words may be of a type (as understood by the shell):
- Command pre-asignements:
LC=ALL ...
- Command
LC=ALL echo
- Arguments
LC=ALL echo "hello"
- Redirection
LC=ALL echo "hello" >&2
Brace expansion
Only if a "brace string" (without spaces or meta-characters) is a single word (as described above) and is not quoted, it is a candidate for "Brace expansion". More checks are performed on the internal structure later.
Thus, this: {ls,-l}
qualifies as "Brace expansion" to become ls -l
, either as first word
or argument
(in bash, zsh is different).
$ {ls,-l} ### executes `ls -l`
$ echo {ls,-l} ### prints `ls -l`
But this will not: {ls ,-l}
. Bash will split on space and parse the line as two words: {ls
and ,-l}
which will trigger a command not found
(the argument ,-l}
is lost):
$ {ls ,-l}
bash: {ls: command not found
Your line: {ls;echo hi}
will not become a "Brace expansion" because of the two meta-characters ; and space.
It will be broken into this three parts: {ls
new command: echo
hi}
. Understand that the ; triggers the start of a new command. The command {ls
will not be found, and the next command will print hi}
:
$ {ls;echo hi}
bash: {ls: command not found
hi}
If it is placed after some other command, it will anyway start a new command after the ;:
$ echo {ls;echo hi}
{ls
hi}
List
One of the "compound commands" is a "Brace List" (my words): { list; }
.
As you can see, it is defined with spaces and a closing ;
.
The spaces and ; are needed because both {
and }
are "Reserved Words".
And therefore, to be recognized as words, must be surrounded by meta-characters (almost always: space).
As described in the point 2 of the linked page
- Checks the first token of each command to see if it is .... , {, or (, then the command is actually a compound command.
Your example: {ls;echo hi}
is not a list.
It needs a closing ; and one space (at least) after {. The last } is defined by the closing ;.
This is a list { ls;echo hi; }
. And this { ls;echo hi;}
is also (less commonly used, but valid)(Thanks @choroba for the help).
$ { ls;echo hi; }
A-list-of-files
hi
But as argument (the shell knows the difference) to a command, it triggers an error:
$ echo { ls;echo hi; }
bash: syntax error near unexpected token `}'
But be careful in what you believe the shell is parsing:
$ echo { ls;echo hi;
{ ls
hi
The block {
is a shell keyword, so it must separated from the next word by space, while in brace expansion, there should be no space (if you need to brace expand a space, you have to escape it: echo {\ ,a}{b,c}
).
You can use brace expansion at the start of a command:
{ls,.} # expands to "ls ."
You can't use it to expand to a block, though, as parsing of the grouping commands happens before expansions:
echo {'{ ls','.;}'} # { ls .;}
{'{ ls','.;}'} # bash: { ls: No such file or directory
It knows by checking the syntax of the command line. In the same way it knows that in the expression echo echo
, the first echo should be treated as a command and the second echo as a parameter of the first echo.
In bash it is very simple, since { cmd; }
should have spaces and semicolon. However, for example in zsh they are not needed, but still by analyzing context of {}
shell is able to tell what should be done with its content.
Consider the following:
alias 1..3=date
{ 1..3; } #in bash
{1..3} #in zsh
Both return current date, but
echo {1..3}
returns 1 2 3
because shell knows {}
in an argument for command echo
, so should be expanded.