My colleague, Ryan, came to me with a bug in his Bash script, and I identified the problem with this test:
$ mkdir ryan
$ mkdir ryan/smells-bad
$ FOO=ryan/smells-*
$ echo $FOO
ryan/smells-bad
$ touch $FOO/rotten_eggs
touch: cannot touch `ryan/smells-*/rotten_eggs': No such file or directory
From this I infer that the globbing happens during the echo command, not when the variable FOO is created.
We have a couple of workarounds, in descending order of ungracefulness:
touch `echo $FOO`/rotten_eggs
Or:
pushd
cd $FOO
touch rotten_eggs
popd
But neither is satisfying. Am I missing a trick?
The problem is that the glob will only expand if the file "rotten_eggs" exists, because it is included in the glob pattern. You should use an array.
FOO=( ryan/smells-* )
touch "${FOO[@]/%//rotten_eggs}"
The FOO array contains everything matched by the glob. The expansion using % appends /rotten_eggs to each element.
Consider
for dir in $FOO; do
touch "$dir/rotten_eggs"
done
Note that this will touch
multiple files if the glob pattern matches more than one pathname.
The code as intended with the result of the glob assigned to the variable would be like this:
$ mkdir ryan
$ mkdir ryan/smells-bad
$ FOO=(ryan/smells-*)
$ echo "${FOO[@]}"
ryan/smells-bad
$ echo "$FOO"
ryan/smells-bad
$ touch "$FOO/rotten_eggs"
$ ls -l "$FOO"
total 0
-rw-r--r-- 1 ryan ryan 0 Mar 1 11:17 rotten_eggs
$FOO
is actually an array here, but $FOO also works to get the first element of the array.
but, see how the glob can match more than one file (hence the array is a good idea)
$ mkdir ryan/clean
$ FOO=(ryan/*)
$ echo "$FOO"
ryan/clean
$ echo "${FOO[@]}"
ryan/clean ryan/smells-bad
In these cases the results of the glob is assigned to the variable as desired, rather than the variable being expanded as a glob at point of use.
Of course this means that the variable should really always be used in double quotation marks "..."
otherwise if the filename itself (the glob expansion) also had a *
in it, it would glob again.
e.g.
$ touch ryan/'*ea*'
$ FOO=(ryan/*ea*)
$ echo "${FOO[@]}"
ryan/clean ryan/*ea*
$ echo ${FOO[@]}
ryan/clean ryan/clean ryan/*ea*
I would do it like this:
for FOO in ryan/smells-*; do
touch "$FOO"/rotten_eggs
done
This way $FOO
contains the actual directory name, not the glob pattern. If there's more than one match, though, it will only contain the last one after the loop, so the array solution might be better for that case.