7

I have a file hierarchy like:

data
├── debug.log
├── messages
│   ├── msg001.txt
│   ├── msg002.txt
│   └── msg003.txt
└── pictures
    ├── msg002
    │   └── pic001.jpg
    └── msg003
        ├── pic001.jpg
        └── pic002.jpg

I would like to find all the files, and all the directories below the top two levels (data, data/messages, and data/pictures). All the things that are not a part of the fixed structure of the hierarchy, if that makes sense.

Can i do this with a single find invocation?

I can find the files:

$ find data -type f | sort
data/debug.log
data/messages/msg001.txt
data/messages/msg002.txt
data/messages/msg003.txt
data/pictures/msg002/pic001.jpg
data/pictures/msg003/pic001.jpg
data/pictures/msg003/pic002.jpg

And i can find the directories:

$ find data -mindepth 2 -type d | sort
data/pictures/msg002
data/pictures/msg003

But i can't combine these, because -mindepth is an option, not a test:

$ find data -type f -o \( -mindepth 2 -type d \) | sort
find: warning: you have specified the -mindepth option after a non-option argument -type, but options are not positional (-mindepth affects tests specified before it as well as those specified after it). Please specify options before other arguments.

data/messages/msg001.txt
data/messages/msg002.txt
data/messages/msg003.txt
data/pictures/msg002
data/pictures/msg002/pic001.jpg
data/pictures/msg003
data/pictures/msg003/pic001.jpg
data/pictures/msg003/pic002.jpg

(note that data/debug.log is not found here)

Is there any way to consider the depth in the hierarchy as a real test?

The best kludge i can think of is using a regexp on the path to recognise the top two levels of directories:

$ find data -type f -o \( -type d -regextype posix-extended \! -regex 'data(/[^/]+)?' \) | sort
0

4 Answers 4

5

Is there any way to consider the depth in the hierarchy as a real test?

There is in FreeBSD find, it has -depth N also as a condition:

% find data -type f -o -depth +1 -type d |sort
data/debug.log
data/messages/msg001.txt
data/messages/msg002.txt
data/messages/msg003.txt
data/pictures/msg002
data/pictures/msg002/pic001.jpg
data/pictures/msg003
data/pictures/msg003/pic001.jpg
data/pictures/msg003/pic002.jpg

(and yes, that can be easily confused with the -depth option.)

If you know the set of directories that are part of the "fixed structure" (and IMO you should, if the structure is indeed fixed), you could just exclude those:

% find data ! -path data ! -path data/messages ! -path data/pictures
...

or the same with grep:

% find data | grep -vEe '^(data|data/(pictures|messages))$'
...

(since you're already using the default output format of find, instead of find -print0, your filenames likely don't contain newlines)

2
  • 2
    There is not one BSD find. find is different between different BSDs. That -depth [+-]n AFAIK was introduced by FreeBSD. It's not available in NetBSD or OpenBSD for instance. Aug 22, 2023 at 19:20
  • 1
    @StéphaneChazelas, yep, I'm actually not surprised. It was sloppy of me not to check.
    – ilkkachu
    Aug 22, 2023 at 19:22
3

Note that -mindepth is a non-standard extension (initially from GNU find but since added to a few other implementations). Contrary to -maxdepth, it's easy to emulate with standard predicates though. For instance LC_ALL=C find . -path './*/*' emulates GNU's find . -mindepth 2.

So, here:

LC_ALL=C find data '(' -type d -path '*/*/*' -o -type f ')' -print0 |
  sort -z |
  tr '\0' '\n'

(you can't pipe the output of find to sort unless you use NUL-delimited records btw as file paths can be made of more than one line. -print0 and -z are GNU extensions, -print0 now common (and soon POSIX), -z less so).

2
  • How does LC_ALL=C help in that command? What is the corner case it deals with?
    – aviro
    Aug 22, 2023 at 19:30
  • 2
    @aviro * matches zero or more characters, while file paths are made of 1 or more bytes other than 0 which don't have to make up characters in the user's locale. For instance, with GNU find on a GNU system (using the GNU libc's fnmatch()), in a locale using the UTF-8 charmap -name '*' would not match on a file created with touch $'Ste\351phane' (which happens to be Stéphane encoded in iso8859-1 aka latin1 instead of UTF-8) or touch $'\x7f\x80' (file names don't have to be text) Aug 22, 2023 at 19:34
3

This is not the prettiest approach, but you could do something as follows with GNU find:

find data -printf '%d\0%y\0%p\n' | awk -F '\0' '$2 == "f" || $2 == "d" && $1 >= 2 {print $3}'

find with the -printf flag will print the following fields (separated by the null character - \0)

  %d     File's depth in the directory tree; 0 means the file is a starting-point.
  %y     File's type (like in ls -l), U=unknown type (shouldn't happen)
  %p     File's name.

Then the awk command will print the names for all the files ($2 == "f") and directories whose depth equals or greater than 2 ($2=="d" && $1 >= 2).

2

Disclaimer: I'm the current author of the rawhide (rh) program used in this response (see https://github.com/raforg/rawhide).

With rh, you can do it like this:

rh data 'f || (d && depth >= 2)'

This searches in the data directory for files (f), and for directories (d) whose depth is at least 2 (depth >= 2).

It works on Linux, FreeBSD, OpenBSD, NetBSD, macOS, Solaris, and Cygwin.

You must log in to answer this question.

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