ChatGPT解决这个技术问题 Extra ChatGPT

How do I exclude a directory when using `find`?

How do I exclude a specific directory when searching for *.js files using find?

find . -name '*.js'

G
GetFree

If -prune doesn't work for you, this will:

find -name "*.js" -not -path "./directory/*"

Caveat: requires traversing all of the unwanted directories.


One of the comments in the accepted answer points out the problem. -prune does not exclude the directory itself, it exclude its content, which means you are going to get an unwanted line in the output with the excluded directory.
Great answer. I'd add to this that you can exclude a directory at ANY level by changing the first . to *. so find -name "*.js" -not -path "*/omitme/*" would omit files from a directory named "omitme" at any level of depth.
It still traverses all of the unwanted directory, though. I'm adding my own answer. :-)
Note, however, that the prune option only doesn't work if you don't use -print explicitly.
It would be better to say "This is an alternative to using -prune". The answers suggesting -prune are clearly not wrong, they just aren't the way you would do it.
o
oguz ismail

Use the -prune primary. For example, if you want to exclude ./misc:

find . -path ./misc -prune -o -name '*.txt' -print

To exclude multiple directories, OR them between parentheses.

find . -type d \( -path ./dir1 -o -path ./dir2 -o -path ./dir3 \) -prune -o -name '*.txt' -print

And, to exclude directories with a specific name at any level, use the -name primary instead of -path.

find . -type d -name node_modules -prune -o -name '*.json' -print

This didn't work for me until I prefixed my local path wih ./, e.g. ./.git. This distinction for find might not be obvious to the occasional find user.
Since this is the accepted answer, I feel it should be mentioned here that the reason -print must be added to the end is to prevent the default behavior, which is to also print the names of the pruned directories. @cycollins explains this well in another answer.
find . -type d \( -path "./Library/*" -o -path "./.Trash" \) -prune -o -name '.DS_Store' -print0 | xargs -0 rm From ~/ on my mac cleans it up nicely :)
@Thomas regarding -print, default seems the keyword in your comment.
If u wanna skip the exclude dir in the result file lists, just add -false after -prune. like find . -path ./misc -prune -false -o -name '*.txt' -print
K
Konrad Rudolph

I find the following easier to reason about than other proposed solutions:

find build -not \( -path build/external -prune \) -name \*.js
# you can also exclude multiple paths
find build -not \( -path build/external -prune \) -not \( -path build/blog -prune \) -name \*.js

Important Note: the paths you type after -path must exactly match what find would print without the exclusion. If this sentence confuses you just make sure to use full paths through out the whole command like this: find /full/path/ -not \( -path /full/path/exclude/this -prune \) .... See note [1] if you'd like a better understanding.

Inside \( and \) is an expression that will match exactly build/external (see important note above), and will, on success, avoid traversing anything below. This is then grouped as a single expression with the escaped parenthesis, and prefixed with -not which will make find skip anything that was matched by that expression.

One might ask if adding -not will not make all other files hidden by -prune reappear, and the answer is no. The way -prune works is that anything that, once it is reached, the files below that directory are permanently ignored.

This comes from an actual use case, where I needed to call yui-compressor on some files generated by wintersmith, but leave out other files that need to be sent as-is.

