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.

0

6 Answers 6

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.

0
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 .

3
  • One way to nest -execs is to use a shell as in -exec sh -c 'for file do find "$file" -exec cmd \{\} +; done' sh {} + Sep 18, 2019 at 6:13
  • 1
    And the -prune problem can then be addessed with find "$file/." ! -name . -prune... Sep 18, 2019 at 6:14
  • @StéphaneChazelas Sure but for this particular example, if you're going to use a shell, you don't need a nested find invocation at all. I was discussing a find-without-sh solution. Sep 18, 2019 at 6:31
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"'
5
  • That is pretty close to what I'm locking for. I updated my question in order to be more precise. This solution finds any file whose path contains a directory named foo. However, I'm interested only in files whose parent is named foo.
    – Max Maier
    Sep 17, 2019 at 18:49
  • Ah, sorry I missed that. I'll work on an update.
    – Scottie H
    Sep 17, 2019 at 19:34
  • I think @Jeff added that. Thanks for that!
    – Scottie H
    Sep 17, 2019 at 19:57
  • 4
    Note: a/foo/c/foo/z should be found. Sep 17, 2019 at 20:02
  • Just updated the answer to show that.
    – Scottie H
    Sep 17, 2019 at 22:22
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/[^/]*$'
0

The first thing that comes to mind is:

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

1
  • Unfortunately, this also finds the file n, which should be excluded since it lives in bar, not in foo.
    – Kusalananda
    Sep 17, 2019 at 19:20
-2

Tried with Below approach

find  . -type d -iname "*foo* -exec ls -ltr {} \; 2>/dev/null| awk '!/^d/||/^l/{print $0}'
1
  • Unbalanced double quote. Also note that the output format should be a pathname, not a filename.
    – Kusalananda
    Sep 18, 2019 at 5:30

You must log in to answer this question.

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