How to replicate find -execdir behavior using fd

Updated April 28, 2025 · 10 min ·  

Linux Rust Find Fd

Cover Image

While find is a powerful Linux command-line tool for file searching, its syntax can be verbose. fd offers a more user-friendly and faster alternative. In my previous article on Freeing Up Gigabytes: Reclaiming Disk Space from Rust Cargo Builds, I shared an option for cleaning Rust build artifacts using find -execdir to invoke cargo clean from the root directory of each Rust project. This post explores how to achieve a similar workflow using fd, which lacks a direct equivalent to -execdir, and the workarounds involved.

Let’s dive in!

Background

I was seeking to recover disk space from Rust Cargo builds which involved executing cargo clean within the root directory of each Rust project.

My Rust projects were located in several directories in the following hierarchy. (I am only showing the src and target directory for each Rust project for the sake of brevity.)

├── battinfo
│   ├── src
│   ├── target
├── human-time-cli
│   ├── src
│   ├── target
└── level2
    ├── bat
    │   ├── src
    │   ├── target
    └── level3
        └── rust-base64
            ├── src
            └── target

I used find with -execdir to locate the parent directory of each target folder, effectively identifying the root of all Rust projects. From each of these root directories, I then executed cargo clean to remove build artifacts.

find -type d -name target -execdir cargo clean \;

The find -execdir option differs from find -exec in that it first changes the working directory to the location of the discovered file before executing the command. This feature is essential for commands like cargo clean that need to be run within the context of a specific project directory.

How can we use fd to accomplish the equivalent objective?

Installing fd

The fd command is a modern, Rust-based re-imagining of of the venerable find command. If you don’t already have it installed, it is readily available on most Unix-based systems.

If you’re running Debian/Ubuntu, you can install the officially maintained package:

apt install fd-find

Fedora users can install using:

dnf install fd-find

fd is available on numerous other Linux distros including Arch:

pacman -S fd

In fact, fd is even available on Windows and can be installed via Winget:

winget install sharkdp.fd

First steps toward a solution

As a first step, let’s run fd to find all directories named target:

# This finds all directories named `target` since they will be of
# type = directory
$ fd target -t d

Surprisingly, fd is not returning any results. After a bit of digging, it turns out that fd ignores files and directories listed in .gitignore by default when searching inside a project. Since target/ is included in .gitignore for these Rust projects, it will not be returned in the fd search results unless a -I or -no-ignore is included. This option instructs fd to not respect .gitignore files in the search results. After this update, we see that fd is now returning results for the target directory:

$ fd target -t d -I
battinfo/target/
human-time-cli/target/
level2/bat/target/
level2/level3/rust-base64/target/

We’re making progress!

A next logical step would be to run fd with the -x execute command; however, this yields errors.

$ fd target -t d -I -x cargo clean
error: unrecognized subcommand './level2/level3/rust-base64/target'

Usage: cargo clean [OPTIONS]

For more information, try '--help'.
error: unrecognized subcommand './human-time-cli/target'

Usage: cargo clean [OPTIONS]

For more information, try '--help'.
error: unrecognized subcommand './level2/bat/target'

Usage: cargo clean [OPTIONS]

For more information, try '--help'.
error: unrecognized subcommand './battinfo/target'

Usage: cargo clean [OPTIONS]

For more information, try '--help'.

This approach doesn’t work because cargo clean isn’t designed to receive file paths as arguments, and fd is providing them. We’re also not running cargo clean in the context of the parent directory of the matched path.

Let’s take a step back and use the echo command with no parameters to diagnose the issue and prove that fd provides file paths as arguments.

$ fd target -t d -I -x echo
./battinfo/target
./human-time-cli/target
./level2/bat/target
./level2/level3/rust-base64/target

Sure enough! This helps us realize that fd is consistently passing the matched path as an argument as STDIN to commands.

Furthermore, we can explicitly pass {} as an argument to the echo command to get the same result, demonstrating that {} acts as a placeholder for each matched path within fd.

$ fd target -t d -I -x echo {}
./battinfo/target
./human-time-cli/target
./level2/bat/target
./level2/level3/rust-base64/target

Very good - we are seeing all of the matched target directories in our hierarchy of Rust projects using {}.

🔑 Key insight: When using fd, {} is a placeholder for the matched path.

As a next step, we want to cd into the parent directory before running the command to emulate find’s -execdir functionality.

It turns out that fd uses {//} as a placeholder expansion to represent the basename (the filename without the directory path) of the found item. In our case, since we are searching for directories named target, this removes the target from the matched path in the found results, yielding the parent directory. For example, ./battinfo/target becomes ./battinfo. Let’s use echo again to see this in action:

$ fd target -t d -I -x echo {//}
./battinfo
./human-time-cli
./level2/bat
./level2/level3/rust-base64

This is a significant step forward! The {//} path modifier successfully renders the parent directory of each matched item.

The final solution

The final solution involves chaining commands within fd. We use fd -x to execute a sh command that first changes the directory to the parent of the matched target directory (cd "{//}") and then runs cargo clean.

$ fd target -t d -I -x sh -c 'cd "$1" && cargo clean' sh {//}
     Removed 52 files, 6.2MiB total
     Removed 370 files, 177.2MiB total
     Removed 377 files, 190.2MiB total
     Removed 2280 files, 943.2MiB total

It works and cargo clean is successfully invoked in the context of the parent directory of each found target directory!

Let’s review the main parts of the fd command executed above:

  • fd: the fd (find) command
  • target: the search pattern (we want to find directories named “target”)
  • -t d: specifies the type of file to search for which is files of type directory
  • -I: do not respect .gitignore files
  • -x: specifies the command to execute. Similar to find -exec
  • sh -c: invoke the shell command string that follows
  • cd "$1": change directory to the parent directory of the found match
  • &&: a shell operator that means “if the previous command succeeds (exits with a status of 0), then execute the next command”
  • cargo clean: a Cargo command used in Rust projects to remove the target directory, which contains build artifacts
  • sh: this represents the $0 inside the script (shell convention). It is merely a placeholder and other placeholder text could be used like an underscore _
  • {//}: the parent directory of the match, and is passed as $1.

The command could be simplified further with the sh {//} removed from the end and using {//} directly instead of a positional parameter $1 like this:

# ⚠️ This syntax is simpler but less secure
$ fd target -t d -I -x sh -c 'cd "{//}" && cargo clean'

This, however, is not recommended from a security perspective since it can be used in command injection attacks if a malicious file or directory name is lurking in the found path waiting to do damage such as file.txt"; rm -rf /important/data; echo ". The shell positional parameter ($1) ensures that no matter what the directory name or filename contains (spaces, quotes, special characters, etc.), it will behave safely and not execute unanticipated commands. Many thanks to Tavian Barnes (tavianator) on GitHub who pointed this out to me since I was initially advocating the simplified, but less secure, command syntax in this article.

🔑 Key insight: To achieve the equivalent of find -execdir using fd use this core syntax: fd -x sh -c 'cd "$1" && your_command_here' sh {//}

Expanding the solution to run a command with arguments

Our solution works flawlessly for cargo clean since it doesn’t need arguments. But how can we achieve the equivalent of find -execdir with fd when we need to to invoke the command with arguments?

Let’s consider an example where we recursively walk through all wav files in our directory structure and convert wav files to mp3 files.

We’ll be using ffmpeg which allows us to convert .wav files to .mp3. For example:

ffmpeg -i example.wav example.mp3

This example is somewhat contrived since we do not technically need to invoke the wav to mp3 conversion commands in the context of the current directory; nonetheless, it serves as a helpful example since it demonstrates some additional placeholder expansions that can be used with fd. Here are the key placeholder expansions available that we’ll use in the following commands:

PlaceholderExpands toExample Output
{}full pathdir/README.md
{//}parent directorydir
{/}basename with extREADME.md
{/.}basename without extREADME

Let’s build up to our final solution. First, let’s find all wav files in the directory hierarchy. We’ll use fd -e (where -e filters by file extension) to find files with the .wav extension:

$ fd -e wav
sample.wav
level2/hello.wav
level2/level3/happy trails.wav

Looking good - we see the .wav files to convert. We even have a .wav file that contains spaces.

I love the ergonomics of the -e (filter by extension option) in lieu of crafting a complex regex to fetch the tail end of each file!

Next, let’s view just the name of the .wav file (sans directory) we’re going to convert to mp3 using the {/} (basename with extension) syntax:

$ fd -e wav -x echo "{/}"
sample.wav
hello.wav
happy trails.wav

We could wrap this in a sh command with a positional parameter, but we’re just keeping it simple and getting the lay of the land and working with known safe files in this context.

Let’s also just get the basename without a file extension, {/.}, since we will ultimately need this to construct a new file name with a .mp3 extension:

$ fd -e wav -x echo "{/.}"
hello
happy trails
sample

Finally, we put this all together to create the find -execdir equivalent with fd using sh and positional parameters:

fd -e wav -x sh -c 'cd "$1" && ffmpeg -i "$2" "$3".mp3' sh {//} {/} {/.}

We can also chain together an additional command to delete the .wav file if the conversion is successful:

fd -e wav -x sh -c 'cd "$1" && ffmpeg -i "$2" "$3".mp3 && rm "$2"' sh {//} {/} {/.}

Zooming back out, this contrived example shows how we can emulate find -execdir using fd and pass arguments from the matched path. This demonstrates the full context, but in real life could have also been simplified if we are only seeking to convert all .wav files to .mp3:

fd -e wav -x sh -c 'ffmpeg -i "$1" "$2".mp3' sh {} {/.}

Conclusion

Whether you’re managing Rust projects or wrangling files across a broader Linux environment, fd offers a powerful, ergonomic alternative to the venerable find command. While it lacks a direct equivalent to find -execdir, we’ve seen that with a little shell-fu and some of fd’s powerful placeholder expansions, we can emulate that behavior cleanly and concisely.

For Rust developers, this means a faster, more intuitive way to clean up disk space with cargo clean across scattered target directories:

$ fd target -t d -I -x sh -c 'cd "$1" && cargo clean' sh {//}

We also explored how fd can be extended for more general tasks, like converting .wav files to .mp3. While that particular example was a bit contrived—since we don’t actually need to change directories to convert audio files—it served as a helpful demonstration of how to pass matched filenames as arguments to commands.

fd -e wav -x sh -c 'cd "$1" && ffmpeg -i "$2" "$3".mp3' sh {//} {/} {/.}

The key takeaway here is that while fd doesn’t offer a built-in equivalent to find -execdir, it’s absolutely possible to emulate that behavior using its flexible placeholder system and shell command execution. By combining fd with sh -c and the parent directory placeholder syntax, {//}, we built a clean and effective workaround that mirrors the find -execdir functionality. The next time you have a need to find files, give fd a try!

Updated April 28, 2025. Originally published April 22, 2025

Share this Article