230

I am working on a script that needs to perform an action in every sub-directory of a specific folder.

What is the most efficient way to write that?

jww
  • 97,681
  • 90
  • 411
  • 885
mikewilliamson
  • 24,303
  • 17
  • 59
  • 90
  • 2
    Please consider coming back through and reevaluating answers for correctness -- you've got an accepted answer getting a lot of views despite major bugs (f/e, running it over a directory where someone previously ran `mkdir 'foo * bar'` will cause `foo` and `bar` to be iterated over even if they don't exist, and the `*` will be replaced with a list of *all* filenames, even non-directory ones). – Charles Duffy May 08 '18 at 16:46
  • 1
    ...even worse is if someone ran `mkdir -p '/tmp/ /etc/passwd /'` -- if someone runs a script following this practice on `/tmp` to, say, find directories to delete, they could end up deleting `/etc/passwd`. – Charles Duffy May 08 '18 at 16:48

10 Answers10

332

A version that avoids creating a sub-process:

for D in *; do
    if [ -d "${D}" ]; then
        echo "${D}"   # your processing here
    fi
done

Or, if your action is a single command, this is more concise:

for D in *; do [ -d "${D}" ] && my_command; done

Or an even more concise version (thanks @enzotib). Note that in this version each value of D will have a trailing slash:

for D in */; do my_command; done
Community
  • 1
  • 1
kanaka
  • 70,845
  • 23
  • 144
  • 140
  • 57
    You can avoid the `if` or `[` with: `for D in */; do` – enzotib Oct 23 '10 at 06:24
  • 4
    +1 because directory names don't begin with ./ as opposed to accepted answer – Hayri Uğur Koltuk Feb 05 '14 at 11:00
  • 4
    This one is correct even up to spaces in the directory names +1 – Alex Reinking May 04 '14 at 19:49
  • 6
    There is one problem with the last command: if you are in a directory without subdirectories; it will return "*/". So better use the second command `for D in *; do [ -d "${D}" ] && my_command; done` or a combination of the two latest: `for D in */; do [ -d $D ] && my_command; done` – Chris Maes Oct 02 '14 at 06:12
  • 5
    Note that this answer ignores hidden directories. To include hidden directories use `for D in .* *; do` instead `for D in *; do`. – patryk.beza May 18 '16 at 17:34
  • note for noobs like me that if you're wanting to do this on a directory that isn't the current, the syntax is `for D in ./directory/*; do` – trueCamelType Aug 07 '19 at 20:53
  • 1
    use `${D%/*}` instead of $D to trim the trailing slash. – Hafthor Oct 05 '19 at 13:04
  • 2
    @Hafthor `${D%/}` is probably safer so extra characters after the `/` aren't accidentally removed. – kanaka Oct 05 '19 at 20:03
188
for D in $(find . -mindepth 1 -maxdepth 1 -type d); do
    //Do whatever you need with D
done
Jay M
  • 3,736
  • 1
  • 24
  • 33
Mike Clark
  • 11,769
  • 6
  • 39
  • 43
133

The simplest non recursive way is:

for d in */; do
    echo "$d"
done

The / at the end tells, use directories only.

There is no need for

  • find
  • awk
  • ...
d0x
  • 11,040
  • 17
  • 69
  • 104
  • 12
    Note: this will not include dot dirs (which can be a good thing, but it important to know). – wisbucky Sep 18 '17 at 18:23
  • Useful to note that you can use `shopt -s dotglob` to include dotfiles/dotdirs when expanding wildcards. See also: https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html – Steen Schütt Aug 09 '18 at 07:47
  • I think you meant `/*` instead of `*/` with `/` representing the path you want to use. – Brōtsyorfuzthrāx Sep 07 '18 at 15:05
  • 2
    @Shule `/*` would be for absolute path whereas `*/` would include the subdirectories from the current location – Dan G Oct 02 '18 at 17:37
  • 5
    helpful hint: if you need to trim the trailing '/' from $d, use `${d%/*}` – Hafthor Oct 05 '19 at 12:59
24

Use find command.

In GNU find, you can use -execdir parameter:

find . -type d -execdir realpath "{}" ';'

or by using -exec parameter:

find . -type d -exec sh -c 'cd -P "$0" && pwd -P' {} \;

or with xargs command:

find . -type d -print0 | xargs -0 -L1 sh -c 'cd "$0" && pwd && echo Do stuff'

Or using for loop:

for d in */; { echo "$d"; }

For recursivity try extended globbing (**/) instead (enable by: shopt -s extglob).


For more examples, see: How to go to each directory and execute a command? at SO

Community
  • 1
  • 1
kenorb
  • 155,785
  • 88
  • 678
  • 743
  • 1
    `-exec {} +` is POSIX-specified, `-exec sh -c 'owd=$PWD; for arg; do cd -- "$arg" && pwd -P; cd -- "$owd"; done' _ {} +` is another legal option, and invokes fewer shells than `-exec sh -c '...' {} \;`. – Charles Duffy Aug 13 '18 at 15:49
14

Handy one-liners

for D in *; do echo "$D"; done
for D in *; do find "$D" -type d; done ### Option A

find * -type d ### Option B

Option A is correct for folders with spaces in between. Also, generally faster since it doesn't print each word in a folder name as a separate entity.

