Search code examples
arraysbashglob

Why does "a=( * )" assign an array with one element for each filename in '*' instead of each word?


Question Details

Suppose we have a directory with three files in it: file_1, file_2, and the very inconveniently named file 3. If my understanding of filename expansion is correct, the way bash interprets the string

echo *

is that it sees the (unquoted) *, and modifies the string so that it now reads

echo file_1 file_2 file 3

Then, since there are no more expansions to be performed, bash attempts to evaluate the string. In this case, it runs the command echo, passing to it four arguments: file, 3, file_1, and file_2. In any case, the outputs are identical:

$ echo *
> file 3 file_1 file_2
$ echo file 3 file_1 file_2
> file 3 file_1 file_2

However, in other contexts, this doesn't seem to be what happens. For instance

$ arr1=( * )
$ arr2=( file 3 file_1 file_2 )
$ echo ${#arr1}
> 3
$ echo ${#arr2}
> 4

And yet, if shell expansion works the way it's described in the bash documentation, these ought to be identical.

Something similar happens in a for loop:

$ for f in *; do echo $f; done
> file 3
> file_1
> file_2
$ for f in file 3 file_1 file_2; do echo $f; done
> file
> 3
> file_1
> file_2

What am I missing? Does globbing not happen in these cases?

Use case

I'm putting together a GitHub repo to centralize my dotfiles, following this suggestion from MIT's Hacker Tools. The script I'm writing has two usages:

./install.sh DOTFILE [DOTFILE [DOTFILE ...]]
./install.sh -a

In the first case, each of the named dotfiles in src/config is symlinked to a corresponding dotfile in my home directory; in the second, the -a flag prompts the script to run as if I had entered every dotfile as an argument.

The solution I came up with was to run ln -sih in a for loop using one of two arrays: $@ and *.1 So, simply assign FILES=( $@ ) or FILES=( * ), and then run for f in $FILES--except, it seems to me, * should break in this assignment if there's a filename with a space in it. Clearly bash is smarter than me, since it doesn't, but I don't understand why.


1: Obviously, you don't want the script itself to run through the loop, but that's easy enough to exclude with an if [[ "$f" != "$0" ]] clause.


Solution

  • From the bash documentation you linked to:

    The order of expansions is: brace expansion; tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution (done in a left-to-right fashion); word splitting; and filename expansion.

    Filename expansion happens after word splitting, and therefore the expanded filenames are not themselves subject to further word splitting.