2

This answer on opening all files in vim except [condition]:

https://unix.stackexchange.com/a/149356/98426

gives an answer similar to this:

find . \( -name '.?*' -prune \) -o -type f -print

(I adapted the answer because my question here is not about vim)

Where the negated condition is in the escaped parentheses. However, on my test files, the following

find . -type f -not -name '^.*'

produces the same results, but is easier to read and write. The -not method, like the -prune method, prunes any directories starting with a . (dot). I am wondering what are the edge cases where the -not and the -prune -o -print method would have different results.

Findutils' infopage says the following:

-not expr: True if expr is false

-prune: If the file is a directory, do not descend into it. (and further explains that -o -print is required to actually exclude the top matching directory)

They seem to be hard to compare this way, because -not is a test and -prune is an action, but to me, they are interchangeable (as long as -o -print comes after -prune)

6
  • Doesn't the first also exclude files in hidden directories? Simple test case: Run in the top level of any git repository.
    – muru
    Jan 24, 2021 at 12:03
  • @Quasímodo My goal is to understand the difference between -prune -o -print and -not The linked answer did not fail so solve my problem. My question was inspired by the answer. I believe my question is self contained. It has nothing to do with the linked question per se. I also believe that this all is obvious from my question. I am open to suggestions about how to make the question more clear. Jan 24, 2021 at 13:07
  • 3
    Your find with -not doesn’t exclude directories starting with .; it will output any file name not starting with ^., regardless of any .s in its path components. Try it in a git workspace as muru suggested. (-name doesn’t match regexes.) Jan 24, 2021 at 13:25
  • @StephenKitt You are right, I confuse globs and regexes. Do you think I should edit the question and replace ^. with .* to reflect my original intention or should I leave it like this? Jan 24, 2021 at 14:48
  • 1
    @YordanGrigorov even if you used .*, it would still match filename not starting with . inside directories whose name do start with ..
    – muru
    Jan 24, 2021 at 14:58

2 Answers 2

6

First, note that -not is a GNU extension and is the equivalent of the standard ! operator. It has virtually no advantage over !.

The -prune predicate always evaluates to true and affects the way find walks the directory tree. If the file for which -prune is run is of type directory (possibly determined after symlink resolution with -L/-H/-follow), then find will not descend into it.

So -name 'pattern' -prune (short for -name 'pattern' -a -prune) is the same as -name 'pattern' except that the directories whose name matches pattern will be pruned, that is find won't descend into them.

-name '.?*' matches on files whose name starts with . followed by one character (the definition of which depends on the current locale) followed by 0 or more characters. So in effect, that matches . followed by one or more characters (so as not to prune . the starting directory).

So that matches hidden files with the caveat that it matches only those whose name is also entirely made of characters, that is are valid text in the current locale (at least with the GNU implementation).

So here,

find . \( -name '.?*' -a -prune \) -o -type f -a -print

Which is the same as

find . -name '.?*' -prune -o -type f -print

since AND (-a, implied) has precedence over OR (-o).

finds files that are regular (no symlink, directory, fifo, device...) and are not hidden and are not in hidden directories (assuming all file paths are valid text in the locale).

find . -type f -not -name '^.*'

Or its standard equivalent:

find . -type f ! -name '^.*'

Would find regular files whose name doesn't start with ^..

find . -type f ! -name '.*'

Would find regular files whose name doesn't start with ., but would still report files in hidden directories.

find . -type f ! -path '*/.*'

Would omit hidden files and files in hidden directories, but find would still descend into hidden directories (any level deep) only to skip all the files in them, so is less efficient than the approach using -prune.

3
  • Although Kusalananda's answer was easier for a noobie to understand, I have to admit that your answer covers a lot more of the find utility and ultimately gave me a deeper understanding of what is going on. Jan 24, 2021 at 15:00
  • 1
    It has virtually no advantage over !. The advantage is the syntax collision with shell history expansion in bash.
    – Riking
    Jan 25, 2021 at 3:27
  • 1
    @Riking, ! alone is left alone by csh history expansion (copied both by bash and zsh). If it weren't you'd have problems not only with find but also with test/[, ([ ! -f file ]) and the ! shell keyword. find/test existed before csh and csh existed before GNU find. Jan 25, 2021 at 7:11
5

The -prune predicate in find removes a branch of the search tree. Therefore, using -prune as in the first command in your question would only give you the same results as your second command if there are no files in directories with hidden names.

Example:

$ tree -a
.
|-- .hiddendir
|   `-- file
`-- dir
    `-- file

2 directories, 2 files
$ find . \( -name '.?*' -prune \) -o -type f -print
./dir/file
$ find . -type f ! -name '.*'
./dir/file
./.hiddendir/file

(I corrected the -name test here as you seem to use a nonsensical regular expression, and I used ! in place of the non-standard -not).

As you can see, since the name .hiddendir matches the globbing pattern .?*, it is removed from the search tree with the first command, and the file file beneath it is not found.

In the second command, the .hiddendir directory is not pruned from the search tree, so the file file in there is found.

2
  • Note that even if ^.* were a regular expression, it wouldn't make much sense as it would match anything (any number (*) of characters (.) at the start of the subject (^)). To match on hidden files, that would be ^\., or \..* for regexps implicitly anchored at the start and end (like GNU find's -regex, though that -regex matches on the full path, not the just the name). Jan 24, 2021 at 13:43
  • Thank you. Your answer is the easier one to follow since you focused on the highlighted part of my question. Jan 24, 2021 at 14:54

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .