10

I want to display all directories, that do not contain files with a specific file ending. Therefore I tried using the following code:

find . -type d \! -exec test -e '{}/*.ENDING' \; -print

In this example I wanted to display all directories, that do not contain files with the ending .ENDING, but this does not work.

Where is my mistake?

1

8 Answers 8

7

Here's a solution in three steps:

temeraire:tmp jenny$ find . -type f -name \*ENDING -exec dirname {} \; |sort -u > /tmp/ending.lst
temeraire:tmp jenny$ find . -type d |sort -u > /tmp/dirs.lst
temeraire:tmp jenny$ comm -3 /tmp/dirs.lst /tmp/ending.lst 
1
  • 2
    One-liner without temporary files if using the Bash shell: comm -3 <(find . -type f -name \*ENDING -exec dirname {} \; |sort -u) <(find . -type d |sort -u) Jun 16, 2014 at 18:34
4

Here we go!

#!/usr/bin/env python3
import os

for curdir,dirnames,filenames in os.walk('.'):
  if len(tuple(filter(lambda x: x.endswith('.ENDING'), filenames))) == 0:
    print(curdir)

Or alternately (and more pythonic):

#!/usr/bin/env python3
import os

for curdir,dirnames,filenames in os.walk('.'):
    # Props to Cristian Cliupitu for the better python
    if not any(x.endswith('.ENDING') for x in filenames):
        print(curdir)

Bonus DLC Content!

The (mostly) corrected version of the find command:

find . -type d \! -exec bash -c 'set nullglob; test -f "{}"/*.ENDING' \; -print
4
  • I think the find solutions fails with test: ...: binary operator expected if there are multiple *.ENDING files in a directory. Jun 16, 2014 at 17:03
  • next(filter(lambda x: x.endswith('.ENDING'), filenames)) could also be written using generator comprehension i.e. next(x for x in filenames if x.endswith('.ENDING')). Jun 16, 2014 at 17:55
  • @CristianCiupitu: Truth: the find command is hacky and not really fully tested. Generator comprehension - yeah that's another nice way to do it.
    – MikeyB
    Jun 16, 2014 at 17:59
  • 1
    I think an even nicer solution would be to use if not any(x.endswith('.ENDING') for x in filenames) based on the fact that any returns False for an empty iterable. Jun 16, 2014 at 18:15
3

The shell expands the *, but in your case there's no shell involved, just the test command executed by find. Hence the file whose existence is tested, is literally named *.ENDING.

Instead you should use something like this:

find . -type d \! -execdir sh -c 'test -e {}/*.ENDING' \; -print

This would result in sh expanding *.ENDING when test is executed.

Source: find globbing on UX.SE

8
  • Does not work. I get the following error: sh: -c: line 0: syntax error near unexpected token ('`. My directory names have the format 'xyz (dfdf)'. In fact its a calibre library.
    – bamboo
    Jun 16, 2014 at 14:15
  • 1
    When I try it, I get sh: line 0: test: foo1/bar.ENDING: binary operator expected for the directory containing a file with the ending ENDING.
    – Jenny D
    Jun 16, 2014 at 14:19
  • This seems to work for me with plain a.ENDING filenames
    – user9517
    Jun 16, 2014 at 14:24
  • I have the following test directory structure: test->test (test)->test.ending After using this code I get sh: -c: line 0: syntax error near unexpected token (' sh: -c: line 0: 'test -e test (test)/*.ending' ./test (test)`. But when I change .ending to .xyz I get the same result. This is because i have bracts as directory name, right? How can I fix that?
    – bamboo
    Jun 16, 2014 at 14:30
  • 3
    @bamboo I would give up with shell utilities and look for a different solution at this point.
    – user9517
    Jun 16, 2014 at 14:31
2

Inspired by Dennis Nolte's and MikeyB's answers, I came up with this solution:

find . -type d                                                           \
  \! -exec bash -c 'shopt -s failglob; echo "{}"/*.ENDING >/dev/null' \; \
  -print 2>/dev/null

It works based on the fact that

if the failglob shell option is set, and no matches are found, an error message is printed and the command is not executed.

By the way, that's why stderr was redirected to /dev/null.

1

I'd do it in perl personally

#!/usr/bin/perl

use strict;
use warnings;

use File::Find;


#this sub is called by 'find' on each file it traverses. 
sub checkdir {
    #skip non directories. 
    return unless ( -d $File::Find::name );

    #glob will return an empty array if no files math - which evaluates as 'false'
    if ( not glob ( "$File::Find::name/*.ENDING" ) ) {
        print "$File::Find::name does not contain any files that end with '.ENDING'\n";
    }
}

#kick off the search on '.' - set a directory path or list of directories to taste.
#  - you can specify multiple in a list if you so desire. 
find (  \&checkdir, "." );

Should do the trick (works on my very simplistic test case).

1

There are many solutions here but many involve using an interpreted language. @sk8asd123 's answer seems ince. Here is another one:

find . -iname "*ENDING" | sed 's/\(.*\/\)\(.*\)/\1/' | sort -u

It is kind of dirty and has different logic than the question:

  1. First find the files (any expression find can understand),
  2. remove the file names (part after last /), and finally
  3. find unique lines with the help of sort.

but the end result is what was desired; folders containing files matching "*ENDING".

0

Heres a find one-liner.

find ./ -type d ! -regex '.*.ENDING$' -printf "%h\n" | sort -u

Edit: Oops, wont work either.

2
  • It doesn't work because you're testing the names of the directories, not of the files inside them. Jun 16, 2014 at 18:41
  • I've given it another try. Badly Jun 16, 2014 at 19:26
0
find . -type d '!' -exec sh -c 'ls -1 "{}"|egrep -q "*ENDING$"' ';' -print
  • q in egrep is for quiet

  • With egrep you can swap the regex that you need

  • ls -1 "{}" outputs the file names from the find command

You must log in to answer this question.

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