'find -exec' a shell function in Linux

93,442

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.

Share:
93,442

Related videos on Youtube

alxndr
Author by

alxndr

open-source developer

Updated on April 09, 2022

Comments

  • alxndr
    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 see dosomething?

  • alxndr
    alxndr over 13 years
    Was trying to avoid more files in my ~/bin. Thanks though!
  • SourceSeeker
    SourceSeeker over 13 years
    You beat me. By the way you can put the braces inside the quotes instead of using $0.
  • alxndr
    alxndr over 13 years
    Was trying to avoid more files in my ~/bin. Thanks though!
  • alxndr
    alxndr over 13 years
    Gotcha; makes sense. Was trying to avoid more files in my ~/bin though.
  • Adam Rosenfield
    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
    Adam Rosenfield over 13 years
    @alxndr: You can get monospaced text in comments by putting backticks (`) around your text.
  • Gordon Davisson
    Gordon Davisson over 13 years
    @alxndr: that'll fail on filenames with double-quotes, backquotes, dollar-signs, some escape combos, etc...
  • Tom
    Tom almost 12 years
    Nice 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
    chepner about 11 years
    Keep in mind, though, that it will break on filenames containing newlines.
  • alxndr
    alxndr about 10 years
    At 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
    gniourf_gniourf over 9 years
    This 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
    kdubs almost 9 years
    I'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
    Ray Foss over 8 years
    None 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
    Ray Foss over 8 years
    This 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
    hraban over 8 years
    Note also that any functions your function might be calling will not be available unless you export -f those as well.
  • Роман Коптев
    Роман Коптев about 8 years
    The 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 years
    It's good for /bin/bash but will not work in /bin/sh. What's a pity.
  • Dominik
    Dominik over 7 years
    I 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 use export -f dosomething; find . -exec bash -c 'dosomething "$1"' _ {} \; instead.
  • Dominik
    Dominik over 7 years
    Yes, this is very bad. It can be exploited by injections. Very unsafe. Do not use this!
  • user829755
    user829755 about 7 years
    cool 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
    Camusensei about 7 years
    I considered downvoting but the solution in itself is not bad. Please just use correct quoting: dosomething $1 => dosomething "$1" and start your file correctly with find . -exec bash dosomething.sh {} \;
  • Camusensei
    Camusensei about 7 years
    not to mention this will fail with find: ‘myscript.sh’: No such file or directory if started as bash myscript.sh...
  • user5359531
    user5359531 over 5 years
    I 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
    user5359531 over 5 years
    looks like while read has issues when running commands with ssh; stackoverflow.com/questions/13800225/…
  • user5359531
    user5359531 over 5 years
    also I fixed my issue by changing while read for a for loop; for item in $(find . ); do some_function "${item}"; done
  • crobc1
    crobc1 over 5 years
    user5359531, 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
    Yuhis almost 5 years
    Out of curiosity, what is the role of "$0" in bash -c 'dosomething "$0"' {} or "$1" in bash -c 'dosomething "$1"' _ {}? Is there a name for that feature? What should I search for if I wanted more information?
  • Adam Rosenfield
    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
    Richard over 4 years
    this has size limits
  • sdenham
    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 using bash -c, $0 is the first argument, not the script name.
  • sdenham
    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
    sdenham over 4 years
    @РоманКоптев How fortunate that at least it works in /bin/bash.
  • Arsen Zahray
    Arsen Zahray over 3 years
    export -f didn't work for me. which shell is this for?
  • forumulator
    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
    tripleee over 3 years
    This 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
    tripleee over 3 years
    This reads like a test case for shellcheck.net - in short, don't do any of this.
  • jarno
    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 of find.
  • jarno
    jarno about 3 years
    @DennisWilliamson it is a bad practice, see comments in another answer.
  • William Pursell
    William Pursell about 3 years
    This 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
    Michael over 2 years
    this also seems like it could have issues if a file name e.g. contains spaces