Bash expansion

I had posted about the weird behavior of bash expansion, but I think I just ran across a wiki page that discusses this issue.

The key issue seems to be the order of expansions:

  1. Syntax analysis (Parsing)
  2. Brace expansion
  3. Tilde expansion
  4. Parameter and variable expansion
  5. Command substitution
  6. Arithmetic expansion
  7. Word splitting
  8. Filename expansion
  9. Quote removal

Command substitution happens after brace expansion, but before filename expansion (which * expansion is). Similarly, if you attempt to do tilde expansion, it also “won’t work”:

maciek@anemone:~$ echo $(echo \~)

Mystery solved. I ran across this when trying to figure out what was it that eval did that’s more interesting than spawning a new subshell. Clearly, the answer is that it lets you get a second pass through these steps, which can definitely be handy.


The voodoo of bash token expansion

Unlike the stunted Windows command shell (I admit I haven’t tried PowerShell), the bash shell combines the ability to put together many different small utilities with some built-in smarts to offer an incredibly powerful interactive experience.

One of the more handy built-in smarts is bash’s “magic” token expansion. If bash sees one of these while evaluating a statement, it expands it before doing anything else. The most common of these is the asterisk, which expands to everything in the current directory:

maciek@potato:~/bash$ ls
maciek@potato:~/bash$ touch foo bar # touch creates empty files named like the arguments
maciek@potato:~/bash$ echo *
bar foo

One interesting aspect is that if there is nothing in the current directory, the asterisk does not expand:

maciek@potato:~/bash$ rm foo bar
maciek@potato:~/bash$ echo *

You can also tell bash that you want a literal asterisk by escaping it with a backslash:

maciek@potato:~/bash$ touch foo bar
maciek@potato:~/bash$ echo *
bar foo
maciek@potato:~/bash$ echo \*

One of the more powerful aspects of bash is being able to spawn a subshell with $(...) (the output of the subshell is “injected” wherever the subshell block occurs). The interesting thing in terms of token expansion is that the result of the subshell execution seems to be a candidate for token expansion as well:

maciek@potato:~/bash$ echo $(echo \*)  
bar foo  

However, there are other types of expansions that bash does for you. One of my favorite is brace expansion:

maciek@potato:~/bash$ echo foo{1,2,3}
foo1 foo2 foo3

This is extremely handy for, e.g., moving files nested deep in some directory hierarchy. However, a weird thing seems to happen with brace expansion and the results of subshells:

maciek@potato:~/bash$ echo $(echo foo\{1,2,3\})

The expansion is ignored. I’m far from a shell scripting expert, but I can’t figure this out. It seems that it should be analogous to the ‘*’ expansion, but it behaves differently. Any ideas?

Posted in programming. Tags: . 1 Comment »