Note [1]: If you want to exclude /tmp/foo/bar and you run find like this "find /tmp \(..." then you must specify -path /tmp/foo/bar. If on the other hand you run find like this cd /tmp; find . \(... then you must specify -path ./foo/bar.


Outstanding answer, thank you. This works and is scalable (readable) for multiple exclusions. You are a gentlemen and a scholar sir. Thank you for the example for multiple exclusions
This does not work if I want to use -delete switch: find . -not \( -path ./CVS -prune \) -type f -mtime +100 -delete find: The -delete action atomatically turns on -depth, but -prune does nothing when -depth is in effect. If you want to carry on anyway, just explicitly use the -depth option.
@Janis You can use -exec rm -rf {} \; instead of -delete.
By examining the output of find, this is obvious really, but it tripped me up. If you are searching in the current directory (by specifying . as the search path, or not specifying one at all), you most likely want your pattern after -path to start with ./, e.g: find -not \( -path ./.git -prune \) -type f.
A more precise (and POSIX compatible) variation of this method: find searchdir \! \( -type d \( -path './excludedir/*' -o -path './excludedir2/*' -o -path './excludedir3/*' \) -prune \) followed by any conditions that should match what you are looking for.
C
Community

There is clearly some confusion here as to what the preferred syntax for skipping a directory should be.

GNU Opinion

To ignore a directory and the files under it, use -prune

From the GNU find man page

Reasoning

-prune stops find from descending into a directory. Just specifying -not -path will still descend into the skipped directory, but -not -path will be false whenever find tests each file.

Issues with -prune

-prune does what it's intended to, but are still some things you have to take care of when using it.

find prints the pruned directory. TRUE That's intended behavior, it just doesn't descend into it. To avoid printing the directory altogether, use a syntax that logically omits it. -prune only works with -print and no other actions. NOT TRUE. -prune works with any action except -delete. Why doesn't it work with delete? For -delete to work, find needs to traverse the directory in DFS order, since -deletewill first delete the leaves, then the parents of the leaves, etc... But for specifying -prune to make sense, find needs to hit a directory and stop descending it, which clearly makes no sense with -depth or -delete on.

Performance

I set up a simple test of the three top upvoted answers on this question (replaced -print with -exec bash -c 'echo $0' {} \; to show another action example). Results are below

----------------------------------------------
# of files/dirs in level one directories
.performance_test/prune_me     702702    
.performance_test/other        2         
----------------------------------------------

> find ".performance_test" -path ".performance_test/prune_me" -prune -o -exec bash -c 'echo "$0"' {} \;
.performance_test
.performance_test/other
.performance_test/other/foo
  [# of files] 3 [Runtime(ns)] 23513814

> find ".performance_test" -not \( -path ".performance_test/prune_me" -prune \) -exec bash -c 'echo "$0"' {} \;
.performance_test
.performance_test/other
.performance_test/other/foo
  [# of files] 3 [Runtime(ns)] 10670141

> find ".performance_test" -not -path ".performance_test/prune_me*" -exec bash -c 'echo "$0"' {} \;
.performance_test
.performance_test/other
.performance_test/other/foo
  [# of files] 3 [Runtime(ns)] 864843145

Conclusion

Both f10bit's syntax and Daniel C. Sobral's syntax took 10-25ms to run on average. GetFree's syntax, which doesn't use -prune, took 865ms. So, yes this is a rather extreme example, but if you care about run time and are doing anything remotely intensive you should use -prune.

Note Daniel C. Sobral's syntax performed the better of the two -prune syntaxes; but, I strongly suspect this is the result of some caching as switching the order in which the two ran resulted in the opposite result, while the non-prune version was always slowest.

Test Script

#!/bin/bash

dir='.performance_test'

setup() {
  mkdir "$dir" || exit 1
  mkdir -p "$dir/prune_me/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/w/x/y/z" \
    "$dir/other"

  find "$dir/prune_me" -depth -type d -exec mkdir '{}'/{A..Z} \;
  find "$dir/prune_me" -type d -exec touch '{}'/{1..1000} \;
  touch "$dir/other/foo"
}

cleanup() {
  rm -rf "$dir"
}

stats() {
  for file in "$dir"/*; do
    if [[ -d "$file" ]]; then
      count=$(find "$file" | wc -l)
      printf "%-30s %-10s\n" "$file" "$count"
    fi
  done
}

name1() {
  find "$dir" -path "$dir/prune_me" -prune -o -exec bash -c 'echo "$0"'  {} \;
}

name2() {
  find "$dir" -not \( -path "$dir/prune_me" -prune \) -exec bash -c 'echo "$0"' {} \;
}

name3() {
  find "$dir" -not -path "$dir/prune_me*" -exec bash -c 'echo "$0"' {} \;
}

printf "Setting up test files...\n\n"
setup
echo "----------------------------------------------"
echo "# of files/dirs in level one directories"
stats | sort -k 2 -n -r
echo "----------------------------------------------"

printf "\nRunning performance test...\n\n"

echo \> find \""$dir"\" -path \""$dir/prune_me"\" -prune -o -exec bash -c \'echo \"\$0\"\'  {} \\\;
name1
s=$(date +%s%N)
name1_num=$(name1 | wc -l)
e=$(date +%s%N)
name1_perf=$((e-s))
printf "  [# of files] $name1_num [Runtime(ns)] $name1_perf\n\n"

echo \> find \""$dir"\" -not \\\( -path \""$dir/prune_me"\" -prune \\\) -exec bash -c \'echo \"\$0\"\' {} \\\;
name2
s=$(date +%s%N)
name2_num=$(name2 | wc -l)
e=$(date +%s%N)
name2_perf=$((e-s))
printf "  [# of files] $name2_num [Runtime(ns)] $name2_perf\n\n"

echo \> find \""$dir"\" -not -path \""$dir/prune_me*"\" -exec bash -c \'echo \"\$0\"\' {} \\\;
name3
s=$(date +%s%N)
name3_num=$(name3 | wc -l)
e=$(date +%s%N)
name3_perf=$((e-s))
printf "  [# of files] $name3_num [Runtime(ns)] $name3_perf\n\n"

echo "Cleaning up test files..."
cleanup

Thank you for a very good analysis. Regarding "I strongly suspect this is the result of some caching" you can run this command: sudo sh -c "free && sync && echo 3 > /proc/sys/vm/drop_caches && free" to clear the cache (see unix.stackexchange.com/questions/87908/…).
After few tests on those two with -prune I can tell there is rarely any difference. Do keep in mind that which command start first will benefit from cpu performance, the later cpu warm up > performance drop cause minor slow down (I did purge cache before each command as @ndemou suggestion)
Try switch number among name1() name2() name3() in @BroSlow test script above to change execute order to get a visual about what I said. In real life, it is unnoticeable between those two though.
Applause. Thank you for this quality answer.
You should not be -o which means or. so you are pruning in the first step and then forgetting all about it in the next.
M
Mateen Ulhaq

This works on both macOS and Ubuntu:

find / -name MyFile ! -path '*/Directory/*'

Searches for "MyFile", excluding "Directory". Notice the stars * .


This method works on macOS while the accepted answer doesn't. I know original question is for Linux.
Note that you can add multiple ! -path '*/Directory/*' to your command in succession to ignore multiple directories
In a docker container only works with sh -c "find..."
Worked great for me to locate native package.json files: find . -name package.json ! -path '*/node_modules/*'
Nice and concise. Works on Ubuntu 20.0.4 LTS
J
Joshua

One option would be to exclude all results that contain the directory name with grep. For example:

find . -name '*.js' | grep -v excludeddir

This will make your search very slow
This one worked for me, others (which use -prune) - doesn't.
Slow in large results, but useful in smaller sets. But how to exclude multiple directories using grep? Of course this way: find . -name '*.js' | grep -v excludeddir | grep -v excludedir2 | grep -v excludedir3 but there may be some one grep way.
If you want to perform multiple greps then you would be better off writing it as regular expressions: egrep -v '(dir1|dir2|dir3)'. However, in this specific case study, it would be better to exclude directories within find itself.
yes, and you don't need parentheses and it would be better to use ^ to ensure it matches directoryname at the start of the string eg: find . -name '*.js' | egrep -v "^\./excludeddir1|^\./excludeddir2"
m
mpapis

I prefer the -not notation ... it's more readable:

find . -name '*.js' -and -not -path directory

Sorry, it doesn't work. The man page for find says: "To ignore a directory and the files under it, use -prune".
This is wrong. It doesn't prevent find from entering the directory and traversing all the files inside.
find . -iname '*' -and -not -path './somePath' doesn't prevent it from entering said directory.
This helped me with .git path find . -iname '*' -not -path './.git/*'
@rane: More specifically find . -not -path "*/.git*" would be what you want.
G
Gabriel Staples

Tested in Linux Ubuntu 18.04 and 20.04.

Notice that the ./ (or */, see below) before and the /* (or *, but see the caveat below) after the folder name to exclude are required in order to exclude dir_to_exclude, and anything within it!

Also, for speed, and to not traverse excluded directories, notice the really important escaped grouping parenthesis and the -prune option. Ex: find -not \( -path "*/dir_to_exclude/*" -prune \).

To see examples of these escaped grouping parenthesis in the manual pages, run man find, and then press / to search. Search for the pattern \(, for instance, using the regular expression pattern \\\(. Press Enter to begin searching the man pages. Press N for "next match" while searching.

Summary

These work:

# [my favorite #1] exclude contents of `dir_to_exclude` at the search root
find -not -path "./dir_to_exclude/*"

# exclude all files & folders beginning with the name `dir_to_exclude` at the
# search root   
find -not -path "./dir_to_exclude*"

# [my favorite #2] exclude contents of `dir_to_exclude` at any level within your
# search path
find -not -path "*/dir_to_exclude/*"

# exclude all files & folders beginning with the name `dir_to_exclude` at any
# level within your search path
find -not -path "*/dir_to_exclude*"

# To exclude multiple matching patterns, use `-not -path "*/matching pattern/*"`
# multiple times, like this
find -not -path "*/dir_to_exclude1/*" -not -path "*/dir_to_exclude2/*"

[USE THESE] These work too, and are BETTER because they cause find to NOT unnecessarily traverse down excluded paths!:
(This makes a huge difference in speed (is 2x~100x faster)! See here and here. You can also search the man find pages locally for the strings \( and \) with the escaped search strings \\\( and \\\), respectively).

find -not \( -path "./dir_to_exclude" -prune \)  # works here but not above
find -not \( -path "./dir_to_exclude*" -prune \)
find -not \( -path "./dir_to_exclude/*" -prune \)
find -not \( -path "*/dir_to_exclude" -prune \)  # works here but not above
find -not \( -path "*/dir_to_exclude*" -prune \)
find -not \( -path "*/dir_to_exclude/*" -prune \)

# To exclude multiple matching patterns at once, use the `-not \( ... \)` 
# pattern multiple times, like this
find -not \( -path "*/dir_to_exclude1/*" -prune \) \
     -not \( -path "*/dir_to_exclude2/*" -prune \)

...but these do NOT work:

# These do NOT work!
find -not -path "dir_to_exclude"
find -not -path "dir_to_exclude/*"
find -not -path "./dir_to_exclude"
find -not -path "./dir_to_exclude/"

The key is that generally, to make it work, you must begin each matching pattern with either ./ or */, and end each matching pattern with either /* or *, depending on what you're trying to achieve. I say "generally", because there are two noted exceptions in the -not \( ... \)-style section above. You can identify these two exceptions by the comments to the right of them which say: # works here but not above.

Further Explanation:

[BEST, depending on what you want] This WORKS! Exclude all files and folders inside dir_to_exclude at the root of where you are searching. Note that this excludes all subfiles and subfolders inside dir_to_exclude, but it does NOT exclude the dir_to_exclude dir itself. find -not \( -path "./dir_to_exclude/*" -prune \) Also exclude the dir_to_exclude dir itself (and any file or folder with a name which begins with these characters). Caveat: this also excludes dir_to_exclude1, dir_to_exclude2, dir_to_exclude_anyTextHere, etc. It excludes ANY file or folder which merely begins with the text dir_to_exclude and is in the root directory of where you're searching. find -not \( -path "./dir_to_exclude*" -prune \) [BEST, depending on what you want] to recursively exclude a dir by this name at any level in your search path. Simply add a wildcard * to the front of the path too, rather than using the . to indicate the search root directory. find -not \( -path "*/dir_to_exclude/*" -prune \) Recursively exclude any file or folder with a name which begins with the characters dir_to_exclude at any level in your search path. (See also the caveat above). find -not \( -path "*/dir_to_exclude*" -prune \)

Summary:

In ./, the . at the beginning means "start in the current directory" (or in */, the * is a wildcard to pick up any characters up to this point), and in /* at the end, the * is a wildcard to pick up any characters in the path string after the / character. That means the following:

"./dir_to_exclude/*" matches all subfiles and subfolders within dir_to_exclude in the root search directory (./), but does NOT match the directory itself. "./dir_to_exclude*" matches all files and folders within the root search directory (./), including dir_to_exclude, as well as all contents within it, but also with the caveat it will match any file or folder name beginning with the characters dir_to_exclude. "*/dir_to_exclude/*" matches all subfiles and subfolders within dir_to_exclude in any directory at any level in your search path (*/), but does NOT match the directory itself. "*/dir_to_exclude*" matches all files and folders at any level (*/) within your search path with a name which begins with dir_to_exclude.

Going further

From there, I like to pipe to grep to search for certain matching patterns in the paths of interest. Ex: search for any path that is NOT inside the dir_to_exclude directory, and which has desired_file_name.txt in it:

# Case-sensitive; notice I use `\.` instead of `.` when grepping, in order to
# search for the literal period (`.`) instead of the regular expression
# wildcard char, which is also a period (`.`).
find -not \( -path "./dir_to_exclude/*" -prune \) \
    | grep "desired_file_name\.txt"

# Case-INsensitive (use `-i` with your `grep` search)
find -not \( -path "./dir_to_exclude/*" -prune \) \
    | grep -i "desired_file_name\.txt"

# To make `dir_to_exclude` also case INsensitive, use the `find` `-ipath` option
# instead of `-path`:
find -not -ipath \( -path "./dir_to_exclude/*" -prune \) \
    | grep -i "desired_file_name\.txt"

To exclude multiple matching patterns, simply use -not \( -path "*/matching pattern/*" -prune \) multiple times. Ex:

# Exclude all ".git" and "..git" dirs at any level in your search path
find -not \( -path "*/.git/*" -prune \) -not \( -path "*/..git/*" -prune \)

I use the above example as part of my sublf alias here (update: that alias is being expanded and moved into a sublf.sh script in this folder here instead). This alias allows me to use the fzf fuzzy finder to quickly search for and open multiple files in Sublime Text. See the links above for the latest version of it.

alias sublf='FILES_SELECTED="$(find -not \( -path "*/.git/*" -prune \) \
-not \( -path "*/..git/*" -prune \) \
| fzf -m)" \
&& echo "Opening these files in Sublime Text:" \
&& echo "$FILES_SELECTED" \
&& subl $(echo "$FILES_SELECTED")'

References:

[the main answer to this question] How to exclude a directory in find . command https://unix.stackexchange.com/questions/350085/is-it-possible-to-exclude-a-directory-from-the-find-command/350172#350172 https://unix.stackexchange.com/questions/32155/find-command-how-to-ignore-case/32158#32158

See also:

[I still need to study and read this] https://www.baeldung.com/linux/find-exclude-paths [my answer] How to store the output of find (a multi-line string list of files) into a bash array

Keywords: exclude dir in find command; don't search for path with find; case-insensitive find and grep commands


Doesn't work on mac
@holms, I don't have a Mac, but the MacOs manual for find shows that -not and -path are both supported, so any idea why it's not working, or how to make it work on Mac?
Actually, didn't work for me. The reason is that "this works" needs to be replaced by find -not -path "./dir_to_exclude*", without the last slash. Your version still lists the dir_to_exclude.
@emk2203, I see what you mean and I've massively updated my answer as a result. I still prefer the matching pattern "./dir_to_exclude/*", however, as "./dir_to_exclude*" will have the unintended side-effect of matching any file or folder which begins with the string dir_to_exclude, such as dir_to_exclude1, dir_to_exclude2, dir_to_exclude some other string, etc.
The situation where I encountered it was when piping file names to tar. With the dir_to_exclude still in the output, even if there are no files, the dir and its contents get compressed and added to the archive nonetheless. I agree with your concerns, but there are cases when you have to exclude the directory as well or the desired action fails.
D
Drew Frezell

Use the -prune option. So, something like:

find . -type d -name proc -prune -o -name '*.js'

The '-type d -name proc -prune' only look for directories named proc to exclude. The '-o' is an 'OR' operator.


This is the only pure-"find" solution that worked for me. The directories I wished to exclude are NOT immediately below the current working directory.
However, adding -print to the end may improve results. find . -type d -name .hg -prune -o -name data ignored the contents of the (multiple) .hg directories, but listed the .hg directories themselves. With -print, it only listed the "data" directories I was seeking.
yeah, significantly better explained than the accepted answer. and it works. Here's a flavor that finds markdowns, except those under /node_modules/ : find . -name node_modules -prune -o -name '*.md' Nothing wrong with wanting to add extra stuff like -print, but at least lets have something basic that works first.
w
wisbucky

-prune definitely works and is the best answer because it prevents descending into the dir that you want to exclude. -not -path which still searches the excluded dir, it just doesn't print the result, which could be an issue if the excluded dir is mounted network volume or you don't permissions.

The tricky part is that find is very particular about the order of the arguments, so if you don't get them just right, your command may not work. The order of arguments is generally as such:

find {path} {options} {action}

{path}: Put all the path related arguments first, like . -path './dir1' -prune -o

{options}: I have the most success when putting -name, -iname, etc as the last option in this group. E.g. -type f -iname '*.js'

{action}: You'll want to add -print when using -prune

Here's a working example:

# setup test
mkdir dir1 dir2 dir3
touch dir1/file.txt; touch dir1/file.js
touch dir2/file.txt; touch dir2/file.js
touch dir3/file.txt; touch dir3/file.js

# search for *.js, exclude dir1
find . -path './dir1' -prune -o -type f -iname '*.js' -print

# search for *.js, exclude dir1 and dir2
find . \( -path './dir1' -o -path './dir2' \) -prune -o -type f -iname '*.js' -print

u
user1882879

This is the format I used to exclude some paths:

$ find ./ -type f -name "pattern" ! -path "excluded path" ! -path "excluded path"

I used this to find all files not in ".*" paths:

$ find ./ -type f -name "*" ! -path "./.*" ! -path "./*/.*"

I tried this and it still descends into the directories, so speed is definitely not improved.
I
Istopopoki

There are plenty of good answers, it just took me some time to understand what each element of the command was for and the logic behind it.

find . -path ./misc -prune -o -name '*.txt' -print

find will start finding files and directories in the current directory, hence the find ..

The -o option stands for a logical OR and separates the two parts of the command :

[ -path ./misc -prune ] OR [ -name '*.txt' -print ]

Any directory or file that is not the ./misc directory will not pass the first test -path ./misc. But they will be tested against the second expression. If their name corresponds to the pattern *.txt they get printed, because of the -print option.

When find reaches the ./misc directory, this directory only satisfies the first expression. So the -prune option will be applied to it. It tells the find command to not explore that directory. So any file or directory in ./misc will not even be explored by find, will not be tested against the second part of the expression and will not be printed.


Everyone's got a solution but yours explained it the best. I was adamant to have -name be used first rather than -path. Your explanation was adequate to arrive at what I wanted. find . -name "*.txt" -print -o -path ./misc -prune
W
Wolfgang Fahl

The -path -prune approach also works with wildcards in the path. Here is a find statement that will find the directories for a git server serving multiple git repositiories leaving out the git internal directories:

find . -type d \
   -not \( -path */objects -prune \) \
   -not \( -path */branches -prune \) \
   -not \( -path */refs -prune \) \
   -not \( -path */logs -prune \) \
   -not \( -path */.git -prune \) \
   -not \( -path */info -prune \) \
   -not \( -path */hooks -prune \)  

B
Bhupesh Varshney

If anyone's researching on how to ignore multiple paths at once. You can use bash arrays (works perfectly on GNU bash, version 4.4.20(1)-release)

#!/usr/bin/env bash

# This script helps ignore unnecessary dir paths while using the find command

EXCLUDE_DIRS=(
    "! -path /*.git/*"
    "! -path /*go/*"
    "! -path /*.bundle/*"
    "! -path /*.cache/*"
    "! -path /*.local/*"
    "! -path /*.themes/*"
    "! -path /*.config/*"
    "! -path /*.codeintel/*"
    "! -path /*python2.7/*"
    "! -path /*python3.6/*"
    "! -path /*__pycache__/*"
)
find $HOME -type f ${EXCLUDE_DIRS[@]}

# if you like fzf

find $HOME -type f ${EXCLUDE_DIRS[@]} | fzf --height 40% --reverse

Also for some reason, you won't be able to ignore /bin/ directory paths.


I love that this works without using -prune!
For __pycache__ to be ignored (in any nested directory as well as all it's contents) I needed to replace /*__pycache__/* with ./*__pycache__/*.
What also works is to store the arguments of excluded dirs without the quotes in a file, one for each line, and then call it with find $HOME -type f $(< ~/excludelist). File has first line with ! -path /*.git/*, second line with ! -path /*.mozilla/* and so forth.
P
Peter Mortensen

To exclude multiple directories:

find . -name '*.js' -not \( -path "./dir1" -o -path "./dir2/*" \)

To add directories, add -o -path "./dirname/*":

find . -name '*.js' -not \( -path "./dir1" -o -path "./dir2/*" -o -path "./dir3/*"\)

But maybe you should use a regular expression, if there are many directories to exclude.


c
cycollins

a good trick for avoiding printing the pruned directories is to use -print (works for -exec as well) after the right side of the -or after -prune. For example, ...

find . -path "*/.*" -prune -or -iname "*.j2"

will print the path of all files beneath the current directory with the `.j2" extension, skipping all hidden directories. Neat. But it will also print the print the full path of each directory one is skipping, as noted above. However, the following does not, ...

find . -path "*/.*" -prune -or -iname "*.j2" -print

because logically there's a hidden -and after the -iname operator and before the -print. This binds it to the right part of the -or clause due to boolean order of operations and associativity. But the docs say there's a hidden -print if it (or any of its cousins ... -print0, etc) is not specified. So why isn't the left part of the -or printing? Apparently (and I didn't understand this from my first reading the man page), that is true if there there is no -print -or -exec ANYWHERE, in which case, -print is logically sprinkled around such that everything gets printed. If even ONE print-style operation is expressed in any clause, all those hidden logical ones go away and you get only what you specify. Now frankly, I might have preferred it the other way around, but then a find with only descriptive operators would apparently do nothing, so I guess it makes sense as it is. As mentioned above, this all works with -exec as well, so the following gives a full ls -la listing for each file with the desired extension, but not listing the first level of each hidden directory, ...

find . -path "*/.*" -prune -or -iname "*.j2" -exec ls -la -- {} +

For me (and others on this thread), find syntax gets pretty baroque pretty quickly, so I always throw in parens to make SURE I know what binds to what, so I usually create a macro for type-ability and form all such statements as ...

find . \( \( ... description of stuff to avoid ... \) -prune \) -or \
\( ... description of stuff I want to find ... [ -exec or -print] \)

It's hard to go wrong by setting up the world into two parts this way. I hope this helps, though it seems unlikely for anyone to read down to the 30+th answer and vote it up, but one can hope. :-)


P
Peter Mortensen

For a working solution (tested on Ubuntu 12.04 (Precise Pangolin))...

find ! -path "dir1" -iname "*.mp3"

will search for MP3 files in the current folder and subfolders except in dir1 subfolder.

Use:

find ! -path "dir1" ! -path "dir2" -iname "*.mp3"

...to exclude dir1 AND dir2


Doesn't work for me. Neither do any of the above answers. RedHat.
R
Richard Gomes
find . \( -path '.**/.git' -o -path '.**/.hg' \) -prune -o -name '*.js' -print

The example above finds all *.js files under the current directory, excluding folders .git and .hg, does not matter how deep these .git and .hg folders are.

Note: this also works:

find . \( -path '.*/.git' -o -path '.*/.hg' \) -prune -o -name '*.js' -print

but I prefer the ** notation for consistency with some other tools which would be off topic here.


d
dansch
find -name '*.js' -not -path './node_modules/*' -not -path './vendor/*'

seems to work the same as

find -name '*.js' -not \( -path './node_modules/*' -o -path './vendor/*' \)

and is easier to remember IMO.


s
supersan

You can also use regular expressions to include / exclude some files /dirs your search using something like this:

find . -regextype posix-egrep -regex ".*\.(js|vue|s?css|php|html|json)$" -and -not -regex ".*/(node_modules|vendor)/.*" 

This will only give you all js, vue, css, etc files but excluding all files in the node_modules and vendor folders.


C
Community

None of previous answers is good on Ubuntu. Try this:

find . ! -path "*/test/*" -type f -name "*.js" ! -name "*-min-*" ! -name "*console*"

I have found this here


I don't see any reason why any of the answers with more than 100 points shouldn't work on Ubuntu.
find is everywhere the same implementation on all Linux distributions — the one from the GNU Project. The only difference might be the versions. But the changes in the past decade were not that invasive, except maybe for permission matching.
S
Siju V

You can use the prune option to achieve this. As in for example:

find ./ -path ./beta/* -prune -o -iname example.com -print

Or the inverse grep “grep -v” option:

find -iname example.com | grep -v beta

You can find detailed instructions and examples in Linux find command exclude directories from searching.


The grep solution is the only one that excludes all directories by the same name. When trying to exclude "node_modules" that is quite useful.
@bmacnaughton - not true! I came here specifically looking to exclude "node_modules" and after reading many fine answers I settled on find . -type f -print -o -path "*/node_modules" -prune ... using the wildcard this skips "node_modules" at any level; using -print on the first alternative -type f -print makes only that part print, so the "node_modules" directories themselves are not listed. (it can also be reversed: find . -path "*/node_modules" -prune -o -type f -print)
what is */ doing there. What is the exact file you want to exclude.Are ypu using it as wildcard?
@StephenP, thanks for pointing this out; I learned the difference between using ./node_modules and */node_modules from it. For my case, where node_modules only exists in the the directory I start the search in (and under that node_modules directory), I can use find . -type f -print -o -path "./node_modules" -prune because there won't be a node_modules directory under any other directory.
@SijuV - in the directory where I was searching there was a node_modules subdirectory, but there were also subdirectories that had their own node_modules ... using ./node_modules matches only the subdirectory node_modules under the current directory . and prunes it; using */node_modules matches and prunes the directory at any depth, because the * as a glob matches any leading path prefix, such as ./test5/main/node_modules, not only the ./ prefix. The * is a wildcard, but as a glob not as a regex.
D
Dima Lituiev
find . -name '*.js' -\! -name 'glob-for-excluded-dir' -prune

Can't get this one to work. find ~/Projects -name '*.js' -\! -name 'node_modules' -prune is still turning up files with node_modules in their path
@mpen , From stackoverflow.com/questions/4210042/…, I learned that the syntax you want is find ~/Projects -path ~/Projects/node_modules -prune -o -name '*.js' -print. The name of that path must match exactly what find would print if it were going to print the directory.
V
Victoria Stuart

TLDR: understand your root directories and tailor your search from there, using the -path <excluded_path> -prune -o option. Do not include a trailing / at the end of the excluded path.

Example:

find / -path /mnt -prune -o -name "*libname-server-2.a*" -print

To effectively use the find I believe that it is imperative to have a good understanding of your file system directory structure. On my home computer I have multi-TB hard drives, with about half of that content backed up using rsnapshot (i.e., rsync). Although backing up to to a physically independent (duplicate) drive, it is mounted under my system root (/) directory: /mnt/Backups/rsnapshot_backups/:

/mnt/Backups/
└── rsnapshot_backups/
    ├── hourly.0/
    ├── hourly.1/
    ├── ...
    ├── daily.0/
    ├── daily.1/
    ├── ...
    ├── weekly.0/
    ├── weekly.1/
    ├── ...
    ├── monthly.0/
    ├── monthly.1/
    └── ...

The /mnt/Backups/rsnapshot_backups/ directory currently occupies ~2.9 TB, with ~60M files and folders; simply traversing those contents takes time:

## As sudo (#), to avoid numerous "Permission denied" warnings:

time find /mnt/Backups/rsnapshot_backups | wc -l
60314138    ## 60.3M files, folders
34:07.30    ## 34 min

time du /mnt/Backups/rsnapshot_backups -d 0
3112240160  /mnt/Backups/rsnapshot_backups    ## 3.1 TB
33:51.88    ## 34 min

time rsnapshot du    ## << more accurate re: rsnapshot footprint
2.9T    /mnt/Backups/rsnapshot_backups/hourly.0/
4.1G    /mnt/Backups/rsnapshot_backups/hourly.1/
...
4.7G    /mnt/Backups/rsnapshot_backups/weekly.3/
2.9T    total    ## 2.9 TB, per sudo rsnapshot du (more accurate)
2:34:54          ## 2 hr 35 min

Thus, anytime I need to search for a file on my / (root) partition, I need to deal with (avoid if possible) traversing my backups partition.

EXAMPLES

Among the approached variously suggested in this thread (How to exclude a directory in find . command), I find that searches using the accepted answer are much faster -- with caveats.

Solution 1

Let's say I want to find the system file libname-server-2.a, but I do not want to search through my rsnapshot backups. To quickly find a system file, use the exclude path /mnt (i.e., use /mnt, not /mnt/, or /mnt/Backups, or ...):

## As sudo (#), to avoid numerous "Permission denied" warnings:

time find / -path /mnt -prune -o -name "*libname-server-2.a*" -print
/usr/lib/libname-server-2.a
real    0m8.644s              ## 8.6 sec  <<< NOTE!
user    0m1.669s
 sys    0m2.466s

## As regular user (victoria); I also use an alternate timing mechanism, as
## here I am using 2>/dev/null to suppress "Permission denied" warnings:

$ START="$(date +"%s")" && find 2>/dev/null / -path /mnt -prune -o \
    -name "*libname-server-2.a*" -print; END="$(date +"%s")"; \
    TIME="$((END - START))"; printf 'find command took %s sec\n' "$TIME"
/usr/lib/libname-server-2.a
find command took 3 sec     ## ~3 sec  <<< NOTE!

... finds that file in just a few seconds, while this take much longer (appearing to recurse through all of the "excluded" directories):

## As sudo (#), to avoid numerous "Permission denied" warnings:

time find / -path /mnt/ -prune -o -name "*libname-server-2.a*" -print
find: warning: -path /mnt/ will not match anything because it ends with /.
/usr/lib/libname-server-2.a
real    33m10.658s            ## 33 min 11 sec (~231-663x slower!)
user    1m43.142s
 sys    2m22.666s

## As regular user (victoria); I also use an alternate timing mechanism, as
## here I am using 2>/dev/null to suppress "Permission denied" warnings:

$ START="$(date +"%s")" && find 2>/dev/null / -path /mnt/ -prune -o \
    -name "*libname-server-2.a*" -print; END="$(date +"%s")"; \
    TIME="$((END - START))"; printf 'find command took %s sec\n' "$TIME"
/usr/lib/libname-server-2.a
find command took 1775 sec    ## 29.6 min

Solution 2

The other solution offered in this thread (SO#4210042) also performs poorly:

## As sudo (#), to avoid numerous "Permission denied" warnings:

time find / -name "*libname-server-2.a*" -not -path "/mnt"
/usr/lib/libname-server-2.a
real    33m37.911s            ## 33 min 38 sec (~235x slower)
user    1m45.134s
 sys    2m31.846s

time find / -name "*libname-server-2.a*" -not -path "/mnt/*"
/usr/lib/libname-server-2.a
real    33m11.208s            ## 33 min 11 sec
user    1m22.185s
 sys    2m29.962s

SUMMARY | CONCLUSIONS

Use the approach illustrated in "Solution 1"

find / -path /mnt -prune -o -name "*libname-server-2.a*" -print

i.e.

... -path <excluded_path> -prune -o ...

noting that whenever you add the trailing / to the excluded path, the find command then recursively enters (all those) /mnt/* directories -- which in my case, because of the /mnt/Backups/rsnapshot_backups/* subdirectories, additionally includes ~2.9 TB of files to search! By not appending a trailing / the search should complete almost immediately (within seconds).

"Solution 2" (... -not -path <exclude path> ...) likewise appears to recursively search through the excluded directories -- not returning excluded matches, but unnecessarily consuming that search time.

Searching within those rsnapshot backups:

To find a file in one of my hourly/daily/weekly/monthly rsnapshot backups):

$ START="$(date +"%s")" && find 2>/dev/null /mnt/Backups/rsnapshot_backups/daily.0 -name '*04t8ugijrlkj.jpg'; END="$(date +"%s")"; TIME="$((END - START))"; printf 'find command took %s sec\n' "$TIME"
/mnt/Backups/rsnapshot_backups/daily.0/snapshot_root/mnt/Vancouver/temp/04t8ugijrlkj.jpg
find command took 312 sec   ## 5.2 minutes: despite apparent rsnapshot size
                            ## (~4 GB), it is in fact searching through ~2.9 TB)

Excluding a nested directory:

Here, I want to exclude a nested directory, e.g. /mnt/Vancouver/projects/ie/claws/data/* when searching from /mnt/Vancouver/projects/:

$ time find . -iname '*test_file*'
./ie/claws/data/test_file
./ie/claws/test_file
0:01.97

$ time find . -path '*/data' -prune -o -iname '*test_file*' -print
./ie/claws/test_file
0:00.07

Aside: Adding -print at the end of the command suppresses the printout of the excluded directory:

$ find / -path /mnt -prune -o -name "*libname-server-2.a*"
/mnt
/usr/lib/libname-server-2.a

$ find / -path /mnt -prune -o -name "*libname-server-2.a*" -print
/usr/lib/libname-server-2.a

It's not the size of the files that slows find, it's the number of directory entries it must examine. So it's much worse if you have many, many small files (especially if they are all multiply linked!) than if you just have a handful of multi-gigabyte files.
@TobySpeight: good point. I mentioned the search space size to indicate scale, which also contains many files. A quick search of root (/) with sudo ls -R / | wc -l indicates ~76.5M files (most of which are backed up except "non-config" system files); /mnt/Vancouver/ with ls -R | wc -l indicates ~2.35M files; /home/victoria/ contains 0.668M files.
E
EIIPII

The following commands works:

find . -path ./.git -prune -o -print

If You have a problem with find, use the -D tree option to view the expression analysis information.

find -D tree . -path ./.git -prune -o -print

Or the -D all, to see all the execution information.

find -D all . -path ./.git -prune -o -print

P
Peter Mortensen

This is suitable for me on a Mac:

find . -name *.php -or -path "./vendor" -prune -or -path "./app/cache" -prune

It will exclude vendor and app/cache dir for search name which suffixed with php.


Better put single quotes around '*.php' or you're not going to find what you're looking for.
L
Lemmings19

I was using find to provide a list of files for xgettext, and wanted to omit a specific directory and its contents. I tried many permutations of -path combined with -prune but was unable to fully exclude the directory which I wanted gone.

Although I was able to ignore the contents of the directory which I wanted ignored, find then returned the directory itself as one of the results, which caused xgettext to crash as a result (doesn't accept directories; only files).

My solution was to simply use grep -v to skip the directory that I didn't want in the results:

find /project/directory -iname '*.php' -or -iname '*.phtml' | grep -iv '/some/directory' | xargs xgettext

Whether or not there is an argument for find that will work 100%, I cannot say for certain. Using grep was a quick and easy solution after some headache.


J
JaredTS486

For those of you on older versions of UNIX who cannot use -path or -not

Tested on SunOS 5.10 bash 3.2 and SunOS 5.11 bash 4.4

find . -type f -name "*" -o -type d -name "*excluded_directory*" -prune -type f

Could pass more than the specified directory.
C
Community

how-to-use-prune-option-of-find-in-sh is an excellent answer by Laurence Gonsalves on how -prune works.

And here is the generic solution:

find /path/to/search                    \
  -type d                               \
    \( -path /path/to/search/exclude_me \
       -o                               \
       -name exclude_me_too_anywhere    \
     \)                                 \
    -prune                              \
  -o                                    \
  -type f -name '*\.js' -print

To avoid typing /path/to/seach/ multiple times, wrap the find in a pushd .. popd pair.

pushd /path/to/search;                  \
find .                                  \
  -type d                               \
    \( -path ./exclude_me               \
       -o                               \
       -name exclude_me_too_anywhere    \
     \)                                 \
    -prune                              \
  -o                                    \
  -type f -name '*\.js' -print;         \
 popd

From stackoverflow.com/questions/4210042/…, I learned that the syntax used for the -path must match the name that find would print if it were to print the directory so, for example, find . -path ./.git -prune -o -print, or find $HOME/foo -path $HOME/foo/.git -prune -o -print Some of the answers just say -path somedir which unfortunately is not exact enough to be useful.
I
ImportanceOfBeingErnest

I tried command above, but none of those using "-prune" works for me. Eventually I tried this out with command below:

find . \( -name "*" \) -prune -a ! -name "directory"