1. Overview

In the Linux environment, we often encounter situations where we need to verify the absence of a specific file within a set of directories. We may imagine a scenario where we’re scanning for directories missing a particular configuration file.

In this tutorial, we’ll explore multiple approaches for finding directories not containing a specific file in Linux.

2. Understanding the Scenario

To simulate and understand the scenario, let’s use a sequence of the mkdir and touch commands for creating a directory structure under the simulation/ directory:

$ mkdir -p ./simulation && cd simulation
$ mkdir -p ./dir-grandparent-{1..2}
$ mkdir -p ./dir-grandparent-{1..2}/dir-parent-{1..2}
$ mkdir -p ./dir-grandparent-{1..2}/dir-parent-{1..2}/dir-{1..2}
# add sample files in directories
$ touch ./dir-grandparent-{1..2}/file-grandparent.txt
$ touch ./dir-grandparent-{1..2}/dir-parent-{1..2}/file-parent.txt
$ touch ./dir-grandparent-{1..2}/dir-parent-{1..2}/dir-{1..2}/file.txt
$ touch ./dir-grandparent-{1..2}/dir-parent-{1..2}/dir-1/specificfile.txt

It’s important to note that the specificfile.txt file is present in multiple directories, but is missing from the remaining ones.

Further, we can visualize the file and directory structure using the exa command with its –tree option:

$ exa --tree .
.
├── dir-grandparent-1
│  ├── dir-parent-1
│  │  ├── dir-1
│  │  │  ├── file.txt
│  │  │  └── specificfile.txt
│  │  ├── dir-2
│  │  │  └── file.txt
│  │  └── file-parent.txt
│  ├── dir-parent-2
│  │  ├── dir-1
│  │  │  ├── file.txt
│  │  │  └── specificfile.txt
│  │  ├── dir-2
│  │  │  └── file.txt
│  │  └── file-parent.txt
│  └── file-grandparent.txt
└── dir-grandparent-2
   ├── dir-parent-1
   │  ├── dir-1
   │  │  ├── file.txt
   │  │  └── specificfile.txt
   │  ├── dir-2
   │  │  └── file.txt
   │  └── file-parent.txt
   ├── dir-parent-2
   │  ├── dir-1
   │  │  ├── file.txt
   │  │  └── specificfile.txt
   │  ├── dir-2
   │  │  └── file.txt
   │  └── file-parent.txt
   └── file-grandparent.txt

Great! We’ve got a simulation directory available with us now for exploring different approaches to find the directories where specificfile.txt is missing.

3. Using find With -exec Option

The find command is quintessential for finding files and directories in Linux. In this section, let’s see how to use the find command with the -exec option to solve our use case.

3.1. Using test

We can call the find command to search within a path using a set of expressions:

$ find [path...] [expression]

It’s important to note that an expression is a combination of options, tests, and actions. So, it decides how to filter the search results and what action to take for each matching result.

Now, let’s first see how we can use the -exec option to execute a command for each file that matches the search criteria:

$ find [path...] [expression] -exec command {} \;

Usually, it’s common to use commands that are actions, such as echo, rm, and so on. However, we can also use the test command with the -exec option to get an overall result of a test instead of an action.

Next, let’s put these concepts together to search for all directories that don’t contain the specificfile.txt file:

