Perform an action in every sub-directory using Bash - 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?

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

for D in `find . -type d`
do
//Do whatever you need with D
done

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

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

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

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

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.

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

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

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

Related

How to stop Bash expansion of '*.h" in a function?

In trying to run the following function—Bash is expanding my variable in an unexpected way—thus preventing me from getting my expected result.
It comes down to the way bash deals with a "*.h" which I am passing in to the function.
Here is the function I call:
link_files_of_type_from_directory "*.h" ./..
And where I would expect this variable to stay this way all the way through at some point, by the time it hits the echo $command_to_run; part of my Bash script...this variable has expanded to...
MyHeader1.h MyHeader2.h MyHeader3.h
and so on.
What I want is for Bash to not expand my files so that my code runs the following:
find ./.. -type f -name '*.h'
Instead of
find ./.. -type f -name MyHeader1.h MyHeader2.h MyHeader3.h
This is the code:
function link_files_of_type_from_directory {
local file_type=$1;
local directory_to_link=$2;
echo "File type $file_type";
echo "Directory to link $directory_to_link";
command="find $directory_to_link -type f -name $file_type";
echo $command;
#for i in $(find $directory_to_link -type f -name $file_type);
for i in $command;
do
echo $i;
if test -e $(basename $i); then
echo $i exists;
else
echo Linking: $i;
ln -s $i;
fi
done;
}
How can I prevent the expansion so that Bash does search for files that end in *.h in my the directory I want to pass in?
UPDATE 1:
So I've updated the call to be
link_files_of_type_from_directory "'*.h'" ..
And the function now assembles the string of the command to be evaluated like so:
mmd="find $directory_to_link -type f -name $file_type";
When I echo it out—it's correct :)
find .. -type f -name '*.h'
But I can't seem to get the find command to actually run. Here are the errors / mistakes I'm getting while trying to correctly assemble the for loop:
# for i in $mmd; # LOOPS THROUGH STRINGS IN COMMAND
# for i in '$(mdd)'; # RUNS MMD LITERALLY
# for i in ${!mmd}; # Errors out with: INVALID VARIABLE NAME — find .. -type f -name '*.h':
Would love help on this part—even though it is a different question :)
With quoting of your variables, removed semicolons and your loop wrapped into an -exec action to prevent problems with spaces, tabs and newlines in filenames, your function looks like this:
function link_files_of_type_from_directory {
local file_type=$1
local directory_to_link=$2
echo "File type $file_type"
echo "Directory to link $directory_to_link"
find "$directory_to_link" -type f -name "$file_type" -exec sh -c '
for i do
echo "$i"
if test -e "$(basename "$i")"; then
echo "$i exists"
else
echo "Linking: $i"
ln -s "$i"
fi
done
' sh {} +
}

Count filenumber in directory with blank in its name

