math2001's blog

Bash's find command

24 September 2017

The find command in bash is quite powerful, and knowing the basics might save you some scripting.

What does it do? It “finds” files. By default, it outputs their path relative to where you ran find. But, in addition of providing you with advanced “filters” it actually allows you to run commands on each of those files.

The basics

With this structure:

.
├── app
│   ├── index.html
│   ├── app.js
│   └── style.css
└── dist
    ├── app.js
    ├── index.html
    └── style.css
$ find
.
./app
./app/app.js
./app/index.html
./app/style.css
./dist
./dist/app.js
./dist/index.html
./dist/style.css

It lists every folder and files recursively.

You can specify a path to find items in, like so:

$ find app
app
app/index.html
app/script
app/script/app.js
app/style
app/style/style.css

Tests (filters)

Some of find's power comes from it's ability to filter which files and folder it “selects”. They are called tests. So, here are some of them:

-type

$ find -type f
./app/app.js
./app/index.html
./app/style.css
./dist/app.js
./dist/index.html
./dist/style.css
$ find -type d
.
./app
./dist

Here, the filter is -type. You guessed it, -type f selects files, and -type d selects directories. More about -type

-name

$ find -name "*.js"
./app/app.js
./dist/app.js
$ find -name "*.JS"
$ find -iname "*.JS"
./app/app.js
./dist/app.js

-name takes a glob. The -iname variant is case insensitive. More about -name

-path

This is the exact same as name, except that it doesn't only apply on the filename, but the whole path (the path that would be outputted). Unlike -path though, * will match both / and leading dots in filename

Same, you have -ipath for a case insensitive version of it. More about -path

Note: -wholename is the same as -path, but -path is more portable.

Combining test – operators

Every expression returns a value except operators. A test returns true if it matches the current file (-name "*.js" returns true for app.js, but not index.html). You can conjugate everything with operators.

Every operators only applies to the next expression. So, expr1 or expr2 and expr3 is the same as (expr1 or expr2) and expr3.

-and

$ find -name "*.js" -type f
./app/app.js
./dist/app.js

Pretty straight forward, right? You select items that finish with .js and that are file. You can guess the operator -and is the default one. Therefore find -name "*.js" -and -type f is the exact same!

-or

What if you want .js and .css files? You can use the -or operator:

$ find -name "*.js" -or -name "*.css" -type f
./app/app.js
./app/style.css
./dist/app.js
./dist/style.css

Again, this is the same as find -name "*.js" -or -name "*.css" -and -type f.

-not

If you want every files that do not end with .js, you can do:

$ find -not -name "*.js" -type f
./app/index.html
./app/style.css
./dist/index.html
./dist/style.css

Grouping

Of course, you can group expressions together. Here, we find every file that finishes by .js or any directories.

$ find \( -name "*.js" -type f \) -or -type d
.
./app
./app/script
./app/script/app.js
./app/style
./dist
./dist/app.js

Thanks to bash (😡), you have to escape the brackets.

Comma ,

Separates 2 expressions: it evaluates both of them, but only returns the value of the second one.

$ find -name "whatever" , -name "*.html"
./app/index.html
./dist/index.html

Aliases

You can use some aliases, although I don't recommend doing so, they aren't as clear:

-o = -or
-a = -and
! = -not (you'll need to escape it though, like so \!)

More about operators

Actions

Now, printing out the filenames if fun, doing some stuff with them is even better! And guess why it actually prints out the filenames: because the default action is -print!

$ find -type f -print
./app/app.js
./app/index.html
./app/style.css
./dist/app.js
./dist/index.html
./dist/style.css

It's exactly what you'd expect, right?

Note: Just as the tests, actions return a value too. Remember this.

-delete

-delete is a pretty useful action. Guess what it does: deletes files (watch out though: it doesn't throw what it deletes to the bin, it actually deletes them, like rm).

If you want to delete every temporary file created by vim (files that end with ~), you can just run this:

$ find -name "*~" -delete

Gone! Every temp files are gone!

What I recommend you do before this is run just find -name "*~" so that you see which file are going to be deleted.

-exec

If you want to do some more complex things though, you might want to use the action -exec.

This action takes an undefined number of parameters representing a command that it's going to run on every selected files. It stops “consuming” arguments as soon as it sees a ;. Note that {} will be replace with file's path. So,

$ find -name "*~" -delete
$ # does the same thing as
$ find -name "*~" -exec rm {} \;

Note: as you can see, we need to escape the ; to prevent bash from interpreting it.

-delete is more efficient though, and more secure. Use it when you can.

Optimizing

It's better to run one command on multiple files than multiple commands on one file each time. For example, the first one is better:

$ rm 1.jpg 2.jpg 3.jpg
$ rm 1.jpg
$ rm 2.jpg
$ rm 3.jpg

Of course, the ability to do that depends on the command, but find gives you the possibility of doing that in your -exec actions.

Note: It'll automatically adapt to the maximum command line length of your system.

In order to do that, you have to use {} +, like so:

$ find -name "*~" -exec rm {} +

In this case {} + will be replaced by as many paths as the maximum command line length of your system allows.

Note that {} + has to be at the end of the command. More about optimizing

Tips and tricks

-maxdepth

Descend at most levels (a non-negative integer) levels of directories below the command line arguments. ‘-maxdepth 0’ means only apply the tests and actions to the command line arguments.

-depth

The -depth option makes find list folders’ content before itself.

Note: the -delete action implies -depth

Examples are shown in the -prune explanation.

-prune

The -prune action allows you to prevent find from going into a directory that matches some tests. Weirdly enough though, it returns true when it found a directory to ignore. For example:

$ find -name "app" -prune
./app

So, it only lists directories that it ignores. To counter that, just add -or -print

$ find -name "app" -prune -or -print
.
./dist
./dist/app.js
./dist/index.html
./dist/style.css

Hold on a sec… -or? But -prune would return false when it looks at ./app/app.js for example, why doesn't it print then?

No, because -prune excludes the directory, remember. So, ./app/app.js never gets looked at.

Gotcha

The problem is that it doesn't play well with -depth. It actually doesn't work, have a look:

$ find -depth -name "app" -prune -or -print
./app/index.html
./app/script/app.js
./app/script
./app/style/style.css
./app/style
./dist/app.js
./dist/index.html
./dist/style.css
./dist
.

It doesn't have the time to “prune” the directory since we start from the bottom.

Remember: -delete implies -depth. Therefore, DO NOT USE -prune with -delete. Instead, “prune” it manually with -path:

$ find -depth -not -path "*app*"
./dist/index.html
./dist/style.css
./dist
.

Ok, I guess that's it! If you want to learn even more, have a look at the documentation.

Hope it'll save you some time. If it does, please share this post!