247

I am writing a script in bash on Linux and need to go through all subdirectory names in a given directory. How can I loop through these directories (and skip regular files)?

For example:
the given directory is /tmp/
it has the following subdirectories: /tmp/A, /tmp/B, /tmp/C

I want to retrieve A, B, C.

rubo77
  • 19,527
  • 31
  • 134
  • 226
Erik Sapir
  • 23,209
  • 28
  • 81
  • 141
  • This fits here: [How do I loop through only directories in bash?](http://unix.stackexchange.com/questions/86722/how-do-i-loop-through-only-directories-in-bash) – rubo77 Oct 21 '13 at 03:18
  • **Solid array** under [tag:bash], (accepting spaced dirnames): `dirs=(/tmp/*/);dirs=("${dirs[@]%/}");dirs=("${dirs[@]#/tmp/}")`, then `printf "%s\n" "${dirs[@]}"` or `for dir in "${dirs[@]}" ;do ...` – F. Hauri - Give Up GitHub Jan 19 '22 at 08:47

11 Answers11

593

All answers so far use find, so here's one with just the shell. No need for external tools in your case:

for dir in /tmp/*/     # list directories in the form "/tmp/dirname/"
do
    dir=${dir%*/}      # remove the trailing "/"
    echo "${dir##*/}"    # print everything after the final "/"
done
Andreas Louv
  • 46,145
  • 13
  • 104
  • 123
ghostdog74
  • 327,991
  • 56
  • 259
  • 343
  • 4
    Well, yes, 'find' is kind of the Swiss Army knife on *NIX machines to find something file related. But a pure version with bash builtins only is good to know, too. +1 – Boldewyn Jan 22 '10 at 10:37
  • 32
    so `for dir in */; do echo $dir; done` is for directories in the current directory. – Ehtesh Choudhury Apr 03 '13 at 21:25
  • 44
    Would be nice if it could contain an explanation what `dir=${dir%*/}` and `echo ${dir##*/}` is doing. – Jeremy S. Feb 03 '15 at 10:59
  • 1
    `dir=${dir%*/} ` will strip the shortest match of `*/` from the end of dir, but what for? – rubo77 Mar 15 '15 at 16:30
  • 13
    It is because the variable dir will include the backslash at the end. He is just removing it to have a clean name. % will remove the / at the end. ## will remove the / at the beginning. These are operators to handle substrings. – sebastianf182 Oct 23 '15 at 01:59
  • 12
    If there's no match, the loop will be triggered with `/tmp/*/`, it would be wise to include a check to see if the directory actually exists. – Jrs42 Jan 11 '16 at 23:49
  • @rubo77 It's hard to guess whether or not the author realizes that '%*/' is the same as the shorter and clearer '%/' in this context. The former, of course, makes you wonder what it is in front of the trailing slash that he's trying to remove, until you tediously work out that "shortest suffix" effectively means that any leading '*' in the pattern is ignored. – Ron Burk May 17 '16 at 23:46
  • 2
    Perhaps using `echo \`basename $dir\`` would have been more clear for this answer. – Rakurai Jan 19 '19 at 09:47
  • What will happen if /tmp/ is empty !! – zappy Apr 25 '19 at 13:25
  • 1
    [`shopt -s nullglob`](https://www.gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html) is handy here if you want zero iterations when `/tmp` does not exist. – Alejandro C De Baca Jul 15 '19 at 15:40
  • 4
    Be careful: if there is no directory in the specified path, all files in the path will be listed, resulting in unwanted behavior. The solution with `find` is more reliable. – dexteritas Sep 24 '19 at 07:48
  • If there is no directory in a path, no only will the entire file list of that directory be iterated upon, you might have really bad performance if those directories have a large number of files. – Gewthen Dec 17 '20 at 04:10
  • If you want to exclude symlinks to directories, see https://unix.stackexchange.com/a/86724/224565 – Peter T. Mar 08 '22 at 17:16
146
cd /tmp
find . -maxdepth 1 -mindepth 1 -type d -printf '%f\n'

A short explanation:

  • find finds files (quite obviously)

  • . is the current directory, which after the cd is /tmp (IMHO this is more flexible than having /tmp directly in the find command. You have only one place, the cd, to change, if you want more actions to take place in this folder)

  • -maxdepth 1 and -mindepth 1 make sure that find only looks in the current directory and doesn't include . itself in the result

  • -type d looks only for directories

  • -printf '%f\n prints only the found folder's name (plus a newline) for each hit.

Et voilà!

ottomeister
  • 5,415
  • 2
  • 23
  • 27
Boldewyn
  • 81,211
  • 44
  • 156
  • 212
  • By the way: Note, that all answers will find hidden folders, too (that is, folders starting with a dot)! If you don't want this, add ` -regex '\./[^\.].*'` before the printf or exec options. – Boldewyn Jan 21 '10 at 09:23
  • 1
    Helpful — if you need to execute only one command for each hit ;-) — Only I have several commands to execute :-( . – Martin Mar 10 '14 at 13:19
  • 1
    No problem: Adapt this solution: http://transnum.blogspot.de/2008/11/bashs-read-built-in-supports-0-as.html Inside the `while..done` loop you can go crazy. – Boldewyn Mar 10 '14 at 13:35
  • 1
    This is a very nice method for some use cases; `find`'s `-exec` option lets you run any command for each file/directory. – jtpereyda Sep 14 '17 at 21:32
62

You can loop through all directories including hidden directrories (beginning with a dot) with:

for file in */ .*/ ; do echo "$file is a directory"; done

note: using the list */ .*/ works in zsh only if there exist at least one hidden directory in the folder. In bash it will show also . and ..


Another possibility for bash to include hidden directories would be to use:

shopt -s dotglob;
for file in */ ; do echo "$file is a directory"; done

If you want to exclude symlinks:

for file in */ ; do 
  if [[ -d "$file" && ! -L "$file" ]]; then
    echo "$file is a directory"; 
  fi; 
done

To output only the trailing directory name (A,B,C as questioned) in each solution use this within the loops:

file="${file%/}"     # strip trailing slash
file="${file##*/}"   # strip path and leading slash
echo "$file is the directoryname without slashes"

Example (this also works with directories which contains spaces):

mkdir /tmp/A /tmp/B /tmp/C "/tmp/ dir with spaces"
for file in /tmp/*/ ; do file="${file%/}"; echo "${file##*/}"; done
rubo77
  • 19,527
  • 31
  • 134
  • 226
