Assign results of globbing to a variable in Bash

2020-02-26 14:23发布

问题:

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?

回答1:

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.



回答2:

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.



回答3:

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*


回答4:

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.



标签: bash glob