Bash – Redirection to a globbed file name fails

bashconfigurationio-redirectionshellwildcards

I use Bash 4.3.48(1) in Ubuntu 16.04 (xenial) with a LEMP stack.

I try to create a php.ini overridings file in a version agnostic way with printf.

1) The version agnostic operation fails:

printf "[PHP]\n post_max_size = 200M\n upload_max_filesize = 200M\n cgi.fix_pathinfo = 0" > /etc/php/*/fpm/zz_overrides.ini

The following error is given:

bash: /etc/php/*/zz_overrides.ini: No such file or directory

2) The version gnostic operation succeeds:

printf "[PHP]\n post_max_size = 200M\n upload_max_filesize = 200M\n cgi.fix_pathinfo = 0" > /etc/php/7.0/fpm/zz_overrides.ini

As you can see, both are basically identical besides * vs 7.0.

  • I didn't find a clue about this (regex?) problem in man printf.
  • I searched in Google and found nothing about "allowing regex in printf".

Why does the version agnostic operation fails and is there any bypass?

Edit: If possible, it is most important for me to use a one-line operation.

Best Answer

The behavior of a pattern match in a redirection appears to differ between shells. Of the ones on my system, dash and ksh93 don't expand the pattern, so you get a file name with a literal *. Bash expands it(1), but only if the pattern matches one file. It complains if there are more filenames that match. Zsh works as if you gave multiple redirections, it redirects the output to all matching files.

(1) except when it's non-interactive and in POSIX mode

If you want the output to go to all matching files, you can use tee:

echo ... | tee /path/*/file > /dev/null

If you want it to go to only one file, the problem is to decide which one to use. If you want to check that there's only one file that matches the pattern, expand the whole list and count them.

In bash/ksh:

names=(/path/*/file)
if [ "${#names[@]}" -gt 1 ] ; then
    echo "there's more than one"
else
    echo "there's only one: ${names[0]}"
fi

In standard shell, set the positional parameters and use $# for the count.


Of course, if the pattern doesn't match any file, it's left as-is, and since the glob is in the middle, the result points to a nonexisting directory. It's the same as trying to create /path/to/file, before /path/to exists, it's just here we have /path/* instead, literally, with the asterisk.

To deal with that, you'd have to expand the directory name(s) without the filename, and then append the file name to all the directories. This is a bit ugly...

dirs=(/path/*)
files=( "${dirs[@]/%/\/file}" )

and then we can use that array:

echo ... | tee "${files[@]}" > /dev/null

Or we could take the easy way out and loop over the filename pattern. It's a bit unsatisfactory in the more general case, since it requires running the main command once for each output file, or using a temporary file to hold the output.

for dir in /path/* ; do
    echo ... > "$dir/file"
done 
Related Question