# Option A
$ time for D in ./big_dir/*; do find "$D" -type d > /dev/null; done
real    0m0.327s
user    0m0.084s
sys     0m0.236s

# Option B
$ time for D in `find ./big_dir/* -type d`; do echo "$D" > /dev/null; done
real    0m0.787s
user    0m0.484s
sys     0m0.308s
Sriram Murali
  • 5,804
  • 2
  • 26
  • 32
10

find . -type d -print0 | xargs -0 -n 1 my_command

Paul Tomblin
  • 179,021
  • 58
  • 319
  • 408
7

This will create a subshell (which means that variable values will be lost when the while loop exits):

find . -type d | while read -r dir
do
    something
done

This won't:

while read -r dir
do
    something
done < <(find . -type d)

Either one will work if there are spaces in directory names.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • For even better handling of weird filenames (including names that end with whitespace and/or include linefeeds), use `find ... -print0` and `while IFS="" read -r -d $'\000' dir` – Gordon Davisson Oct 23 '10 at 16:16
  • @GordonDavisson, ...indeed, I'd even argue that `-d ''` is less misleading about bash syntax and capabilities, since `-d $'\000'` implies (falsely) that `$'\000'` is in some way different from `''` -- indeed, one could readily (and again, falsely) infer from it that bash supports Pascal-style strings (length-specified, able to contain NUL literals) rather than C strings (NUL delimited, unable to contain NULs). – Charles Duffy May 08 '18 at 16:43
6

You could try:

#!/bin/bash
### $1 == the first args to this script
### usage: script.sh /path/to/dir/

for f in `find . -maxdepth 1 -mindepth 1 -type d`; do
  cd "$f"
  <your job here>
done

or similar...

Explanation:

find . -maxdepth 1 -mindepth 1 -type d : Only find directories with a maximum recursive depth of 1 (only the subdirectories of $1) and minimum depth of 1 (excludes current folder .)

leesei
  • 6,020
  • 2
  • 29
  • 51
Henry Dobson
  • 212
  • 2
  • 7
  • This is buggy -- try with a directory name with spaces. See [BashPitfalls #1](http://mywiki.wooledge.org/BashPitfalls#for_i_in_.24.28ls_.2A.mp3.29), and [DontReadLinesWithFor](http://mywiki.wooledge.org/DontReadLinesWithFor). – Charles Duffy May 08 '18 at 16:42
  • Directory name with spaces is enclosed in quotes and therefore works and OP is not trying to read lines from file. – Henry Dobson May 09 '18 at 15:50
  • it works in the `cd "$f"`. It doesn't work when the output from `find` is string-split, so you'll have the separate pieces of the name as separate values in `$f`, making how well you do or don't quote `$f`'s expansion moot. – Charles Duffy May 09 '18 at 15:51
  • I didn't say they *were* trying to read lines from a file. `find`'s output is line-oriented (one name to a line, in theory -- but see below) with the default `-print` action. – Charles Duffy May 09 '18 at 15:52
  • Line-oriented output, as from `find -print` is not a *safe* way to pass arbitrary filenames, since one can run something `mkdir -p $'foo\n/etc/passwd\nbar'` and get a directory that has `/etc/passwd` as a separate line in its name. Handling names from files in `/upload` or `/tmp` directories without care is a great way to get privilege escalation attacks. – Charles Duffy May 09 '18 at 15:54
  • In general, not speaking specifically to UNIX or to filenames, to unambiguously distinguish where one string ends and the next one begins in a list, one must either pass length out-of-band or use a sigil as separator that can't be present in one of those strings. The only character that can't be present in a filename on UNIX systems is NUL, so the only way to safely pass a list of filenames is a NUL-delimited list. (Since arguments on UNIX are C strings, and thus NUL-delimited, this is why `find ... -exec` is safe: Arguments passed on an argv list are individually terminated by NULs). – Charles Duffy May 09 '18 at 15:58
  • ...anyhow, you can easily test this yourself. `( tempdir=$(mktemp -d "$PWD"/tempdir.XXXXXX) && cd "$tempdir" && { mkdir 'directory with spaces' $'directory\nwith\nnewlines'; for d in $(find . -type d -print); do echo "Found: $d"; done; }; rm -rf "$tempdir" )` and you'll see each word on its own separate line in the output, with its own `Found:` prefix. – Charles Duffy May 09 '18 at 16:03
6

the accepted answer will break on white spaces if the directory names have them, and the preferred syntax is $() for bash/ksh. Use GNU find -exec option with +; eg

find .... -exec mycommand +; #this is same as passing to xargs

or use a while loop

find .... | while read -r D
do
    # use variable `D` or whatever variable name you defined instead here
done 
Danny Varod
  • 17,324
  • 5
  • 69
  • 111
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 1
    what param will hold the directory name? eg, chmod +x $DIR_NAME (yes, i know there is a chmod option for only directories) – Mike Graf Dec 15 '12 at 00:50
  • There is one subtle difference between `find -exec` and passing to `xargs`: `find` will ignore the exit value of the command being executed, while `xargs` will fail on a nonzero exit. Either might be correct, depending on your needs. – Jason Wodicka Apr 05 '18 at 01:10
  • `find ... -print0 | while IFS= read -r d` is safer -- supports names that begin or end in whitespace, and names that contain newline literals. – Charles Duffy May 08 '18 at 16:45
1

if you want to perform an action INSIDE the folder and not ON folder.

Explanation: You have many pdfs and you would like to concetrate them inside a single folder. my folders

   AV 001/
   AV 002/
  • for D in *; do cd "$D"; # VERY DANGEROUS COMMAND - DONT USE #-- missing "", it will list files too. It can go up too.
  • for d in */; do cd "$d"; echo $d; cd ..; done; # works succesfully
  • for D in "$(ls -d */)"; do cd "$D"; done; # bash: cd: $'Athens Voice 001/\nAthens Voice 002/' - there is no such folder
  • for D in "$(*/)"; do cd "$D"; done; # bash: Athens Voice 001/: is folder
  • for D in "$(`find . -type d`)"; do cd $D; done; # bash: ./Athens: there is no such folder or file
  • for D in *; do if [ -d "${D}" ] then cd ${D}; done; # many arguments
Estatistics
  • 874
  • 9
  • 24