2

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.

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!

So i think it's failing because of some escaping problem. What have I done wrong?

8

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.

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.

Use arrays, and quote the separate arguments to find properly:

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.


Equivalent implementation suitable for /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.


And lastly, the output of your echo test is not an accurate representation of the command that would have been executed by your function.

Consider this:

cat "my file name"

which runs cat on something called my file name, and

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.

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.

  • 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
  • 1
    @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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.