Four Ways to Traverse Directories in Shell Scripts


by Howard Fosdick, 2025 © RexxInfo.org


Traversing Directories in Shell Scripts

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.



The FIND Command


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.


Running Commands in Each Directory

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.


Solution 1: Save and Restore the Driving Directory

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.


Solution 2: Use Pushd and Popd

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:

  • Changes to the specified directory (just as if you had entered a cd command)
  • Adds the specified directory to the stack
  • Echoes the stack entries to standard output

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.


Solution 3: Use a Subshell

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.


Solution 4: Use FIND -EXEC

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.


Reclaiming Disk Space -- a Real-World Example

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.


Summary

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.


Related Articles


Linux Commands to Display Hardware Information
How to Identify Linux Performance Bottlenecks
How to Upgrade Linux PC Hardware
How to Shrink PDF Files
How to Traverse Directories in Scripts
How to Use PUSHD and POPD
Reclaim Big Disk Space by File Conversions
Linux Performance Tips
Bash to Python Mapping & Comparison
----------------------------------
====> Like this article? Please spread the link love to Slashdot, LXer, Reddit, or wherever.

Back to RexxInfo.org