If you want a breakdown of how many files are in each dir under your current dir:
for i in $(find . -maxdepth 1 -type d) ; do
echo -n $i": " ;
(find $i -type f | wc -l) ;
done
It does not work when the directory name has a blank in the name. Can anyone here tell me how I must edite this shell script so that such directory names also accepted for counting its file contents?
Thanks
Your code suffers from a common issue described in http://mywiki.wooledge.org/BashPitfalls#for_i_in_.24.28ls_.2A.mp3.29.
In your case you could do this instead:
for i in */; do
echo -n "${i%/}: "
find "$i" -type f | wc -l
done
This will work with all types of file names:
find . -maxdepth 1 -type d -exec sh -c 'printf "%s: %i\n" "$1" "$(find "$1" -type f | wc -l)"' Counter {} \;
How it works
find . -maxdepth 1 -type d
This finds the directories just as you were doing
-exec sh -c 'printf "%s: %i\n" "$1" "$(find "$1" -type f | wc -l)"' Counter {} \;
This feeds each directory name to a shell script which counts the files, similarly to what you were doing.
There are some tricks here: Counter {} are passed as arguments to the shell script. Counter becomes $0 (which is only used if the shell script generates an error. find replaces {} with the name of a directory it found and this will be available to the shell script as $1. This is done is a way that is safe for all types of file names.
Note that, wherever $1 is used in the script, it is inside double-quotes. This protects it for word splitting or other unwanted shell expansions.
I found the solution what I have to consider:
Consider_this
#!/bin/bash
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for i in $(find . -maxdepth 1 -type d); do
echo -n " $i: ";
(find $i -type f | wc -l) ;
done
IFS=$SAVEIFS

how do i return all the paths after set point using bash

i am returning this:
./nas/cdn/catalog/swatches
./nas/cdn/catalog/product_shots
./nas/cdn/catalog/product_shots/high_res
./nas/cdn/catalog/product_shots/high_res/back
./nas/cdn/catalog/product_shots/high_res/front
./nas/cdn/catalog/product_shots/low_res
./nas/cdn/catalog/product_shots/low_res/back
./nas/cdn/catalog/product_shots/low_res/front
./nas/cdn/catalog/product_shots/thumbs
./nas/cdn/catalog/full_length
./nas/cdn/catalog/full_length/high_res
./nas/cdn/catalog/full_length/low_res
./nas/cdn/catalog/cropped
./nas/cdn/catalog/drawings
what is the correct way to remove ./nas/cdn/catalog/ from this?
this is the code i have, so far
BASE='./nas/cdn/catalog'
echo $BASE
for d in $(find . -type d -regex "${BASE}/[^.]*")
do
echo $(basename $d)
done
bit this just returns the last folder, i like to return /swatches, /product_shots/high_res etc...
Use sed like below,
BASE='./nas/cdn/catalog'
echo $BASE
for d in $(find . -type d -regex "${BASE}/[^.]*")
do
sed 's~^\([^/]*/\)\{4\}~~' <<< "$d"
done
Example:
$ var="./nas/cdn/catalog/drawings"
$ sed 's~^\([^/]*/\)\{4\}~~' <<< "$var"
drawings
A somewhat simpler approach:
BASE='./nas/cdn/catalog'
echo "$BASE"
( cd "$BASE" ; find */ -type d )
Note: this is not perfectly robust; it will fail when any of the directories immediately inside in $BASE starts with a hyphen. It should only be used when you can guarantee that that is not the case.

Specify multiple directories for recursive search

I have a bash script which searches through all the sub-directories (at all levels) given a target directory:
#! /bin/bash
DIRECTORIES="/home/me/target_dir_1"
for curr in $DIRECTORIES
do
...
Now I want the script to search multiple target directories such as target_dir_1, target_dir_2, target_dir_3. How should I modify the script to do this?
use find instead.
find /home/me/target_dir_1 -type d
You can put that in a for loop:
for d in target_dir_1 target_dir_2
do
find /home/me/"$d" -type d
done
If it is always /home/me, and you want to search all the directories under that, do the follwing:
find /home/me -type d
#!/bin/bash
GIVEN_DIR=$1 ## Or you could just set the value here instead of using $1.
while read -r DIR; do
echo "$DIR" ## do something with subdirectory.
done < <(exec find "$GIVEN_DIR" -type d -mindepth 1)
Run with:
bash script.sh dir
Note that word splitting is a bad idea so don't do this:
IFS=$'\n'
for DIR in $(find "$GIVEN_DIR" -type d -mindepth 1); do
echo "$DIR" ## do something with subdirectory.
done
Neither with other forms like when you could use -print0 for find, although it's fine if you still use while:
while read -r DIR -d $'\0'; do
echo "$DIR" ## do something with subdirectory.
done < <(exec find "$GIVEN_DIR" -type d -mindepth 1 -print0)
Lastly you could record those on an array:
readarray -t SUBDIRS < <(exec find "$GIVEN_DIR" -type d -mindepth 1)
for DIR in "${SUBDIRS[#]}"; do
echo "$DIR" ## do something with subdirectory.
done
Say:
for i in /home/me/target_dir_{1..5}; do
echo $i;
done
This would result in:
/home/me/target_dir_1
/home/me/target_dir_2
/home/me/target_dir_3
/home/me/target_dir_4
/home/me/target_dir_5
Alternatively, you can specify the variable as an array and loop over it:
DIRECTORIES=( /home/me/target_dir_1 /home/me/target_dir_2 /home/me/target_dir_3 )
for i in ${DIRECTORIES[#]}; do echo $i ; done
which would result in
/home/me/target_dir_1
/home/me/target_dir_2
/home/me/target_dir_3

Why does this conditional return "No such file or directory"

My conditional works properly when the dirs exist, but if they don't, it seems to execute both then and else statements (is that the correct term?).
script.sh
#!/bin/bash
if [[ $(find path/to/dir/*[^thisdir] -type d -maxdepth 0) ]]
then
find path/to/dir/*[^thisdir] -type d -maxdepth 0 -exec mv {} new/location \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
prompt
For the first call, the dirs are there; in the second, they've been moved from the first call.
$ sh script.sh
Huzzah!
$ sh script.sh
find: path/to/dir/*[^thisdir]: No such file or directory
hey hey hey
How can I fix this?
tried suggestion(s)
if [[ -d $(path/to/dir/*[^thisdir]) ]]
then
find path/to/dir/*[^thisdir] -type d -maxdepth 0 -exec mv {} statamic-1.3-personal/admin/themes \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
result
$ sh script.sh
script.sh: line 1: path/to/dir/one_of_the_dirs_to_be_moved: is a directory
hey hey hey
There seem to be some errors:
First, the pattern path/to/dir/*[^thisdir] is interpreted in bash in the same manner than path/to/dir/*[^dihstr] mean *all filename ending by d, i, h, s, t or r.
Than if you are searching for something in this dir (path/to/dir) but not on path/to/dir/thisdir, and not on a nth subdir, you could bannish find and write:
Edit: There was an error on my sample too: [ -e $var ] was wrong.
declare -a files=( path/to/dir/!(thisdir) )
if [ -e $files ] ;then
mv -t newlocation "${files[#]}"
echo "Huzzah!"
else
echo "hey hey hey"
fi
If you need find for searching in subirs, please give us samples and/or more descriptions.
Your error is probably occurring at if [[ $(find path/to/dir/*[^thisdir] -type d -maxdepth 0) ]] and then it goes to else because find errors out.
find wants its directory parameter to exist. Based on what you are trying to do you should probably consider
$(find path/to/dir/ -name "appropriate name pattern" -type d -maxdepth 1)
Also, I'd consider using actual logical function in if. See this for file conditionals.
Try adding a #!/bin/bash on the first line to ensure that it is bash that is executing your script, as recommended by this post:
Why is both the if and else executed?
The OP wishes to move all files excluding thisdir to a new location.
A solution using find would be to exclude thisdir using find's functionality, rather than by using bash's shell expansion:
#!/bin/bash
if [[ $(find path/to/directory/* -maxdepth 0 -type d -not -name 'thisdir') ]]
then
find path/to/directory/* -maxdepth 0 -type d -not -name 'thisdir' -exec mv {} new/location \;
echo "Huzzah!"
else
echo "hey hey hey"
fi
This has been tested, and works under bash version 4.2.39, and GNU findutils v4.5.10.

Resources