1

I have a hierarchy of directories. Some directories do not contain files and they only contain other directories. Some contain files.

For example:

- movies
  - 2022
    - action
      - movie.mp4
      - another-movie.mp4
  - 2023
    - drama
      - movie2.mp4
  - 2024
    - thriller
      - movie3.mp4
    - movie4.mp4

I want a find command that would provide this result:

/movies/2022/action
/movies/2023/drama
/movies/2024
/movies/2024/thriller

I read this and this and this, but I could not figure out a find command.

Update: A directory can have files and directories in it. I updated the question. For example in the 2024 I have a movie and another directory. The result should contain both /movies/2024 and /movies/2024/thriller directories.

2
  • Please make sure to always mention your operating system. Different systems have different implementations of find with different capabilities so we need to know what you are working with .
    – terdon
    Oct 15, 2023 at 11:21
  • "Some directories do not contain files and they only contain other directories." – Strictly these must be empty. In Unix/Linux a directory is a file of the type directory. Nov 15, 2023 at 1:15

5 Answers 5

6

Find the files of type regular and print the enclosing directory. Unique the list to ensure each directory is listed only once. If on a GNU system, that can be done reliably even in the presence of file paths containing newline characters with:

find /path -type f -print0 |
    LC_ALL=C sed -z 's!/[^/]*$!!' |
    LC_ALL=C sort -zu |
    tr '\0' '\n'

Standardly and assuming file paths don't contain newline characters:

find /path -type f -print |
    LC_ALL=C sed 's!/[^/]*$!!' |
    LC_ALL=C sort -u
14
  • 1
    ... or -printf '%h\0' to skip the sed step (AFAIK a find with -print0 will also support -printf). Oct 15, 2023 at 8:54
  • May I ask why have you used -z? I mean as much as I understand by using -print0 you remove everything and connect the results together. I tried your code without -z and it worked the same. I'm confused about it. Oct 15, 2023 at 9:27
  • In general, I would appreciate it if you explain your Regex for sed too. Or your entire command in general :D. Oct 15, 2023 at 9:29
  • hey @SaeedNeamati, that's a rather broad question. Could you explain what you specifically couldn't figure out? Oct 15, 2023 at 10:13
  • @MarcusMüller, basically I didn't get why using -print0 and removing new lines and then using -z flags for all commands. And I didn't understand the s! part of the sed too. Oct 15, 2023 at 11:49
2

Simply,

find movies -type f -print | \
    xargs -r dirname | \
    sort --uniq

Read man find xargs dirname sort.

Here's an explanation:

  • find outputs a list of all the files, and their directories, e.g. /movies/2022/action/movie.mp4 /movies/2022/action/another-movie.mp4 ... to STDOUT.
  • xargs packs as many filenames as will fit (see xargs --show-limits </dev/null), and repeatedly executes dirname until xargs runs out of filenames.
  • dirname chops off the righmost / and the filename. E.g. /movies/2022/action/movie.mp4 becomes /movies/2022/actionand /movies/2022/action/another-movie.mp4 also becomes /movies/2022/action/
  • sort --unique eliminates duplicate directory names.

This structure (find, xargs, post-process) is useful for many tasks. Put some effort into understanding it.

1

This should do it:

find /movies -type d -not -empty -links 2

To quote this answer:

The number of links is the number of hard links to the file. For a directory, the number of hard links is the number of (immediate) subdirectories plus the parent directory and itself.

So when the number of links is 2, there is only the parent directory (..) and itself (.), hence no subdirectories.


While this answer worked for the original question, it doesn't work for the later updated question anymore. I'll still leave it, as it might be helpful for others.

4
  • Can you please explain your command? Especially the -links 2 on how it makes it possible. Thanks Oct 15, 2023 at 8:55
  • @SaeedNeamati This answer is a good summary. The number of links is the number of hard links to the file. For a directory, the number of hard links is the number of (immediate) subdirectories plus the parent directory and itself.. So when the number of links is 2, there is only the parent directory and itself, hence no subdirectories. Oct 15, 2023 at 9:09
  • Ah I see, using the fact that subdirectories will contain a .. hardlink. Interesting! Oct 15, 2023 at 9:12
  • 1
    -nlinks 2 doesn't work with btrfs file systems. Its directories always have 1 hard link.
    – raf
    Nov 14, 2023 at 14:54
0

With zsh:

print -rC1 -- **/*(NDFe['()(($#)) $REPLY/*(ND.Y1)'])

Would print raw on 1 Columnt the Full (non-empty) directories that contain at least 1 file that can be determined to be a regular file (.), including Dot files (hidden ones).

Replace . with -. to also take into account symlinks eventually resolving to regular files, or ^/ for files of any type except directory or -^/ for that check to be done after symlink resolution. Remove the first D to not consider hidden directories and/or the second to not consider hidden files in those directories.

0

Disclaimer: I'm the current author of rawhide (rh) (see github.com/raforg/rawhide).

With rawhide (rh) you can do:

rh /movies 'd && !empty && "[ -n \"$(rh -ref -- %S)\" ]".sh'

/movies is a path to search.

The rest is the search criteria:

d means it's a directory.

!empty means it's not empty. This isn't needed but it makes it faster by reducing the number of shell processes created by the next bit.

"[ -n \"$(rh -ref -- %S)\" ]".sh runs the shell command [ -n "$(rh -ref -- %S)" ] which checks if there are any regular files in the candidate directory (with a nested use of rh).

rh -ref -- %S is short for rh -r -e f -- %S.

The -r is like find's -mindepth 1 -maxdepth 1 to only search one level down.

The -e f specifies the search criteria expression f which matches regular files.

The -- stops command line option parsing so as to prevent any malicious filenames from being interpreted as options to rh (e.g. -xreboot) (thanks Stéphane).

The %S is the name of the current candidate directory that the nested rh needs to search.

The [ -n ... ] tests that the nested rh command produced some output (i.e., that it found some regular files in the candidate directory).

20
  • 1
    It seems it escapes newlines as \<newline> in the expansion of %S which is incorrect for sh Nov 14, 2023 at 17:15
  • 1
    Also, you shouldn't use backticks as inside backticks, there's an extra layer of backslash processing. Use $(...) instead. Nov 14, 2023 at 17:15
  • 1
    Using backslash for escaping is not advisable in general. See Escape a variable for use as content of another script for details. Nov 14, 2023 at 17:16
  • 1
    You're also missing a -- which is likely introducing ACE vulnerabilities (like if there's a file called -e"reboot".sh Nov 14, 2023 at 17:26
  • 1
    I'd recommend passing the file paths as extra arguments to the shell so they can be referred to as "$1" or "$@" in the sh code (calling sh with sh, -c, code supplied by the user, sh, found-file), rather than embedding them quoted in the shell code as doing it properly in all contexts is tricky Nov 14, 2023 at 17:47

You must log in to answer this question.

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