502

I have a folder with some directories and some files (some are hidden, beginning with dot).

for d in *; do
 echo $d
done

will loop through all files and directories, but I want to loop only through directories. How do I do that?

rubo77
  • 27,777
  • 43
  • 130
  • 199

14 Answers14

685

You can specify a slash at the end to match only directories:

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

If you want to exclude symlinks, use a test to continue the loop if the current entry is a link. You need to remove the trailing slash from the name in order for -L to be able to recognise it as a symbolic link:

for d in */ ; do
    [ -L "${d%/}" ] && continue
    echo "$d"
done
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
choroba
  • 45,735
  • 7
  • 84
  • 110
  • 2
    You can loop through hidden files too with `for d in */ .*/ : do ...`. But how do I loop through hidden files excluding `../`? – rubo77 Oct 21 '13 at 03:12
  • to use only the physical system (not symlinks), another option is 'set -P' – AsymLabs Oct 21 '13 at 04:45
  • 1
    @AsymLabs That's incorrect, `set -P` only affects commands which change directory. http://sprunge.us/TNac – Chris Down Oct 22 '13 at 06:14
  • If you want this to be recursive you can use `**/` – Aaron Jan 06 '17 at 19:29
  • 1
    Can someone explain the `*/ ;` a little more? I am having trouble understanding exactly that the code is/does. – timbram Jul 28 '17 at 15:22
  • 3
    @timbram: `*` is a wildcard, it stands for "anything". `/` means the "anything" must be a directory in order to match the wildcard expression. `;` is part of the for loop, it separates the `do` from the `for`. – choroba Jul 28 '17 at 16:11
  • 3
    Add `shopt -s nullglob` to prevent problems if there is no directory in the current folder – rubo77 Sep 24 '18 at 04:59
  • For those curious, this answer prints the directory's _relative_ name (eg `dir1`), not the full directory path (eg `/home/user/dir1`) – Raleigh L. May 11 '23 at 22:39
132

You can test with -d:

for f in *; do
    if [ -d "$f" ]; then
        # $f is a directory
    fi
done

This is one of the file test operators.

goldilocks
  • 86,451
  • 30
  • 200
  • 258
71

Beware that choroba's solution, though elegant, can elicit unexpected behavior if no directories are available within the current directory. In this state, rather than skipping the for loop, bash will run the loop exactly once where d is equal to */:

#!/usr/bin/env bash

for d in */; do
    # Will print */ if no directories are available
    echo "$d"
done

I recommend using the following to protect against this case:

#!/usr/bin/env bash

for f in *; do
    if [ -d "$f" ]; then
        # Will not run if no directories are available
        echo "$f"
    fi
done

This code will loop through all files in the current directory, check if f is a directory, then echo f if the condition returns true. If f is equal to */, echo "$f" will not execute.

roaima
  • 107,089
  • 14
  • 139
  • 261
emagdne
  • 811
  • 6
  • 4
35

If you need to select more specific files than only directories use find and pass it to while read:

shopt -s dotglob
find * -prune -type d | while IFS= read -r d; do 
    echo "$d"
done

Use shopt -u dotglob to exclude hidden directories (or setopt dotglob/unsetopt dotglob in zsh).

IFS= to avoid splitting filenames containing one of the $IFS, for example: 'a b'

see AsymLabs answer below for more find options


edit:
In case you need to create an exit value from within the while loop, you can circumvent the extra subshell by this trick:

while IFS= read -r d; do 
    if [ "$d" == "something" ]; then exit 1; fi
