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?
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?
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
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.
if [[ "$f" = "*" ]]; then continue; fi
– Piskvor left the building
Apr 01 '19 at 09:02
-d
test will be false, so no worries and you don't have to compare against the pattern.
– Kusalananda
Jan 26 '21 at 19:31
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.
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)
find
. 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
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
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
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)
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
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
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.
-maxdepth 1
option gives me an error on all symlinks: "filename" Too many levels of symbolic links
– rubo77
Oct 21 '13 at 04:21
... -print0 | xargs -0 ...
if you don't know what the exact names are.
– Anthon
Oct 21 '13 at 04:50
find * ....
– Anthon
Oct 21 '13 at 04:56
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
find * | while read file; do ...
– rubo77
Oct 22 '13 at 04:37
find -type d
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
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
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
*
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
dotglob
option would be effective
– rubo77
Mar 06 '21 at 13:46
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:
"$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
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
?
- 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
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
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
ls -d */ | while read d
do
echo $d
done
This
is
one
directory
with
spaces
in
the
name
- but gets parsed as multiple.
– Piskvor left the building
Mar 28 '19 at 11:35
ls -l | grep ^d
or:
ll | grep ^d
You may set it as an alias
ls
-based answer, which lists directories.
– Jeff Schaller
Sep 11 '17 at 20:26
ls
: https://mywiki.wooledge.org/ParsingLs
– codeforester
Aug 21 '18 at 18:38
for d in */ .*/ : do ...
. But how do I loop through hidden files excluding../
? – rubo77 Oct 21 '13 at 03:12set -P
only affects commands which change directory. http://sprunge.us/TNac – Chris Down Oct 22 '13 at 06:14**/
– Aaron Jan 06 '17 at 19:29*/ ;
a little more? I am having trouble understanding exactly that the code is/does. – timbram Jul 28 '17 at 15:22*
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 thedo
from thefor
. – choroba Jul 28 '17 at 16:11shopt -s nullglob
to prevent problems if there is no directory in the current folder – rubo77 Sep 24 '18 at 04:59dir1
), not the full directory path (eg/home/user/dir1
) – Raleigh L. May 11 '23 at 22:39