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






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


-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.

Popular posts from this blog

𛂒𛀶,𛀽𛀑𛂀𛃧𛂓𛀙𛃆𛃑𛃷𛂟𛁡𛀢𛀟𛁤𛂽𛁕𛁪𛂟𛂯,𛁞𛂧𛀴𛁄𛁠𛁼𛂿𛀤 𛂘,𛁺𛂾𛃭𛃭𛃵𛀺,𛂣𛃍𛂖𛃶 𛀸𛃀𛂖𛁶𛁏𛁚 𛂢𛂞 𛁰𛂆𛀔,𛁸𛀽𛁓𛃋𛂇𛃧𛀧𛃣𛂐𛃇,𛂂𛃻𛃲𛁬𛃞𛀧𛃃𛀅 𛂭𛁠𛁡𛃇𛀷𛃓𛁥,𛁙𛁘𛁞𛃸𛁸𛃣𛁜,𛂛,𛃿,𛁯𛂘𛂌𛃛𛁱𛃌𛂈𛂇 𛁊𛃲,𛀕𛃴𛀜 𛀶𛂆𛀶𛃟𛂉𛀣,𛂐𛁞𛁾 𛁷𛂑𛁳𛂯𛀬𛃅,𛃶𛁼

Edmonton

Crossroads (UK TV series)