A few notes on find's exec

Published Jan 02, 2024

Did you know that many dynamic libraries (*.so) on your system are not actually binaries but rather ASCII files?

$ file /usr/lib/libc.so
/usr/lib/libc.so: ASCII text

$ file -b --mime-type /usr/lib/libc.so

These are called Linker scripts, mostly used to reference other libraries, define sections and such, but, to the great amusement of linker authors, can do plenty of other complex things as well. Fortunately, you can usually get away with implementing just the basics.

Anyway, how would one go about finding all dynamic libraries which are in fact ASCII files in disguise?

Well, we have the file command which seems to work just fine, we just need to hook it up to a file finding utility. Usually I would go with fd but unfortunately while it does have the ability to execute a command per file, it does not allow filtering based on the command's return code.

What about the good ol' find?

-exec command ;
              Execute command; true if 0 status is returned. (...)

The exec action returns true if the command returned status 0. That's exactly what we want. As a side note, it might seem kind of weird to talk about "exec" as "action" but find doesn't really accept a bunch of arguments, it accepts an expression. It's an entire language. That's what fd authors mean when they say "it does not aim to support all of find's powerful functionality".

Alright, so we just need to pass in a command which will examine the given file and return 0 if it matches "text/plain", 1 otherwise. An inline Bash script will do. Always does.

# This is wrong!
-exec bash -c '[[ $(file -b --mime-type "{}") == "text/plain" ]]' \; -print

The exec action considers everything up until ; (or +) as the command, followed by another action -print to actually print any matches (-print is usually the default action but not when using -exec and a few others).

To pass the current file being examined, -exec will replace all occurrences of {} with the file relative to the searched directory (you can also do -execdir which will cd into the file's directory).

What's wrong with this? The problem is that we're literally building the shell script by concatenating a bunch of strings. What if the file name contained some weird characters such as quotes (or worse)? It would cause a syntax error (or command execution). Probably not in this case, but what if!

Okay but how do we pass in the file name without string concatenation? We turn to the Bash's manual page of course.

If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, the first argument is assigned to $0 and any remaining arguments are assigned to the positional parameters. The assignment to $0 sets the name of the shell, which is used in warning and error messages.

$ bash -c 'echo $1' bash Hello

What this means is that we can rewrite the query to instead refer to the $1 variable and avoid interpolating the string altogether.

-exec bash -c '[[ $(file -b --mime-type "$1") == "text/plain" ]]' bash '{}' \; -print

Why put quotes around {}? It's probably not needed anymore, most shells leave them intact. There was however time when Fish replaced empty brackets with an empty string. Fixed in Fish 3.0.0, released Dec 28, 2018. So just in case.

Running this on my Arch machine gives the following list:

$ find /usr/lib -name "*.so" -exec bash -c '[[ $(file -b --mime-type "$1") == "text/plain" ]]' bash '{}' \; -print

Of course there are many other ways how you can achieve the same thing. If you don't intent to use any other filtering, you might not actually need find at all and could use your shell directly. This is what it would look like in Nushell.

ls /usr/lib/**/*.so | where { (file -b --mime-type $in.name) == "text/plain" }

Nushell does have the ability to display mime types directly with ls -m but for performance reasons it only examines the file extension, not the actual content. I have written about Nushell before.

Go back to the front page