'find -exec' a shell function in Linux

LinuxBashShellFindBsd

Linux Problem Overview


Is there a way to get find to execute a function I define in the shell?

For example:

dosomething () {
  echo "Doing something with $1"
}
find . -exec dosomething {} \;

The result of that is:

find: dosomething: No such file or directory

Is there a way to get find's -exec to see dosomething?

Linux Solutions


Solution 1 - Linux

Since only the shell knows how to run shell functions, you have to run a shell to run a function. You also need to mark your function for export with export -f, otherwise the subshell won't inherit them:

export -f dosomething
find . -exec bash -c 'dosomething "$0"' {} \;

Solution 2 - Linux

find . | while read file; do dosomething "$file"; done

Solution 3 - Linux

Jac's answer is great, but it has a couple of pitfalls that are easily overcome:

find . -print0 | while IFS= read -r -d '' file; do dosomething "$file"; done

This uses null as a delimiter instead of a linefeed, so filenames with line feeds will work. It also uses the -r flag which disables backslash escaping, and without it backslashes in filenames won't work. It also clears IFS so that potential trailing white spaces in names are not discarded.

Solution 4 - Linux

Add quotes in {} as shown below:

export -f dosomething
find . -exec bash -c 'dosomething "{}"' \;

This corrects any error due to special characters returned by find, for example files with parentheses in their name.

Solution 5 - Linux

Processing results in bulk

For increased efficiency, many people use xargs to process results in bulk, but it is very dangerous. Because of that there was an alternate method introduced into find that executes results in bulk.

Note though that this method might come with some caveats like for example a requirement in POSIX-find to have {} at the end of the command.

export -f dosomething
find . -exec bash -c 'for f; do dosomething "$f"; done' _ {} +

find will pass many results as arguments to a single call of bash and the for-loop iterates through those arguments, executing the function dosomething on each one of those.

The above solution starts arguments at $1, which is why there is a _ (which represents $0).

Processing results one by one

In the same way, I think that the accepted top answer should be corrected to be

export -f dosomething
find . -exec bash -c 'dosomething "$1"' _ {} \;

This is not only more sane, because arguments should always start at $1, but also using $0 could lead to unexpected behavior if the filename returned by find has special meaning to the shell.

Solution 6 - Linux

Have the script call itself, passing each item found as an argument:

#!/bin/bash

if [ ! $1 == "" ] ; then
   echo "doing something with $1"
   exit 0
fi

find . -exec $0 {} \;

exit 0

When you run the script by itself, it finds what you are looking for and calls itself passing each find result as the argument. When the script is run with an argument, it executes the commands on the argument and then exits.

Solution 7 - Linux

For those of you looking for a Bash function that will execute a given command on all files in current directory, I have compiled one from the above answers:

toall(){
    find . -type f | while read file; do "$1" "$file"; done
}

Note that it breaks with file names containing spaces (see below).

As an example, take this function:

world(){
    sed -i 's_hello_world_g' "$1"
}

Say I wanted to change all instances of "hello" to "world" in all files in the current directory. I would do:

toall world

To be safe with any symbols in filenames, use:

toall(){
    find . -type f -print0 | while IFS= read -r -d '' file; do "$1" "$file"; done
}

(but you need a find that handles -print0 e.g., GNU find).

Solution 8 - Linux

Just a warning regaring the accepted answer that is using a shell, despite it well answer the question, it might not be the most efficient way to exec some code on find results:

Here is a benchmark under bash of all kind of solutions, including a simple for loop case: (1465 directories, on a standard hard drive, armv7l GNU/Linux synology_armada38x_ds218j)

dosomething() { echo $1; }

export -f dosomething
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.364s

time find . -type d | while read file; do dosomething "$file"; done 
real    0m0.340s

time for dir in $(find . -type d); do dosomething $dir; done 
real    0m0.337s

"find | while" and "for loop" seems best and similar in speed.

Solution 9 - Linux

It is not possible to executable a function that way.

To overcome this you can place your function in a shell script and call that from find

# dosomething.sh
dosomething () {
  echo "doing something with $1"
}
dosomething $1

Now use it in find as:

find . -exec dosomething.sh {} \;

Solution 10 - Linux

Put the function in a separate file and get find to execute that.

Shell functions are internal to the shell they're defined in; find will never be able to see them.

Solution 11 - Linux

To provide additions and clarifications to some of the other answers, if you are using the bulk option for exec or execdir (-exec command {} +), and want to retrieve all the positional arguments, you need to consider the handling of $0 with bash -c.

More concretely, consider the command below, which uses bash -c as suggested above, and simply echoes out file paths ending with '.wav' from each directory it finds:

find "$1" -name '*.wav' -execdir bash -c 'echo "$@"' _ {} +

The Bash manual says:

> If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, they are assigned to positional parameters, starting with $0.

Here, 'echo "$@"' is the command string, and _ {} are the arguments after the command string. Note that $@ is a special positional parameter in Bash that expands to all the positional parameters starting from 1. Also note that with the -c option, the first argument is assigned to positional parameter $0.

This means that if you try to access all of the positional parameters with $@, you will only get parameters starting from $1 and up. That is the reason why Dominik's answer has the _, which is a dummy argument to fill parameter $0, so all of the arguments we want are available later if we use $@ parameter expansion for instance, or the for loop as in that answer.

Of course, similar to the accepted answer, bash -c 'shell_function "$0" "$@"' would also work by explicitly passing $0, but again, you would have to keep in mind that $@ won't work as expected.

Solution 12 - Linux

I find the easiest way is as follows, repeating two commands in a single do:

func_one () {
  echo "The first thing with $1"
}

func_two () {
  echo "The second thing with $1"
}

find . -type f | while read file; do func_one $file; func_two $file; done

Solution 13 - Linux

Not directly, no. Find is executing in a separate process, not in your shell.

Create a shell script that does the same job as your function and find can -exec that.

Solution 14 - Linux

For reference, I avoid this scenario using:

for i in $(find $dir -type f -name "$name" -exec ls {} \;); do
  _script_function_call $i;
done;

Get the output of find in the current script file and iterate over the output as you may want. I do agree with the accepted answer, but I don't want to expose functions outside of my script file.

Solution 15 - Linux

I would avoid using -exec altogether. Use xargs:

find . -name <script/command you're searching for> | xargs bash -c

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionalxndrView Question on Stackoverflow
Solution 1 - LinuxAdam RosenfieldView Answer on Stackoverflow
Solution 2 - LinuxJacView Answer on Stackoverflow
Solution 3 - LinuxpajamianView Answer on Stackoverflow
Solution 4 - LinuxWagnerView Answer on Stackoverflow
Solution 5 - LinuxDominikView Answer on Stackoverflow
Solution 6 - LinuxMike MareadyView Answer on Stackoverflow
Solution 7 - LinuxJason BasaneseView Answer on Stackoverflow
Solution 8 - Linuxuser1767316View Answer on Stackoverflow
Solution 9 - LinuxcodaddictView Answer on Stackoverflow
Solution 10 - LinuxAngusView Answer on Stackoverflow
Solution 11 - LinuxfiredrillsergeantView Answer on Stackoverflow
Solution 12 - LinuxedibView Answer on Stackoverflow
Solution 13 - LinuxLaurence GonsalvesView Answer on Stackoverflow
Solution 14 - Linuxdinesh sainiView Answer on Stackoverflow
Solution 15 - LinuxBarryView Answer on Stackoverflow