done < <(find * -prune -type d)
rubo77
  • 27,777
  • 43
  • 130
  • 199
  • 3
    I found this solution on: http://stackoverflow.com/a/8489394/1069083 – rubo77 Oct 22 '13 at 05:06
  • 1
    [Don't loop over the output of `find`](https://unix.stackexchange.com/questions/321697). It doesn't matter that you set `IFS` to an empty string, it will still break on filenames containing newlines. It doesn't matter that these filenames are rare, if it's easy to write code that copes with _all_ filenames, then there's no reason to write code that doesn't. – Kusalananda Jan 26 '21 at 19:32
  • @Kusalananda "Don't loop over the output of `find`" is misleading. It is fine, provided that output elements are not line-terminated (the default), rather null-terminated. This would require changing the IFS to the 0-byte OR making `read` handle it with `read -d`. Then even filenames containing newlines would be processed correctly. – Jonathan Komar Mar 04 '21 at 06:49
  • @JonathanKomar It is not misleading as none of the precautions are taken in this answer. Changing `IFS` to contain a nul character implies a shell that can store these in variables. The `bash` shell does not do that. `read -d` is `bash`-specific (`xargs -0` would be a better fit as it isn't dependent on the shell, even though it's still not standard). My point is that if you can do it right, in a way that is portable and safe, then there is no reason to make it unportable and/or unsafe. `find` has `-exec` for the very reason to provide a way to iterate over found pathnames with user code! – Kusalananda Mar 04 '21 at 08:10
  • @kusalananda: please provide a separate answer with your solution, so we can upvote it – rubo77 Mar 04 '21 at 08:31
  • @rubo77 I did, just now. – Kusalananda Mar 04 '21 at 08:55
14

You can use pure bash for that, but it's better to use find:

find . -maxdepth 1 -type d -exec echo {} \;

(find additionally will include hidden directories)

rush
  • 27,055
  • 7
  • 87
  • 112
  • 10
    Note that it doesn't include symlinks to directories. You can use `shopt -s dotglob` for `bash` to include hidden directories. Yours will also include `.`. Also note that `-maxdepth` is not a standard option (`-prune` is). – Stéphane Chazelas Aug 14 '13 at 16:11
  • 3
    The `dotglob` option is interesting but `dotglob` only applies to the use of `*`. `find .` will always include hidden directories (and the current dir as well) – rubo77 Oct 22 '13 at 05:28
9

This is done to find both visible and hidden directories within the present working directory, excluding the root directory:

to just loop through directories:

 find -path './*' -prune -type d

to include symlinks in the result:

find -L -path './*' -prune -type d

to do something to each directory (excluding symlinks):

find -path './*' -prune -type d -print0 | xargs -0 <cmds>

to exclude hidden directories:

find -path './[^.]*' -prune -type d

to execute multiple commands on the returned values (a very contrived example):

find -path './[^.]*' -prune -type d -print0 | xargs -0 -I '{}' sh -c \
"printf 'first: %-40s' '{}'; printf 'second: %s\n' '{}'"

instead of 'sh -c' can also use 'bash -c', etc.

rubo77
  • 27,777
  • 43
  • 130
  • 199
AsymLabs
  • 2,667
  • 1
  • 11
  • 11
  • What happens if the echo '*/' in 'for d in echo */' contains, say, 60,000 directories? – AsymLabs Oct 21 '13 at 04:05
  • You will get "Too many files" error but that can be solved: [How to circumvent “Too many open files” in debian](http://unix.stackexchange.com/questions/85457/how-to-circumvent-too-many-open-files-in-debian) – rubo77 Oct 21 '13 at 04:09
  • thank you for the link - what I was alluding to is that it is better to avoid this problem. – AsymLabs Oct 21 '13 at 04:20
  • -L without ` -maxdepth 1 ` option gives me an error on **all** symlinks: `"filename" Too many levels of symbolic links` – rubo77 Oct 21 '13 at 04:21
  • +1 for xargs (build and execute command lines from standard input) – rubo77 Oct 21 '13 at 04:29
  • 1
    The xargs example only would works for a subset of directory names 9e.g. those with spaces or newlines would not work). Better to use `... -print0 | xargs -0 ...` if you don't know what the exact names are. – Anthon Oct 21 '13 at 04:50
  • How do I exclude the directory `.` from the result? – rubo77 Oct 21 '13 at 04:54
  • @rubo77 if you don't have any directories under the current directory with names that start with a dot you can do `find * ....` – Anthon Oct 21 '13 at 04:56
  • OK, so I edited your answer (that is a must, if you want to do something to all dirs in a folder) – rubo77 Oct 21 '13 at 05:01
  • @rubo77 thanks for the edit - tried to revise the whole post based upon yours and Anthons comments, and to make it POSIX friendly. – AsymLabs Oct 21 '13 at 06:54
  • **`xargs` can only execute one operation in one command**, so I think you have to create a function and then call that in xargs. can you add en example? – rubo77 Oct 21 '13 at 09:59
  • 1
    @rubo77 added way to exclude hidden files/directories and execution of multiple commands - of course this can be done with a script too. – AsymLabs Oct 21 '13 at 11:22
  • 2
    I added an example in my answer at the bottom, how to do something to each directory using a function with `find * | while read file; do ...` – rubo77 Oct 22 '13 at 04:37
  • If you don't want to over-engineer the problem, you might get away with something as simple as this: `find -type d` – Hopping Bunny Jul 10 '19 at 07:34
4

You can loop through all directories including hidden directories (beginning with a dot) in one line and multiple commands with:

for name in */ .*/ ; do printf '%s is a directory\n' "$name"; done

If you want to exclude symbolic links:

for name in *; do 
  if [ -d "$name" ] && [ ! -L "$name" ]; then
    printf '%s is a directory\n' "$name"
  fi 
done

Note: Using the list */ .*/ works in bash, but also displays the folders . and .. while in zsh it will not show these but throw an error if there is no hidden file in the folder


A cleaner version that will include hidden directories and exclude ../ will be with the dotglob shell option in bash:

shopt -s dotglob nullglob
for name in */ ; do printf '%s is a directory\n' "$name"; done

The nullglob shell option makes the pattern disappear completely (instead of remaining unexpanded) if no name matches it. (Use the pattern *(ND/) in the zsh shell; the / makes the preceding * match only directories, and the ND makes it act as if both nullglob and dotglob were set)

You may unset dotglob and nullglob with

shopt -u dotglob nullglob
Kusalananda
  • 320,670
  • 36
  • 633
  • 936
rubo77
  • 27,777
  • 43
  • 130
  • 199
2

Use find with -exec to loop through the directories and call a function in the exec option:

dosomething () {
  echo "doing something with $1"
}
export -f dosomething
find ./* -prune -type d -exec bash -c 'dosomething "$0"' {} \;

Use shopt -s dotglob or shopt -u dotglob to include/exclude hidden directories

rubo77
  • 27,777
  • 43
  • 130
  • 199
  • The `nullglob` and `dotglob` shell options would unfortunately not make any difference here as the shell is never used for doing globbing. The `-path './*'` test would be true for all pathnames as all are found under the `.` directory (the default search path with GNU `find` if no search path is given). – Kusalananda Mar 04 '21 at 09:11
  • Thx, I removed the`-path` – rubo77 Mar 06 '21 at 12:23
  • Well, now it would look in a directory called `*` in the current directory (due to the quoting). Removing the single quotes would make it search each non-hidden name in the current directory. You probably want just `.` instead. – Kusalananda Mar 06 '21 at 12:33
  • 1
    Thx, I removed the single quotes. I think now the `dotglob` option would be effective – rubo77 Mar 06 '21 at 13:46
1

This answer is assuming that all that needs to be done is to find the sub-directories of some top-level directory.

For an answer specific to bash (and to some degree, the zsh shell), see the second part of rubo77's wiki answer, where dotglob and nullglob is being used to correctly loop over the directories (or symbolic links to directories) in a single directory.

For a portable solution that does not depend on bash for doing the actual selection of files, you may use find to loop over all directories in some top-level directory like so:

topdir=.

find "$topdir" ! -path "$topdir" -prune -type d -exec sh -c '
    for dirpath do
        # user code goes here, using "$dirpath"
        printf "\"%s\" is a directory\n" "$dirpath"
    done' sh {} +

The above code assumes that the top directory that we're interested in is the current directory (.). Set topdir to some other pathname to use it on another directory.

The find utility is asked to ignore pathnames that are identical to the starting search path with ! -path "$topdir", and then to prune (not enter) any other pathnames found. This limits the search to only the directory referenced by $topdir.

Pathnames that are directories (-type d) are collected and a sh -c script is executed with batches of these as arguments. The sh -c script is the code that we'd like to execute on all the directories, so it iterates over its given arguments and does whatever it needs to do with each of them. If you need to do something that requires bash, you would obviously use bash -c instead.

The sh -c script could be separated out into its own script like so:

#!/bin/sh

for dirpath do
    # user code goes here, using "$dirpath"
    printf '"%s" is a directory\n' "$dirpath"
done

... which would be called from find like so:

find "$topdir" ! -path "$topdir" -prune -type d -exec ./myscript.sh {} +

See also:

Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • You could use `find "$topdir"/*` without `! -path "$topdir"` – rubo77 Mar 06 '21 at 12:20
  • @rubo77 Doing that would make it search all the pathnames matching `"$topdir"/*`. This pattern would not match hidden names under `$topdir` and would therefore not find any hidden subdirectories in that directory. – Kusalananda Mar 06 '21 at 12:41
1

Lists only directories. inclusive with spaces in names include hidden directory -A if needed:

grep '/$' <(ls -AF)

Possible sequels:

| xargs -I{} echo {}
| while read; do echo $REPLY; done

But judging by the tricky question, it meant the output of directories in the for loop and by means of the shell:

for i in */ .[^.]*/; do
        [ -h "${i%?}" ] || echo $i
done
nezabudka
  • 2,376
  • 5
  • 15
  • Can you explain `[ -h "${i%?}" ]` please? – rubo77 Sep 09 '21 at 00:06
  • `?` - matches any single character. It's similar `${i%/}`. Removes the trailing slash - the directory indicator. Without this, the `-h` and `-L` options in `test` do not work. – nezabudka Sep 09 '21 at 05:26
  • So why don't you use / then? What else would you want to remove at the end? – rubo77 Sep 14 '21 at 19:48
  • This is just one of two equivalent options in this instance because there can only be a slash at the end. This is the first thing that came to my mind. – nezabudka Sep 14 '21 at 20:25
0

This will include the complete path in each directory in the list:

for i in $(find $PWD -maxdepth 1 -type d); do echo $i; done
rubo77
  • 27,777
  • 43
  • 130
  • 199
-1

This lists all the directories together with the number of sub-directories in a given path:

for directory in */ ; do D=$(readlink -f "$directory") ; echo $D = $(find "$D" -mindepth 1 -type d | wc -l) ; done
pabloa98
  • 99
  • 3
-3
ls -d */ | while read d
do
        echo $d
done
dcat
  • 113
  • 2
-6
ls -l | grep ^d

or:

ll | grep ^d

You may set it as an alias

Michael Mrozek
  • 91,316
  • 38
  • 238
  • 232
  • 2
    Unfortunately, I don't think this answers the question, which was "I want to loop only through directories" -- which is slightly different than this `ls`-based answer, which *lists* directories. – Jeff Schaller Sep 11 '17 at 20:26
  • 3
    It is never a good idea to parse the output of `ls`: https://mywiki.wooledge.org/ParsingLs – codeforester Aug 21 '18 at 18:38