Executing user defined function in a find -exec call

A function is local to a shell, so you'd need find -exec to spawn a shell and have that function defined in that shell before being able to use it. Something like:

find ... -exec ksh -c '
  function foo {
    echo blan: "$@"
  }
  foo "$@"' ksh {} +

bash allows one to export functions via the environment with export -f, so you can do (in bash):

foo() { ...; }
export -f foo
find ... -exec bash -c 'foo "$@"' bash {} +

ksh88 has typeset -fx to export function (not via the environment), but that can only be used by she-bang less scripts executed by ksh, so not with ksh -c.

Another option is to do:

find ... -exec ksh -c "
  $(typeset -f foo)"'
  foo "$@"' ksh {} +

That is, use typeset -f to dump the definition of the foo function inside the inline script. Note that if foo uses other functions, you'll also need to dump them as well.


Use \0 as a delimiter and read the filenames into the current process from a spawned command, like so:

foo() {
  printf "Hello {%s}\n" "$1"
}

while read -d '' filename; do
  foo "${filename}" </dev/null
done < <(find . -maxdepth 2 -type f -print0)

What's going on here:

  • read -d '' reads until the next \0 byte, so you don't have to worry about strange characters in filenames.
  • similarly, -print0 uses \0 to terminate each generated filename instead of \n.
  • cmd2 < <(cmd1) is the same as cmd1 | cmd2 except that cmd2 is run in the main shell and not a subshell.
  • the call to foo is redirected from /dev/null to ensure it doesn't accidentally read from the pipe.
  • $filename is quoted so the shell doesn't try to split a filename that contains whitespace.

Now, read -d and <(...) are in zsh, bash and ksh 93u, but I'm not sure about earlier ksh versions.


This is not always applicable, but when it is, it's a simple solution. Set the globstar option (set -o globstar in ksh93, shopt -s globstar in bash ≥4; it's on by default in zsh). Then use **/ to match the current directory and its subdirectories recursively.

For example, instead of find . -name '*.txt' -exec somecommand {} \;, you can run

for x in **/*.txt; do somecommand "$x"; done

Instead of find . -type d -exec somecommand {} \;, you can run

for d in **/*/; do somecommand "$d"; done

Instead of find . -newer somefile -exec somecommand {} \;, you can run

for x in **/*; do
  [[ $x -nt somefile ]] || continue
  somecommand "$x"
done

When **/ doesn't work for you (because your shell doesn't have it, or because you need a find option that doesn't have a shell analogue), define the function in the find -exec argument.