by Howard Fosdick, 2025 © RexxInfo.org
If you write shell scripts, eventually you’ll face a common problem. How can you traverse the directory structure of the file system?
For example, you might want to execute a set of commands in your home directory and all its subdirectories. Or you might want to run some set of commands in every directory on the entire computer.
This article shows four different ways to traverse an entire directory tree in your shell scripts.
We start with the find line command. It can return a list of all subdirectories beneath a given directory.
For example, this find command lists the current directory and all its subdirectories. The period ( . ) tells the command to start with the current directory (the directory from which you’re running the command). The -type d parameter says to return directory names only, and to exclude file names:
find . -type d
Now we need to capture that output list of directories so that we can process them individually.
One way to do this is to pipe the output of the find command to a read command. Piping sends the output of one line command to the input of another. Shell scripts use the vertical bar ( | ) to indicate piping. So here the find command output feeds directly into a read command:
find . -type d | while read dir
do
echo "$dir"
done
The read command captures each directory into the variable we’ve named $dir. The while-do-done loop then echoes each directory name to the terminal.
Coding the $dir variable inside quotation marks ensures that this code handles cases where directory names include embedded spaces.
With that last bit of code, we have a script that can capture and process an entire directory tree structure. Now the problem becomes: how do we execute our desired commands within each directory?
For simplicity of illustration, we’ll represent the commands we want to execute in each directory by a single pwd command. That will display the current directory and prove that the script reached all the proper subdirectories.
(In practice, you would replace the pwd command with whatever commands you want to execute in each directory.)
Here’s our first attempt. The find command pipes directory names to the read command, which reads them one by one. Then for each directory read, we’ll change to that directory by a cd command. We’ll execute our pwd command in that directory:
find . -type d | while read dir
do
cd "$dir"
pwd # your processing for each directory goes here
done
Oops, that doesn’t work!
It fails because the code changes the working directory with a cd command, and doesn’t change it back, even while the driving find… while read command loop is still in progress!
We’ll show four different ways to resolve this problem.
Here’s an obvious solution: manually manage directory changes. You save the name of the directory that the find… while read loop uses before you change to the new subdirectory. Then, after you’ve run the pwd command on the new subdirectory, restore the original directory back for the find… while read loop. Here’s how this looks:
find . -type d | while read dir
do
save_dir=`pwd` # save the current working directory
cd "$dir" # change to the directory read in by the READ command
pwd # your processing for each directory goes here
cd "$save_dir" # change back to the original directory `
done
Just replace the pwd command with the set of commands you want to execute in each directory of the directory tree.
Here’s alternative solution. It uses the directory stack, a feature provided by the shell that stores a list of directories.
A stack is simply a list of items. The items are stored on the principle of “Last In, First Out.” The last item put into the list is also the first one retrieved.
The pushd and popd shell commands manipulate the directory stack. pushd adds a directory name to the stack, while popd retrieves it.
Specifically, the pushd command performs these actions:
popd reverses the actions of the prior pushd.
Here’s how to change our script to use a directory stack. Instead of manually storing and restoring the directory for the find… while read loop, we use the pushd and popd commands to do that work for us:
#!/bin/bash
find . -type d | while read dir
do
pushd "$dir" >/dev/null
pwd # your processing for each directory goes here
popd >/dev/null
done
In the script, pushd changes to the directory specified in its operand, $dir. So we don’t need to code a cd command to change the directory. pushd also saves that directory name so we can retrieve it later with a popd command.
The pwd command represents the set of commands you wish to execute within the new directory.
popd then restores the script to its prior directory.
Since both pushd and popd echo the stack’s contents to the terminal, we’ve suppressed that from the output by coding: >/dev/null.
Manipulating a stack with the pushd and popd commands enables the script to recurse through all subdirectories and execute commands within each.
Be aware that not every shell includes the directory stack feature. Bash, Zsh, Tcsh, and Fish support it. Sh, Dash, and the ksh93u+ Korn shell do not.
Here’s an entirely different solution to traversing a tree structure. It uses a separate process, called a subshell. The benefit of a subshell is that it separates subshell processing from that of its parent shell. It’s a mechanism to isolate the commands we want to run against each individual directory from the rest of the script.
In shell scripting, you invoke a subshell simply by enclosing its command stream within parentheses. Here’s how to code it:
find . -type d | while read dir
do
(
cd "$dir"
pwd # your processing for each directory goes here
)
done
As the parentheses direct, the script spawns a subshell that isolates the cd and pwd commands, and any other commands we wish to execute in each directory. Then the script pops back to the invoking shell process for the next iteration of the find… while read command loop.
A subshell is sometimes called a child process. It inherits most the attributes of the parent shell. But a parent does not inherit from its child process. So the cd and other commands we perform inside the subshell do not affect the parent shell code. That’s why we don’t have to worry that the subshell code will adversely affect the find… while read loop while it processes.
There’s even a fourth way to traverse directories and issue commands within each. Use the find command’s -exec parameter.
-exec runs the command that follows it, for each directory that find returns. This example executes an ls command in each directory:
find . -type d -exec ls {} \;
The syntax {} \; ensures that the ls command is run in each subdirectory that find returns, and that its results come back properly.
Compared to the other ways to traverse directory trees we’ve shown, this solution is very compact and powerful. However, it can become a readability nightmare if you pack too much code after the -exec parameter. In that case, use one of the other solutions we’ve covered to write intelligible code.
find -exec can do way more than we have space to show here. This article offers many more good practical examples of its use.
Okay, let’s put our directory-traversing script to work to do something useful.
This script converts PNG images to the more compact WEBP image format to save space. This can save you a ton of space – if you don’t mind lossy compression – because WEBP images reclaim up to 90% of the space used by their PNG equivalents.
The script converts all images in the directory in which it is run, as well as in all subdirectories below that. So you could use it to convert all the PNGs in your home directory. Or even all the PNG files on your computer (assuming you run it under a user id that has all the necessary file privileges).
We'll use the ImageMagick convert command to perform the conversions from PNG to WEBP images. So you may need to install ImageMagick first. Just install it from your Linux repository. Or execute a command like this:
sudo apt install imagemagick
We’ll use a subshell to do the work (solution #3 above). Here’s the script:
find . -type d | while read dir
do
(
cd "$dir"
#
# Convert all PNG files in each directory to WEBP files
#
for file in *.png ; do
convert "$file" "$file".webp 2>/dev/null
done
)
done
As you can see, we invoke a subshell for each directory the script accesses. (The parentheses invoke the subshell.)
Then, we run a for loop to process all the PNG files in each directory the script accesses.
In this way, this script could run against all the PNG files in your Home directory, and all subdirectories below that.
Depending how many PNG files you store, this simple little script could reclaim tens, hundreds, or even thousands of megabyes of disk space. That's the power of traversing directories with your shell script program.
When shell scripting, you’ll sometimes find that you need to execute a set of commands for each subdirectory within a directory tree. This article illustrated four common ways to accomplish this goal.
Copy and paste the code snippets in this article and use them as the basis for your own scripts.