35

Let's say I'm in a directory which has some subdirectories, dir1, dir2, and dir3. Each of these directories have a file foo and I would like to execute the same command on each foo in each of the directories and have that command executed from inside that directory.

If I were to do it "by hand", it would look something like this:

cd dir1
(execute on foo)
cd ../dir2
(execute on foo)
cd ../dir3
(execute on foo)

It is imperative that the command be executed from each of the directories. foo is a batch scheduling script (for HTCondor, if you care to know) and must be executed from each subdirectory so that the output of the runs started by the scheduling scripts will end up in each subdirectory.

The question "Find a file and execute a command in the file's directory", as far as I can tell, does not answer my question. The first answer to that question is more of a workaround which will not work in my case, and the second answer does not have enough of an explanation for me to know how to use it.

0

4 Answers 4

50

Assuming your find supports it, use the -execdir option instead of -exec

find . -type f -name 'foo' -execdir pwd \;

This will change into each directory in turn, and if there's a foo file present there it will run pwd.

NB You cannot repurpose this solution for running commands inside directories named foo. For that you'd need to revert to a simple -exec sh -c '...' approach, iterating across the args supplied to sh and using cd for each in turn. This example runs pwd inside each directory:

find . -type d -name 'foodir' -exec sh -c '
    for d in "$@"; do ( cd "$d" || exit; pwd ); done
    ' _ {} +
4
  • When you start hitting "find: The relative path ‘./node_modules/.bin’ is included in the PATH environment variable,": askubuntu.com/questions/621132/… Apr 9, 2019 at 9:08
  • on WSL doesn't work to do a git pull inside any matching directory, like find . -maxdepth 1 -type d -name "repo-*" -execdir git pull {} +
    – tuxErrante
    Apr 4, 2022 at 8:39
  • @tuxErrante you want to run git pull inside the matching directory, but execdir runs insidenthe directory containing the matching directory (i.e., the parent directory). I suspect that's also uav's problem.
    – muru
    Apr 5, 2022 at 10:34
  • @muru you're right, I've already solved here :) unix.stackexchange.com/a/697912/130710
    – tuxErrante
    Apr 5, 2022 at 10:51
4

Assuming you know exactly what directories to run your command in,

for dir in dir1 dir2 dir3; do
    ( cd "$dir" && somecommand foo )
done

or, looping over the foo files,

for foo in ./*/foo; do
  ( cd "${foo%/foo}" && somecommand foo )
done

The subshell prevents the cd to affect the working directory of the rest of the script. The ${foo%/foo} parameter substitution expands to the name of the directory where the foo file lives (by removing the knows suffix string /foo from the value of $foo).

Using standard find to do the same thing (in the case where the foo files may live deep down in a directory structure):

find . -type f -name foo -exec sh -c '
    for foo do
        ( cd "${foo%/foo}" && somecommand foo )
    done' sh {} +

In words: Find all regular files called "foo" anywhere in the current directory or below and give their pathnames to this shell script in batches. The shell script shall iterate over the given pathnames, change directory to the directory portion of each pathname and if that succeeds, execute somecommand foo.

For a solution that uses the nonstandard -execdir, see roaima's answer. His answer adapted to my example:

find . -type f -name foo -execdir somecommand foo ';'
0

Maybe not beautiful, but I think it does the job

find . -name 'foo' -type f -exec sh -c 'cd $(echo {}|cut -d"/" -f2) && ./foo' \;
1
  • As noted in the comments elsewhere, {} in the sh -c code like that is a command injection vulnerability. Never do that.
    – muru
    Apr 16 at 10:56
-1

Tested in WSL and GitBash over Win 10.

execdir from previous solutions won't work because the command will execute using the current directory as an argument, not into it.
You can just cd into the directory and do what you want.
E.g. this is what I use to update all my repos before starting to work

find $HOME/workspace/ -maxdepth 1 -type d -name "repo*" -exec bash -c "cd \"{}\" && echo ____Pulling from \"{}\" && git pull" \;

EDIT:
If you're not on your personal PC/laptop and need more robust code, you should write a script not injectable with external commands, like:

#!/bin/sh
find "$HOME/workspace/" -maxdepth 1 -type d -exec sh -c '
  for dir in "$@"
  do
    echo "____Pulling from $dir"
    cd "$dir"
    git pull
  done' sh {} +
5
  • 2
    Never embed {} in the code argument of some language interpreter such as bash. That makes it a command injection vulnerability. That's also non-standard and non-portable. See How do I properly escape weird characters in a bash script? or use find -exec with a filename containing a dollar sign ($) or How to escape shell metacharacters automatically with `find` command? and many others here. Apr 4, 2022 at 9:27
  • there is no referring here to production code or server side scripting. I've also added the environment where it works. In the end is the only solution that actually does what OP asked for and it was downvoted :)
    – tuxErrante
    Apr 5, 2022 at 10:26
  • It is enough that a filename contains double quotes followed by a semi-colon, and the rest of the filename can be a shell command that should be executed. Try the first command with a directory called repo"; echo oops;.
    – Kusalananda
    Sep 6, 2022 at 13:58
  • "In the end is the only solution that actually does what OP asked for" - actually, no. Your solution expects a target directory and cd's into that. The OP expects a target file and needs to cd into its parent directory Sep 29, 2023 at 7:05
  • @tuxErrante I've updated my answer to show an example of what I've described Oct 4, 2023 at 7:18

You must log in to answer this question.

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