$ find ./* -type d '!' -exec test -f '{}/specificfile.txt' ';' -print
./dir-grandparent-1
./dir-grandparent-1/dir-parent-1
./dir-grandparent-1/dir-parent-1/dir-2
./dir-grandparent-1/dir-parent-2
./dir-grandparent-1/dir-parent-2/dir-2
./dir-grandparent-2
./dir-grandparent-2/dir-parent-1
./dir-grandparent-2/dir-parent-1/dir-2
./dir-grandparent-2/dir-parent-2
./dir-grandparent-2/dir-parent-2/dir-2

The result looks correct.

Lastly, let’s break this down to understand the nitty gritty of the logic:

  • find ./* -type d searches for all directories in the current directory
  • ‘!’ negates the following test
  • -exec test -f ‘{}/specificfile.txt’ ‘;’ checks if the specificfile.txt file exists for each directory found
  • -print is the action that prints the filtered directories that don’t contain specificfile.txt

That’s it! In a nutshell, we look for all directories and check for specificfile.txt in each directory.

3.2. Using ls and grep

Alternatively, we can use a combination of the ls and grep commands to check for the existence of specificfile.txt in a given directory. Moreover, in this scenario, we need to pipe the two commands together, so we’ll invoke a shell using the sh command with its -c option.

Now, let’s go ahead and see this in action:

$ find ./* -type d '!' -exec sh -c 'ls -A "{}" | grep --quiet "specificfile.txt"' \; -print
./dir-grandparent-1
./dir-grandparent-1/dir-parent-1
./dir-grandparent-1/dir-parent-1/dir-2
./dir-grandparent-1/dir-parent-2
./dir-grandparent-1/dir-parent-2/dir-2
./dir-grandparent-2
./dir-grandparent-2/dir-parent-1
./dir-grandparent-2/dir-parent-1/dir-2
./dir-grandparent-2/dir-parent-2
./dir-grandparent-2/dir-parent-2/dir-2

Great! It looks like we’ve got this one right. Further, we must note that we used grep with the –quiet option as we don’t want it to show anything on stdout. Instead, the -print action is everything we need to show the directories matching the desired condition.

4. Using find and xargs

Instead of using the -exec option with find, we can pipe the output from find to the xargs command for executing the tests and actions for each directory.

Let’s see how we can solve our use case with this approach:

$ find ./* -type d -print0 | xargs -0 -I{} sh -c 'test ! -f {}/specificfile.txt && echo {}'
./dir-grandparent-1
./dir-grandparent-1/dir-parent-1
./dir-grandparent-1/dir-parent-1/dir-2
./dir-grandparent-1/dir-parent-2
./dir-grandparent-1/dir-parent-2/dir-2
./dir-grandparent-2
./dir-grandparent-2/dir-parent-1
./dir-grandparent-2/dir-parent-1/dir-2
./dir-grandparent-2/dir-parent-2
./dir-grandparent-2/dir-parent-2/dir-2

Fantastic! It looks like we nailed this one.

Now, let’s break this down to understand the core logic.

Firstly, we must note that we used the -print0 option with find and the -0 option with xargs to produce and consume null-terminated strings, respectively. Secondly, we used -I{} to define {} as a placeholder for individual directory paths. Lastly, we used sh -c to invoke a shell for executing multiple commands such as test and echo.

5. Using Bash With Loop

In this section, let’s solve our use case using loop constructs, such as for loop and while loop, in a Bash script.

5.1. With for Loop

In Bash, for loop is a quite sophisticated way to iterate over a directory:

for file in directory/*
do
    # add logic
done

Using this approach, we can write a Bash script that iterates over the directories recursively to check if they contain specificfile.txt. So, let’s go ahead and write the find_dirs.sh script and look at it in its entirety:

$ cat find_dirs.sh
#!/bin/bash

search_dir="/simulation"
file_to_check="specificfile.txt"

find_directory() {
    for directory in "$search_dir"/*; do
        if [ -d "$directory" ]
        then
            # If the file does not exist in the directory, print the directory's name
            if [ ! -e "$directory/$file_to_check" ]
            then
                echo $directory
            fi
        find_directory "$directory"
        fi
    done
}
find_directory

Now, let’s understand the core elements of our script. Firstly, we initialized the search_dir and file_to_check variables with /simulation and specificfile.txt values, respectively. Then, we defined the find_directory() recursive function that iterates over $search_dir and prints the directories that don’t contain specificfile.txt. Lastly, for each internal directory, we call the find_directory() function to search with a recursive strategy.

Finally, let’s execute our script that calls the find_directory() function to see it in action:

./find_dirs.sh .
/simulation/dir-grandparent-1
/simulation/dir-grandparent-1/dir-parent-1
/simulation/dir-grandparent-1/dir-parent-1/dir-2
/simulation/dir-grandparent-1/dir-parent-2
/simulation/dir-grandparent-1/dir-parent-2/dir-2
/simulation/dir-grandparent-2
/simulation/dir-grandparent-2/dir-parent-1
/simulation/dir-grandparent-2/dir-parent-1/dir-2
/simulation/dir-grandparent-2/dir-parent-2
/simulation/dir-grandparent-2/dir-parent-2/dir-2

Perfect! The script works as expected.

5.2. With while Loop

Unlike the for loop, a while loop doesn’t allow us to iterate over a directory directly. So, we’ll need to rely upon the find command to give us the list of directories:

find . -type d | while read -r directory; do
    # add logic
done

It’s interesting to note that we pipe the output from the find command to the while loop. Furthermore, we get each line into the directory variable using the read command.

Moving on, let’s write the find_dirs_whileloop.sh script to solve our use case:

$ cat find_dirs_whileloop.sh
#!/bin/bash

# Start path and file name to search for
search_dir="/simulation"
file_to_check="specificfile.txt"

cd "$search_dir"
# Find all directories and pass them line-by-line to the while loop
find . -type d | while read -r directory; do
    # If the file does not exist in the directory, print the directory's name
    if [[ ! -e "$directory/$file_to_check" ]]; then
        echo "$directory"
    fi
done

It’s worth noting that we’re checking for the presence of the specificfile.txt file in each directory. Further, we display only those directories where specificfile.txt is missing.

Lastly, let’s execute the find_dirs_whileloop.sh script to show directories that don’t contain the specificfile.txt file:

$ ./find_dirs_whileloop.sh
.
./dir-grandparent-1
./dir-grandparent-1/dir-parent-1
./dir-grandparent-1/dir-parent-1/dir-2
./dir-grandparent-1/dir-parent-2
./dir-grandparent-1/dir-parent-2/dir-2
./dir-grandparent-2
./dir-grandparent-2/dir-parent-1
./dir-grandparent-2/dir-parent-1/dir-2
./dir-grandparent-2/dir-parent-2
./dir-grandparent-2/dir-parent-2/dir-2

Fantastic! We’ve got this one right.

6. Conclusion

In this article, we learned how to find directories not containing a specific file. Furthermore, we explored commands such as find, ls, grep, test, xargs, and loop constructs in Bash to solve our use case.