4

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 back out to continue onto the next directory.

The output should be returned to new directories with the same structure and name as the original. What is the best way to do this?

Alex
  • 41
  • Hi Alex, welcome to U&L. Please tell us what you have tried first? :) – Kevdog777 Mar 17 '16 at 16:28
  • 1
    For suggestions how to handle the directory changing see http://unix.stackexchange.com/questions/13802/execute-a-specific-command-in-a-given-directory-without-cding-to-it?rq=1 However, the second paragraph is unclear; what exactly do you want to achieve? – Murphy Mar 17 '16 at 16:51

2 Answers2

4

The best way is to use subshells:

for dir in */; do
  (cd -- "$dir" && some command there)
done

Avoid things like:

for dir in */; do
  cd -- "$dir" || continue
  some command there
  cd ..
done

or:

here=$PWD
for dir in */; do
  cd -- "$dir" || continue
  some command here
  cd "$here"
done

As they are less reliable especially when symlinks are involved or the path to the current directory may change while you're running the script.

The "correct" way to do it without involving a subshell would be to open the current directory on a file descriptor with the close-on-exec flag, chdir() into the subdirs, and then go back to the original directory with fchdir(fd). However, no shell that I know has any support for that.

You could do it in perl though:

#! /usr/bin/perl

opendir DIR, "." or die "opendir: $!";
while (readdir DIR) {
  if (chdir($_)) {
    do-something-there;
    chdir DIR || die "fchdir: $!";
  }
}
closedir DIR
cuonglm
  • 153,898
0

With for loops

To iterate over the subdirectories of a directory, you can use a for loop.

for dir in */; do
  echo "Doing something in $dir"
done

The wildcard */ expands to subdirectories in the current directory, and to symbolic links to directories. Directories whose name begins with . (i.e. directories that are dot files) are omitted. If you want to skip symbolic links, you can do so explicitly.

for dir in */; do
  if [ -L "${dir%/}" ]; then continue; fi
  echo "Doing something in $dir"
done

To execute a command on all the files in the directory, use the * wildcard. Once again, this excludes dot files. If the directory is empty, the wildcard is not expanded.

for dir in */; do
  if [ -L "${dir%/}" ]; then continue; fi
  if (set -- "$dir"*; [ "$#" -ne 0 ]); then
    somecommand -- "$dir"*
  fi
done

If you need to call the command separately for each file, use a nested loop. In the following snippet, I use [ -e "$file" ] || [ -L "$file" ] to test whether the file is an existing file (excluding broken symbolic links) or a symbolic link (broken or not); this condition only fails if "$file" is an unexpanded wildcard pattern due to the directory being empty.

for dir in */; do
  if [ -L "${dir%/}" ]; then continue; fi
  for file in "$dir"*; do
    if [ -e "$file" ] || [ -L "$file" ]; then
      somecommand -- "$file"
    fi
  done
done

If the command needs to have the subdirectory as its current directory, there are two ways to do it. You can call cd before and after.

for dir in */; do
  if [ -L "${dir%/}" ]; then continue; fi
  if cd -- "$dir"; then
    for file in *; do
      if [ -e "$file" ] || [ -L "$file" ]; then
        somecommand -- "$file"
      fi
    done
    cd ..
  fi
done

This has the downside that it can fail in edge cases such as the subdirectory being moved during the command's execution or the process not having permission to change into parent directory. To avoid these edge cases, an alternative to cd .. is to run the command in a subshell. A subshell duplicates the state of the original shell, including its current directory, and what happens in the subshell doesn't affect the original shell process.

for dir in */; do
  if [ -L "${dir%/}" ]; then continue; fi
  ( cd -- "$dir" &&
    for file in *; do
      if [ -e "$file" ] || [ -L "$file" ]; then
        somecommand -- "$file"
      fi
    done
  )
  fi
done

With find

As you can see, dealing with the non-nominal cases is not so easy. Some of these are edge cases that you may not be worried about, but an empty directory is something you should be able to deal with. There are easier ways to deal with those cases if you use ksh, bash or zsh; above I showed code that works in plain sh (and that doesn't deal with dot files).

Another way to enumerate files and directories is the find command. It's designed to traverse directory trees recursively, but you can also use it even if there are no subsubdirectories. With GNU or FreeBSD find, i.e. on non-embedded Linux, Cygwin, FreeBSD or OSX, there's an easy way to execute a command on all the files in a directory tree:

find . ! -type d -execdir somecommand {} \;

This executes the command on all the files that aren't directories. To execute it only on regular files (excluding symbolic links and other special types of files):

find . -type f -execdir somecommand {} \;

If you want to exclude the files that are directly in the toplevel directory:

find . -mindepth 2 -type f -execdir somecommand {} \;

If there are subsubdirectories and you don't want to recurse into them:

find . -mindepth 2 -maxdepth 3 -type f -execdir somecommand {} \;