3

I am currently aware of how to search directories and list those which do NOT contain a specific file like so:

find parent_directory -mindepth 1 -maxdepth 1 -type d '!' -exec sh -c 'ls -1 "{}"|egrep -i -q "^file_name$"' \; -print

but now I want to use the resulting directories to check whether or not they DO contain another file but I can't manage to do that in an extended one-line expression. Is this possible somehow?

3

You are making this far more complicated than needed. You don't seem to want to recurse into subdirectories, so all you need to find those directories that don't have a specific file is:

for dir in */; do [ ! -e "$dir"/"$filename" ] || printf '%s\n' "$dir"; done

And, to see which of those do have another file:

for dir in */; do 
    [ ! -e "$dir"/"$filename1" ] && 
    [ -e "$dir"/"$filename2" ] && 
    printf '%s\n' "$dir"; 
done

Or, in a slightly clearer syntax:

for dir in */; do 
    if [ ! -e "$dir"/"$filename1" ]; then 
        if [ -e "$dir"/"$filename2" ]; then 
            printf '%s\n' "$dir"; 
        fi 
    fi
 done

This is all done using the built-in tools of the shell. Specifically:

  • [ : this, and the accompanying ] are synonyms for the test builtin (see help [ or help test if using an sh-style shell). They are ways of writing test operations in the shell.
  • -e : this tests whether a file/directory etc exists. See help test. The simple format is: [ -e file ] which will return true if file exists.
  • [ ! -e filename ] : the ! simply inverses the test. So [ ! -e file ] will be true if file does not exist.

Taken together, this means the command above does:

## Iterate over every directory (dirs only because of the '/' in '*/')
## saving each of them in the variable $dir.
for dir in */; do 
    ## If this $dir does not contain $filename1
    if [ ! -e "$dir"/"$filename1" ]; then 
        ## If this $dir does contain $filename2
        if [ -e "$dir"/"$filename2" ]; then 
            ## Print the directory name
            printf '%s\n' "$dir"; 
        fi 
    fi
done

To run this, of course, you need to first set $filename1 and $filename2 accordingly. For example:

filename1="unwantedFile"
filename2="wantedFile"
  • @uitty400 please see update. Is that clear? You can also run help test on your machine for more details. – terdon Jul 24 '17 at 9:35
  • Thanks! I was afraid my comment was stupid so I deleted it. But your explanation helps a lot! – uitty400 Jul 24 '17 at 9:36
  • @uitty400 it is never stupid to ask for clarification! – terdon Jul 24 '17 at 9:37
  • Note that contrary to find's -type d, the shell's */ also includes symlinks to directories and excludes hidden directories. Use zsh and *(ND/) instead to have a closer equivalent to find's approach. – Stéphane Chazelas Jul 24 '17 at 11:07
4

You can determine your required directories the following manner also:

find . -maxdepth 2 -path '*/*/wanted.txt' -type f \
  -execdir test ! -f unwanted.txt \; -execdir pwd \;

How this works is as follows:

  • We look for entries till the depth of 2.
  • The -path option will further constrain them to be exactly a depth of 2 due to the presence of 2 slashes as the maxdepth precludes from going any further and 2 explict slashes prevent from going under depth of 2.
  • The wanted.txt entry found at the depth level of 2 better be a regular file ensured by -type f
  • The -execdir option will elevate the operation to the directory in which the wanted.txt resides and hence the test command will be looking for the unwanted file there.
  • Next it is a simple matter of printing the directory (elevated due to -execdir) where the unwanted file was not found.
  • Without meaning to belabor this point, a directory can contain a particular file just once, hence the -execdir operations are run once / directory and only on those directories that are at least sure to contain wanted.txt regular files.
  • I like @terdon's answer very much, but since I asked for how to do it with find in a one-liner I will accept this answer as the chosen solution. – uitty400 Jul 25 '17 at 10:07
  • @uitty400 terdon's answer is actually a 1-liner, it's not written that way. for d in */; do [ ! -e "$d/notwant" ] && [ -e "$d/want" ] && echo "$d"; done. Plus that method is very clear & direct. You may want to rethink your decision... – user218374 Jul 25 '17 at 11:47
3

With zsh, to list the directories in the current directory that contain a musthave file and not a mustnothave file:

contain() [[ -e $REPLY/$1 || -L $REPLY/$1 ]]

printf '%s\n' *(D/e(contain musthave)^e(contain mustnothave))

Note that doing:

find ... -exec sh -c 'ls {}' \;

is not only not portable but is also a command injection vulnerability. For instance, if there's a directory called $(reboot) or ;reboot, that would run the ls $(reboot) or ls ;reboot command lines causing a reboot. {} should never be embedded in a code argument (of sh or any other language) where it may be misinterpreted. Use:

find ... -exec sh -c 'ls "$1"' sh {} \;

instead.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.