Escaping problem with find command
Escaping problem with find command
I need to find everything in a directory, excluding certain subdirectories and files. My script needs to call this as a function:
function findStuff()
# define exclusions
ignore_dirs=("$1" "*foo*") # exclude base dir
ignore_files=("one.txt" "two.txt" "*three*.txt")
# build patterns for find command
dir_pattern=""
file_pattern=""
for i in "$ignore_dirs[@]"; do dir_pattern=$dir_pattern" ! -path "$i""; done
for i in "$ignore_files[@]"; do file_pattern=$file_pattern" ! -name "$i""; done
# find
find "$1 $dir_pattern $file_pattern"
# now do other stuff with the results...
findStuff /some/base/dir
But this gives me a No such file or directory
error.
No such file or directory
So I wanted to see what the command actually was and tried echo find "$1 $dir_pattern $file_pattern"
and pasted that on the command line and it worked. Then I pasted that into the script and run it, and it also worked!
echo find "$1 $dir_pattern $file_pattern"
So i think it's failing because of some escaping problem. What have I done wrong?
1 Answer
1
find
will use the first arguments (up to the first argument that starts with -
or that is !
or (
) that it gets as the top-level paths to search. You are giving find
a single argument when you call it in your function, the string $1 $dir_pattern $file_pattern
(with the variables expanded). This path is not found.
find
-
!
(
find
$1 $dir_pattern $file_pattern
You also include literal double quotes in the arguments that you intend do give to find
. Double quoting is done to prevent the shell from expanding glob patterns and from splitting on whitespaces (or whatever the IFS
variable contains), but if you use e.g. ! -name "thing"
then the double quotes would be part of the pattern that find
uses to compare against the filenames.
find
IFS
! -name "thing"
find
Use arrays, and quote the separate arguments to find
properly:
find
myfind ()
local ignore_paths=( "$1" "*foo*" )
local ignore_names=( "one.txt" "two.txt" "*three*.txt" )
local path_args=()
for string in "$ignore_paths[@]"; do
path_args+=( ! -path "$string" )
done
local name_args=()
for string in "$ignore_names[@]"; do
name_args+=( ! -name "$string" )
done
find "$1" "$path_args[@]" "$name_args[@]"
Each time we append to path_args
and name_args
above, we add three elements to the list, !
, -path
or -name
, and "$string"
. When expanding "$path_args[@]"
and "$name_args[@]"
(note the double quotes), the elements will be individually quoted.
path_args
name_args
!
-path
-name
"$string"
"$path_args[@]"
"$name_args[@]"
Equivalent implementation suitable for /bin/sh
:
/bin/sh
myfind () (
topdir=$1
set --
# paths to ignore
for string in "$topdir" "*foo*"; do
set -- "$@" ! -path "$string"
done
# names to ignore
for string in "one.txt" "two.txt" "*three*.txt"; do
set -- "$@" ! -name "$string"
done
find "$topdir" "$@"
)
In the sh
shell we only have a single array available to us, which is the list of positional parameters, $@
, so we collect our find
options in that. The bash
-specific solution could also be written to use a single array, obviously, and the sh
variation would run in bash
too.
sh
$@
find
bash
sh
bash
And lastly, the output of your echo
test is not an accurate representation of the command that would have been executed by your function.
echo
Consider this:
cat "my file name"
which runs cat
on something called my file name
, and
cat
my file name
echo cat "my file name"
which outputs the string cat my file name
. This is due to the fact that the shell removes quotes around strings before executing the command. Running that command, cat
would look for three files, not one.
cat my file name
cat
Your command worked well when you copy-pasted it into the shell, because you included the literal double quotes in the string that was outputted by echo
(by escaping them), but that was not the actual command executed by your function.
echo
-name "somefile.txt"
-name 'somefile.txt'
@lonix The arguments to
-name
and -path
should be quoted to prevent word splitting and filename globbing, yes. I do quote these ("$string"
in my code). The double quotes will be stripped off by the shell before the argument is handed over to find
so find
will see the "raw contents" of $string
but no quotes. This is how it should be.– Kusalananda
Sep 12 '18 at 12:48
-name
-path
"$string"
find
find
$string
Thanks for contributing an answer to Unix & Linux Stack Exchange!
But avoid …
To learn more, see our tips on writing great answers.
Required, but never shown
Required, but never shown
By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.
What I don't get is the man pages said I should quote the args, so for example
-name "somefile.txt"
or-name 'somefile.txt'
that is why I quoted above. In your code this is not done, but it works perfectly anyway... why?– lonix
Sep 12 '18 at 12:46