If the file contains
-name "a.txt" -o -name "b.txt"
and you run
find "$path" -type f \( $(cat file) \)
Then what happens is that the shell runs the output from cat
through word splitting, resulting in the arguments -name
, "a.txt"
, -o
, -name
, and "b.txt"
, with the quotes literally as part of the arguments.
(Note that the quotes also won't protect the whitespace. An input string like "foo bar"
would result in the two fields "foo
and bar"
. Also, the results of splitting would go through filename generation/globbing, so something like foo*.txt
would be expanded then and there, not passed to find
as-is.)
This is because word splitting is rather simple, it only splits on whitespace, it does not run the output through the full shell syntax parsing. (If it did, that would be actively dangerous, since it would also imply always recursively running any expansions, including command substitutions. It would be impossible to use the shell relative safely for arbitrary data.)
If you want to have the part from the file go though the full syntax processing, you'll have to run the command with eval
, e.g.:
eval 'find "$path" -type f \( '"$(cat file)"' \)'
Here, after the outer shell processes the quotes and the command substitution, eval
gets the following string, which is then processed as a shell command, including quote processing and all expansions.
find "$path" -type f \( -name "a.txt" -o -name "b.txt" \)
(Note that this means that if the file contains e.g. "$(reboot)"
, that command will run. However, I left "$path"
to be expanded by the inner shell, so that it will not be double-processed.)
You could do the above in multiple steps, e.g. by assigning the words from the file to an array with eval
, and then using that array in the final command. The idea and the caveats are the same.
You might wish to reconsider the format of the file. Even e.g. putting each argument on a separate line (and nixing the quotes) would make it easier to parse.
E.g. if the file was:
-name
a.txt
-o
-name
foo bar.txt
You could do something like this, where readarray
reads each input line to a separate array element:
#!/bin/bash
readarray -t args < file
find "$path" -type f \( "${args[@]}" \)
(That's still limited in that it can't process filenames with newlines, but those are likely to be rarer, and would produce other issues too.)
Then again, if all you need is -name
matches, you could have the file list only the patterns, and produce the -o -name
boilerplate programmatically.
See also:
bash
as your shell. The function then gets used with one or more quoted names. For examplefindany 'a.txt' 'b.txt'
– Chris Davies Jan 13 '24 at 19:06