3

I would like to find all files which reside in any directory foo. For example, consider the following files:

foo/w
a/foo/x
a/b/foo/y
a/b/c/foo/z
a/b/c/foo/bar/n

I would like to find the files w,x,y,z and get them listed as above, i.e., with their relative paths. My attempt was something like

$ find . -path '**/foo' -type f

which doesn't work. The search should not include file n, i.e., only files whose parent directory is named foo we are interested in.

Max Maier
  • 393

6 Answers6

6

Not using find, but globbing in the zsh shell:

$ printf '%s\n' **/foo/*(^/)
a/b/c/foo/z
a/b/foo/y
a/foo/x
foo/w

This uses the ** glob, which matches "recursively" down into directories, to match any directory named foo in the current directory or below, and then *(^/) to match any file in those foo directories. The (^/) at the end of that is a glob qualifier that restricts the pattern from matching directories (use (.) to match regular files only, or (-.) to also include symbolic links to regular files).

In bash:

shopt -s globstar nullglob

for pathname in **/foo/*; do
    if [[ ! -d "$pathname" ]] || [[ -h "$pathname" ]]; then
        printf '%s\n' "$pathname"
    fi
done

I set the nullglob option in bash to remove the pattern completely in case it does not match anything. The globstar shell option enables the use of ** in bash (this is enabled by default in zsh).

Since bash does not have the glob qualifiers of zsh, I'm looping over the pathnames that the pattern matches and test each match to make sure it's not a directory before printing it. Change the "! -d || -h" test to a "-f && ! -h" test to instead pick out only regular files, or just a single "-f" test to also print symbolic links to regular files.

Kusalananda
  • 333,661
3

I'm on RHEL 7. This works for me:

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

Note the DOT before the -path. (Or substitute your path there, such as /home/$USER)

The DOT says "Start looking in the current directory"

the -path says "Look for anything, followed by a sub-directory named foo, followed by anything" except for directories nested under "foo".

The -type f says give me only the files in a matching directory.

Looks like

-path "*/foo/*" ! -path '*/foo/*/*'  

Doesn't get everything as it misses things like ./a/foo/b/foo/c.

If you can guarantee that file and directory names won't contain newline characters, you could post-process the output with awk like:

find . -path "*/foo/*" -type f | awk -F/ '$(NF-1) == "foo"'
Scottie H
  • 664
3

** has no special meaning in find patterns. -path '*/foo/*' would find all files under a directory called foo, including files in subdirectories. -path '*/foo/*' ! -path '*/foo/*/*' would exclude files like a/foo/b/foo/c. I don't think you can do this with just one invocation of POSIX find.

With find implementations that support -regex (GNU, BusyBox, FreeBSD, NetBSD), you can use that to ensure that there's a single / after foo.

find . -regex '.*/foo/[^/]*' -type f

Alternatively, you can use find to locate the foo directories and a shell to enumerate files in this directory.

Another potential approach would be to invoke find again, but I can't find a pure POSIX find solution that actually works. With any POSIX find, invoked again for each foo directory:

find . -name foo -type d -exec find {} -type d ! -name foo -prune -o -type f \;

Beware that this mostly works, but not quite, and it's a little fragile. You need -type d -prune to avoid recursing into subdirectories, but with just -type d -prune, find would stop at the foo directory. ! -name foo does not prune foo/foo, so a file like foo/foo/bar will be reported twice. You can't use -exec in the inner find because its {} would be interpreted by the outer find. If your find has -maxdepth (which is being considered for inclusion in the next version of POSIX), you can make this reliable, but there's still this limitation against -exec:

find . -name foo -type d -exec find {} -maxdepth 1 -type f \;

With any POSIX find and sh:

find . -name foo -type d -exec sh -c '
    for x in "$0/"* "$0/".*; do
      if [ -f "$x" ] && ! [ -L "$x" ]; then
        …;
      fi;
    done
' {} \;

Substitute the code you want to run on the file names for .

2

If you can guarantee that file and directory names won't contain newline characters, one method is to find all files, then use grep to match only the desired results:

find . -type f | grep '/foo/[^/]*$'
Jim L.
  • 7,997
  • 1
  • 13
  • 27
0

The first thing that comes to mind is:

find $ROOT_PATH -type d -name foo | xargs -n1 -I{} find {} -type f

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
-2

Tried with Below approach

find  . -type d -iname "*foo* -exec ls -ltr {} \; 2>/dev/null| awk '!/^d/||/^l/{print $0}'