ChatGPT解决这个技术问题 Extra ChatGPT

Perform an action in every sub-directory using Bash

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?

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).
...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.

C
Community

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

You can avoid the if or [ with: for D in */; do
+1 because directory names don't begin with ./ as opposed to accepted answer
This one is correct even up to spaces in the directory names +1
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
Note that this answer ignores hidden directories. To include hidden directories use for D in .* *; do instead for D in *; do.
M
Mike Clark
for D in `find . -type d`
do
    //Do whatever you need with D
done

this will break on white spaces
Also, note you need to tweak the find params if you want recursive or non-recursive behavior.
The above answer gave me the self directory as well, so the following worked a bit better for me: find . -mindepth 1 -type d
@JoshC, both variants will break the directory created by mkdir 'directory name with spaces' into four separate words.
I needed to add -mindepth 1 -maxdepth 1 or it went too deep.
d
d0x

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

...


Note: this will not include dot dirs (which can be a good thing, but it important to know).
Useful to note that you can use shopt -s dotglob to include dotfiles/dotdirs when expanding wildcards. See also: gnu.org/software/bash/manual/html_node/The-Shopt-Builtin.html
I think you meant /* instead of */ with / representing the path you want to use.
@Shule /* would be for absolute path whereas */ would include the subdirectories from the current location
helpful hint: if you need to trim the trailing '/' from $d, use ${d%/*}
C
Community

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


-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 '...' {} \;.
S
Sriram Murali

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

P
Paul Tomblin

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


can I feed the return value of that find into a for loop? This is part of a larger script...
@Mike, unlikely. $? will probably get you the status of the find or the xargs command, rather than my_command.
D
Dennis Williamson

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.


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
@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).
l
leesei

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 .)


This is buggy -- try with a directory name with spaces. See BashPitfalls #1, and DontReadLinesWithFor.
Directory name with spaces is enclosed in quotes and therefore works and OP is not trying to read lines from file.
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.
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.
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.
D
Danny Varod

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 

what param will hold the directory name? eg, chmod +x $DIR_NAME (yes, i know there is a chmod option for only directories)
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.
find ... -print0 | while IFS= read -r d is safer -- supports names that begin or end in whitespace, and names that contain newline literals.