45

I'm on Solaris 10 and I have tested the following with ksh (88), bash (3.00) and zsh (4.2.1).

The following code doesn't yield any result:

function foo {
    echo "Hello World"
}

find somedir -exec foo \;

The find does match several files (as shown by replacing -exec ... with -print), and the function works perfectly when called outside from the find call.

Here's what the man find page say about -exec:

 -exec command       True if the executed command  returns  a
                     zero  value  as  exit status. The end of
                     command must be punctuated by an escaped
                     semicolon (;).  A command argument {} is
                     replaced by the current pathname. If the
                     last  argument  to  -exec  is {} and you
                     specify + rather than the semicolon (;),
                     the command is invoked fewer times, with
                     {} replaced by groups of  pathnames.  If
                     any  invocation of the command returns a
                     non-zero  value  as  exit  status,  find
                     returns a non-zero exit status.

I could probably get away doing something like this:

for f in $(find somedir); do
    foo
done

But I'm afraid of dealing with field separator issues.

Is it possible to call a shell function (defined in the same script, let's not bother with scoping issues) from a find ... -exec ... call?

I tried it with both /usr/bin/find and /bin/find and got the same result.

2
  • did you try exporting the function after you declare it? export -f foo
    – h3rrmiller
    Commented Oct 12, 2012 at 13:41
  • 2
    You will need to make the function an external script and put it in PATH. Alternatively, use sh -c '...' and both define AND run the function in the ... bit. It may help to understand the differences between functions and scripts.
    – jw013
    Commented Oct 12, 2012 at 13:49

4 Answers 4

43

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.

Or instead of passing the function definition on the command line (which would be visible in the output of ps -f for instance), you can pass it via an environment variable:

FUNCDEFS=$(typeset -f foo) find ... -exec ksh -c '
  eval "$FUNCDEFS" &&
    unset -v FUNCDEFS &&
    foo "$@"' ksh {} +

(the unset -v FUNCDEFS to avoid polluting the environment of commands started by that foo function if any).

4
  • 3
    Can you explain why there are two occurrences of ksh or bash in the -exec command? I understand the first, but not the second occurrence. Commented Oct 15, 2012 at 8:13
  • 7
    @danielkullmann In bash -c 'some-code' a b c, $0 is a, $1 is b..., so if you want $@ to be a, b, c you need to insert something before. Because $0 is also used when displaying error messages, it's a good idea to use the name of the shell, or something that makes sense in that context. Commented Oct 15, 2012 at 8:34
  • @StéphaneChazelas, thank you for such nice answer. Commented Jan 20, 2019 at 20:58
  • Remember to export -f some_function as well as export some_var.
    – WesternGun
    Commented Feb 28, 2022 at 15:48
11

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 IFS= read -d '' -r 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.
  • the -r option (raw) prevents backslash escaping in the filename
  • 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.
  • IFS= prevents Bash from stripping trailing whitespace from the filenames (thanks DanielSmedegaardBuus for this correction)

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

4
  • much faster than standard answer but slightly less than using a for loop: time find . -type d -exec bash -c 'dosomething "$0"' {} \; real 0m16.102s time while read -d '' filename; do dosomething "${filename}" </dev/null; done < <(find . -type d -print0) real 0m0.355s time for dir in $(find . -type d); do dosomething $dir; done real 0m0.339s (1465 dirs, on standard hard drive armv7l GNU/Linux synology_armada38x_ds218j) Commented May 31, 2021 at 21:37
  • readable benchmark here Commented May 31, 2021 at 22:04
  • @user1767316 The problem with a for loop is that it requires generating a complete list of files and then iterating over that list, potentially exhausting shell size limits. Reading from a piped input requires no caching, and won't test any limits. It will work regardless of the amount of filenames being discovered by the find command. Commented Dec 22, 2021 at 12:16
  • 1
    Actually, there's one thing missing here, because even with -print0 and -d '', you'd think that funky file names such as those with leading or trailing whitespace would be good, but no. Bash will trim that whitespace off. To prevent this from happening, you can do while IFS= read -d '' filename; do. What a crazy mistress the shell is. OMG. Commented Dec 22, 2021 at 13:45
10

if you want a child process, spawned from your script, to use a pre-defined shell function you need to export it with export -f <function>

NOTE: export -f is bash specific

since only a shell can run shell functions:

find / -exec /bin/bash -c 'function "$1"' bash {} \;

EDIT: essentially your script should resemble this:

#!/bin/bash
function foo() { do something; }
export -f foo
find somedir -exec /bin/bash -c 'foo "$0"' {} \;
4
  • 1
    export -f is a syntax error in ksh, and prints the function definition to the screen in zsh. In all of ksh, zsh and bash, it does not solve the problem.
    – rahmu
    Commented Oct 12, 2012 at 13:48
  • 1
    find / -exec /bin/bash -c 'function' \;
    – h3rrmiller
    Commented Oct 12, 2012 at 13:54
  • Now it works, but only with bash. Thank you!
    – rahmu
    Commented Oct 12, 2012 at 14:42
  • 5
    Never embed {} in the shell code! That means the file name is interpreted as shell code so is very dangerous Commented Jan 6, 2015 at 15:03
7

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.

5
  • I cannot seem to find a globstar option on the version of ksh (ksh88) I'm using.
    – rahmu
    Commented Oct 16, 2012 at 10:04
  • @rahmu Indeed, it's new in ksh93. Doesn't Solaris 10 have a ksh93 somewhere? Commented Oct 16, 2012 at 10:08
  • According to this blog post ksh93 has been introduced in Solaris 11 to replace both the Bourne Shell and ksh88...
    – rahmu
    Commented Oct 16, 2012 at 13:39
  • @rahmu. Solaris has had ksh93 for a while as dtksh (ksh93 with some X11 extensions), but an old version and possibly part of an optional package where CDE is optional. Commented Jan 23, 2013 at 6:56
  • It should be noted that recursive globbing differs from find in that it excludes dotfiles and doesn't descend into dotdirs and that it sorts the file list (both of which can be addresses in zsh through globbing qualifiers). Also with **/* as opposed to ./**/*, filenames may start with -. Commented Jan 23, 2013 at 6:58

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .