38

I'm attempting to write a script that will be run in a given directory with many single level sub directories. The script will cd into each of the sub directories, execute a command on the files in the directory, and cd out to continue onto the next directory. What is the best way to do this?

  • 1
    Given the current level of information given, I see no relevance to youtube-dl. – HalosGhost Dec 6 '14 at 14:39
74
for d in ./*/ ; do (cd "$d" && somecommand); done
  • 11
    So since the answerer has omitted any sort of explanation, I'll attempt one. for d in ./*/ starts a loop that stores every item in ./*/ (a list of files/folders, in this case) in a variable $d. do (cd "$d" && somecommand); starts the body of the loop. Inside the body, it starts a subshell and runs the cd and somecommand commands. Since it is a child shell, the parent shell (the shell from which you're running this command) retains its CWD and other environment variables. done simply closes the loop body. – Qix Dec 6 '14 at 3:25
  • this methodworks for sub directories of directories: for d in ./*/ ; do (cd "$d" && ls); done ,will not work. but, for d in ./*/ ; do (cd "$d" && for d in ./*/ ; do (cd "$d" && ls); done ); done will work. -using ls as the command in this example. – Michael Dimmitt Sep 6 '16 at 13:06
13

The best way is to not use cd at all:

find some/dir -type f -execdir somecommand {} \;

execdir is like exec, but the working directory is different:

   -execdir command {} [;|+]
          Like   -exec,   but  the  specified  command  is  run  from  the
          subdirectory containing the matched file, which is not  normally
          the  directory  in  which  you  started  find.  This a much more
          secure  method  for  invoking  commands,  as  it   avoids   race
          conditions  during resolution of the paths to the matched files.

It is not POSIX.

  • Does this work with aliases? I have one to download certain files but it isn't recognizing it when I type in find */.link -type f -execdir md $(cat .link) {} \; – Something Jones Dec 5 '14 at 15:51
  • @SomethingJones no, find executes those commands, so it wouldn't be aware of aliases. What is md and is the .link a directory? – muru Dec 5 '14 at 15:52
  • .link is a text file that has the URL it needs to download. md is an alias to wget with a bunch of flags set. Is there a way to make it aware of aliases? – Something Jones Dec 5 '14 at 16:15
  • @SomethingJones For your particular use-case, in bash: find . -type f -iname '*.link' -execdir ${BASH_ALIASES[md]} -i {} \; You don't need to do cat with wget, which has an -i flag for reading in an URL from a file. Also this is somewhat different from your original question (since you seem to be interested in only files named .link and not any other files which may be present). – muru Dec 5 '14 at 16:20
  • Do you know how to do that with zsh? I tried what you gave me and am getting a "Bad substitution" error. Also, how would I be able to extract the content of the .link file? I know I don't need it in this case but I imagine I will soon. – Something Jones Dec 5 '14 at 21:13
2
cd -P .
for dir in ./*/
do cd -P "$dir" ||continue
   printf %s\\n "$PWD" >&2
   command && cd "$OLDPWD" || 
! break; done || ! cd - >&2

The above command doesn't need to do any subshells - it just tracks its progress in the current shell by alternating $OLDPWD and $PWD. When you cd - the shell exchanges the value of these two variables, basically, as it changes directories. It also prints the name for each directory as it works there to stderr.

I just had a second look at it and decided I could do a better job with error handling. It will skip a dir into which it cannot cd - and cd will print a message about why to stderr - and it will break w/ a non-zero exit code if your command does not execute successfully or if running command somehow affects its ability to return to your original directory - $OLDPWD. In that case it also does a cd - last - and writes the resulting current working directory name to stderr.

2
pushd subdir
# do stuff
popd

pushd/popd are bash builtins that provide a stack of directories. You can do multiple pushds then multiple popds and get back to where you started. You can also manipulate the stack with dirs.

As a bash builtin, the documentation is in the bash manpage (man 1 bash and search (/) for pushd, n to move forward through search results) or use help pushd help popd help dirs.

1
for D in ./*; do
    if [ -d "$D" ]; then
        cd "$D"
        run_something
        cd ..
    fi
done

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.