BLayer's answer is correct, but to deconstruct what's really happening here (ignoring the typo of the missing -name
primary):
#!/bin/bash
while IFS= read -r -d '' file; do
files+=$file
done < <(find -type f -name '*.c' -print0)
echo "${files[@]}"
In the shell started by process substitution (<(...)
), the following command is parsed by bash:
find -type f -name '*.c' -print0
Because the glob *.c
is quoted, bash does not expand it. However, the single quotes are stripped off. So when the find
process starts, what it sees as its argument list is:
-type
f
-name
*.c
-print0
Note that these arguments are separated with null bytes, not with spaces or newlines. This is at the C level, not at the shell level. This has to do with how programs are executed using execve()
in C.
Now to contrast, in the following snippet:
#!/bin/bash
find_args="-type f -name '*.c' -print0"
while IFS= read -r -d '' file; do
files+=$file
done < <(find $find_args)
echo "${files[@]}"
The value of the variable find_args
is set to:
-type f -name '*.c' -print0
(The double quote marks are not part of the value, but the single quote characters are.)
When the command find $find_args
is run, as per man bash
, the token $find_args
is subject to parameter expansion followed by word splitting followed by pathname expansion (a.k.a. glob expansion).
After parameter expansion, you have -type f -name '*.c' -print0
. Note that this is after quote removal. So the single quotes will not be removed.
After word splitting, you have the following as separate words:
-type
f
-name
'*.c'
-print0
Then comes pathname expansion. Of course '*.c'
isn't likely to match anything as you don't usually put single quotes in your filenames, so the result would likely be that '*.c'
will be passed as a literal pattern to find
, and thus the -name
primary will fail on all files. (It would succeed only if there is a file whose name starts with a single quote and ends with the three characters .c'
)
Edit: Actually, if there is such a file, the glob '*.c'
will expand to match that file and any other such files and then the expansion [the actual file name] will be passed to find
as a pattern. So whether the -print0
primary will ever be reached or not depends on (a) whether there is only one such filename, and (b) whether that filename, interpreted as a glob, matches itself.
Examples:
If you run touch "'something.c'"
, then the glob '*.c'
will expand to 'something.c'
, and then the find
primary -name 'something.c'
will match that file as well and it will be printed.
If you run touch "'namewithcharset[a].c'"
, the glob '*.c'
will be expanded to that by the shell, but the find
primary -name 'namewithcharset[a].c'
will not match itself—it would only match 'namewithcharseta.c'
, which doesn't exist—so -print0
would not be reached.
If you run touch "'x.c'" "'y.c'"
, the glob '*.c'
will expand to both filenames, which will cause an error to be output from find
because 'y.c'
isn't a valid primary (and it can't be as it doesn't start with a hyphen).
If the nullglob
option is set, you'll get different behavior.
See also:
readarray -d '' files < <(find . -type f -name '*.c' -print0)
. For details on this, you could runhelp readarray
but that will just tell you that readarray is a synonym for mapfile, sohelp mapfile
instead :) – cas Jul 28 '17 at 05:55