23

Works with directories which contains spaces

Inspired by Sorpigal

while IFS= read -d $'\0' -r file ; do 
    echo $file; ls $file ; 
done < <(find /path/to/dir/ -mindepth 1 -maxdepth 1 -type d -print0)

Original post (Does not work with spaces)

Inspired by Boldewyn: Example of loop with find command.

for D in $(find /path/to/dir/ -mindepth 1 -maxdepth 1 -type d) ; do
    echo $D ;
done
Community
  • 1
  • 1
zpon
  • 1,482
  • 1
  • 15
  • 21
9
find . -mindepth 1 -maxdepth 1 -type d -printf "%P\n"
Ignacio Vazquez-Abrams
  • 776,304
  • 153
  • 1,341
  • 1,358
7

The technique I use most often is find | xargs. For example, if you want to make every file in this directory and all of its subdirectories world-readable, you can do:

find . -type f -print0 | xargs -0 chmod go+r
find . -type d -print0 | xargs -0 chmod go+rx

The -print0 option terminates with a NULL character instead of a space. The -0 option splits its input the same way. So this is the combination to use on files with spaces.

You can picture this chain of commands as taking every line output by find and sticking it on the end of a chmod command.

If the command you want to run as its argument in the middle instead of on the end, you have to be a bit creative. For instance, I needed to change into every subdirectory and run the command latemk -c. So I used (from Wikipedia):

find . -type d -depth 1 -print0 | \
    xargs -0 sh -c 'for dir; do pushd "$dir" && latexmk -c && popd; done' fnord

This has the effect of for dir $(subdirs); do stuff; done, but is safe for directories with spaces in their names. Also, the separate calls to stuff are made in the same shell, which is why in my command we have to return back to the current directory with popd.

Matthew Leingang
  • 2,579
  • 3
  • 20
  • 19
4

a minimal bash loop you can build off of (based off ghostdog74 answer)

for dir in directory/*                                                     
do                                                                                 
  echo ${dir}                                                                  
done

to zip a whole bunch of files by directory

for dir in directory/*
do
  zip -r ${dir##*/} ${dir}
done                                               
Harry Moreno
  • 10,231
  • 7
  • 64
  • 116
4

If you want to execute multiple commands in a for loop, you can save the result of find with mapfile (bash >= 4) as a variable and go through the array with ${dirlist[@]}. It also works with directories containing spaces.

The find command is based on the answer by Boldewyn. Further information about the find command can be found there.

IFS=""
mapfile -t dirlist < <( find . -maxdepth 1 -mindepth 1 -type d -printf '%f\n' )
for dir in ${dirlist[@]}; do
    echo ">${dir}<"
    # more commands can go here ...
done
dexteritas
  • 162
  • 8
4

TL;DR:

(cd /tmp; for d in */; do echo "${d%/}"; done)

Explanation.

There's no need to use external programs. What you need is a shell globbing pattern. To avoid the need of removing /tmp afterward, I'm running it in a subshell, which may or not be suitable for your purposes.

Shell globbing patterns in a nutshell:

  • * Match any non-empty string any number of times.
  • ? Match exactly one character.
  • [...] Matches with a character from between the brackets. You can also specify ranges ([a-z], [A-F0-9], etc.) or classes ([:digit:], [:alpha:], etc.).
  • [^...] Match one of the characters not between the braces.

* If no file names match the pattern, the shell will return the pattern unchanged. Any character or string that is not one of the above represents itself.


Consequently, the pattern */ will match any file name that ends with a /. A trailing / in a file name unambiguously identifies a directory.

The last bit is removing the trailing slash, which is achieved with the variable substitution ${var%PATTERN}, which removes the shortest matching pattern from the end of the string contained in var, and where PATTERN is any valid globbing pattern. So we write ${d%/}, meaning we want to remove the trailing slash from the string represented by d.

Luis Lavaire.
  • 599
  • 5
  • 17
2

find . -type d -maxdepth 1

Anonymous
  • 49,213
  • 1
  • 25
  • 19
  • 3
    this answer is good, except that i receive the full paths, while i want only the names of the directories. – Erik Sapir Jan 21 '10 at 09:09
0

In short, put the results of find into an array and iterate the array and do what you want. Not the quickest but more organized thinking.

#!/bin/bash

cd /tmp

declare -a results=(`find -type d`)

#Iterate the results

for path in ${results[@]}
do
    echo "Your path is $path"
    #Do something with the path..

    if [[ $path =~ "/A" ]]; then
        echo $path | awk -F / '{print $NF}'
        #prints A

    elif [[ $path =~ "/B" ]]; then
        echo $path | awk -F / '{print $NF}'
        #Prints B

    elif [[ $path =~ "/C" ]]; then
        echo $path | awk -F / '{print $NF}'
        #Prints C
    fi
done

This can be reduced to find -type d | grep "/A" | awk -F / '{print $NF}' prints A

find -type d | grep "/B" | awk -F / '{print $NF}' prints B find -type d | grep "/C" | awk -F / '{print $NF}' prints C