'find -exec' a shell function in Linux
Solution 1
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
find . | while read file; do dosomething "$file"; done
Solution 3
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
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
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.
Related videos on Youtube
Comments
-
alxndr about 2 years
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 seedosomething
? -
alxndr over 13 yearsWas trying to avoid more files in my ~/bin. Thanks though!
-
SourceSeeker over 13 yearsYou beat me. By the way you can put the braces inside the quotes instead of using
$0
. -
alxndr over 13 yearsWas trying to avoid more files in my ~/bin. Thanks though!
-
alxndr over 13 yearsGotcha; makes sense. Was trying to avoid more files in my ~/bin though.
-
Adam Rosenfield over 13 years@Dennis: Ah, good call, I thought that
{}
argument to find had to be a separate word, but apparently not. -
Adam Rosenfield over 13 years@alxndr: You can get monospaced text in comments by putting backticks (`) around your text.
-
Gordon Davisson over 13 years@alxndr: that'll fail on filenames with double-quotes, backquotes, dollar-signs, some escape combos, etc...
-
Tom almost 12 yearsNice solution. Doesn't require exporting the function or messing around escaping arguments and is presumably more efficient since it's not spawning subshells to execute each function.
-
chepner about 11 yearsKeep in mind, though, that it will break on filenames containing newlines.
-
alxndr about 10 yearsAt the time, IIRC it was in an attempt to reduce the amount of resources used. Think finding millions of empty files and deleting them.
-
gniourf_gniourf over 9 yearsThis is not the correct way to use
{}
. This will break for a filename containing double quotes.touch '"; rm -rf .; echo "I deleted all you files, haha
. Oops. -
kdubs almost 9 yearsI'm trying to make it fail with weird filenames. I'm using this command : find . -exec ls -d "{}" \; I've got a a file named ./file"; rm -rf . ; echo " hi nothing bad seems to be happening
-
Ray Foss over 8 yearsNone of the your global variables will be set in the function you'll either need to export everything or use @Jac's method.
-
Ray Foss over 8 yearsThis is more "shell'ish" as your global variables and functions will be available, without creating an entirely new shell/environment each time. Learned this the hard way after trying Adam's method and running into all sorts of environment problems. This method also does not corrupt your current user's shell with all the exports and requires less dicipline.
-
hraban over 8 yearsNote also that any functions your function might be calling will not be available unless you export -f those as well.
-
Роман Коптев about 8 yearsThe
export -f
will work only in some versions of bash. It's not posix, not crossplatforn,/bin/sh
will have an error with it -
Роман Коптев about 8 yearsIt's good for
/bin/bash
but will not work in/bin/sh
. What's a pity. -
Dominik over 7 yearsI think this could break if the filename has special meaning to the shell. Also it's inconsistent with arguments starting at
$1
. If the mini-script becomes a little more complicated, this could be very confusing. I propose to useexport -f dosomething; find . -exec bash -c 'dosomething "$1"' _ {} \;
instead. -
Dominik over 7 yearsYes, this is very bad. It can be exploited by injections. Very unsafe. Do not use this!
-
user829755 about 7 yearscool idea but bad style: uses same script for two purposes. if you want to reduce the number of files in your bin/ then you could merge all your scripts into a single one that has a big case clause at the start. very clean solution, isn't it?
-
Camusensei about 7 yearsI considered downvoting but the solution in itself is not bad. Please just use correct quoting:
dosomething $1
=>dosomething "$1"
and start your file correctly withfind . -exec bash dosomething.sh {} \;
-
Camusensei about 7 yearsnot to mention this will fail with
find: ‘myscript.sh’: No such file or directory
if started asbash myscript.sh
... -
user5359531 over 5 yearsI keep having problems where, when multiple files are found, some are randomly skipped when doing this. No weird characters in the filenames, either. Using bash 4.2.46, GNU find 4.5.11, RHEL 7
-
user5359531 over 5 yearslooks like
while read
has issues when running commands withssh
; stackoverflow.com/questions/13800225/… -
user5359531 over 5 yearsalso I fixed my issue by changing
while read
for a for loop;for item in $(find . ); do some_function "${item}"; done
-
crobc1 over 5 yearsuser5359531, that won't work with evil filenames since the output of find is expanded onto the command line and thus subject to word splitting. It's basically only reliable to expand "$@" (or array elements or subscripts) after keyword 'in', and the double quotes are essential.
-
Yuhis almost 5 yearsOut of curiosity, what is the role of
"$0"
inbash -c 'dosomething "$0"' {}
or"$1"
inbash -c 'dosomething "$1"' _ {}
? Is there a name for that feature? What should I search for if I wanted more information? -
Adam Rosenfield over 4 years@Yuhis Those are called the positional parameters: gnu.org/software/bash/manual/bash.html#Positional-Parameters . They evaluate to the nth parameter that was passed to the current script or function, with the 0th parameter having the special meaning of the name of the shell. Unfortunately, variables such as these are difficult to search for if you don't already know the name of them; but as a general rule, looking through the Bash manual for these is a good strategy.
-
Richard over 4 yearsthis has size limits
-
sdenham over 4 years@kdubs: Use $0 (unquoted) within the command-string and pass the filename as the first argument:
-exec bash -c 'echo $0' '{}' \;
Note that when usingbash -c
, $0 is the first argument, not the script name. -
sdenham over 4 years@AdamRosenfield I just found that in this particular case (using bash -c command-string ...), $0 is not the shell name, but the first argument following the command-string (and $1 is the 2nd. such argument, etc.) Hence the use of a throwaway parameter (just an underscore) in the example
bash -c 'dosomething "$1"' _ {}
-
sdenham over 4 years@РоманКоптев How fortunate that at least it works in /bin/bash.
-
Arsen Zahray over 3 years
export -f
didn't work for me. which shell is this for? -
forumulator over 3 years+1, this is probaby the best approach. See pajamian's answer for a solution that won't break on newline in filenames.
-
tripleee over 3 yearsThis has all the usual beginner mistakes, and will break on many different types of unusual filenames (filenames with newlines, filenames with backslashes, filenames with irregular whitespace, filenames with wildcard characters in them, etc).
-
tripleee over 3 yearsThis reads like a test case for shellcheck.net - in short, don't do any of this.
-
jarno about 3 years@sdenham You should double quote $0 to avoid word splitting. But in Bash it does not seem to be neccessary to quote
{}
. I guess it is necessary for some shell since they tell you to quote it in manual page offind
. -
jarno about 3 years@DennisWilliamson it is a bad practice, see comments in another answer.
-
William Pursell about 3 yearsThis is the correct approach. There's reallly no concern about additional files in ~/bin; presumably you already have a definition of
dosomething
in a startup file somewhere, and proper maintenance of your startup files will have you splitting them into distinct files anyway, so you might as well put that definition in an executable script. -
Michael over 2 yearsthis also seems like it could have issues if a file name e.g. contains spaces