Sign up ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. It's 100% free, no registration required.

I want to combine multiple conditions in a shell if statement, and negate the combination. I have the following working code for a simple combination of conditions:

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ; then
  # do stuff with the files
fi

This works fine. If I want to negate it, I can use the following working code:

if ! ( [ -f file1 ] && [ -f file2 ] && [ -f file3 ] ) ; then
  echo "Error: You done goofed."
  exit 1
fi
# do stuff with the files

This also works as expected. However, it occurs to me that I don't know what the parentheses are actually doing there. I want to use them just for grouping, but is it actually spawning a subshell? (How can I tell?) If so, is there a way to group the conditions without spawning a subshell?

share|improve this question
    
I suppose I could also use boolean logic to determine the equivalent expression: if ! [ -f file1 ] || ! [ -f file 2 ] || ! [ -f file3 ] ; then but I'd like a more general answer. – Wildcard 22 hours ago
    
You can also negate inside the braces, e. g. if [[ ! -f file1 ]] && [[ ! -f file2 ]]; then – DopeGhoti 22 hours ago
1  
Yes, I just tested if [ ! 1 -eq 2 ] && [ ! 2 -eq 3 ]; then echo yep; fi and it works. I just always write tests with double-braces as a matter of habit. Also, to ensure it's not bash, I further tested if /bin/test ! 1 -eq 2 && /bin/test ! 2 -eq 3 ; then echo yep; fi and it works that way also. – DopeGhoti 22 hours ago
1  
@Wildcard - [ ! -e file/. ] && [ -r file ] will drop directories. negate it as you like. of course, that's what -d does. – mikeserv 20 hours ago
1  
Related question (similar but not as much discussion and answers not so helpful): unix.stackexchange.com/q/156885/135943 – Wildcard 19 hours ago

7 Answers 7

up vote 12 down vote accepted

You need to use { list;} instead of (list):

if ! { [ -f file1 ] && [ -f file2 ] && [ -f file3 ]; }; then
  : do something
fi

Both of them are Grouping Commands, but { list;} executes commands in current shell environment.

Note that, the ; in { list;} is needed to delimit the list from } reverse word, you can use other delimiter as well. The space (or other delimiter) after { is also required.

share|improve this answer
    
Thank you! Something that tripped me up at first (since I use curly braces in awk more often than in bash) is the need for whitespace after the open curly brace. You mention "you can use other delimiter as well"; any examples? – Wildcard 22 hours ago
    
@Wildcard: Yes, the whitespace after the open curly brace is needed. An example for other delimiter is a newline, as you often do want you defining a function. – cuonglm 22 hours ago
    
@don_crissti: In zsh, you can use whitespace – cuonglm 22 hours ago
    
@don_crissti: the & is also a separator, you can use { echo 1;:&}, multiple newline can be use as well. – cuonglm 21 hours ago
2  
You can use any metacharacter quote from manual they must be separated from list by whitespace or another shell metacharacter. In bash they are: metacharacter: | & ; ( ) < > space tab . For this specific task, I believe that any of & ; ) space tab will work. .... .... From zsh (as a comment): each sublist is terminated by ;', &', &|', &!', or a newline. – BinaryZebra 19 hours ago

You could use entirely the test functionality to achieve what you want. From the man page of test:

 ! expression  True if expression is false.
 expression1 -a expression2
               True if both expression1 and expression2 are true.
 expression1 -o expression2
               True if either expression1 or expression2 are true.
 (expression)  True if expression is true.

So your condition could look like:

if [ -f file1 -a -f file2 -a -f file3 ] ; then
    # do stuff with the files
fi

For negating use escaped parentheses:

if [ ! \( -f file1 -a -f file2 -a -f file3 \) ] ; then
    echo "Error: You done goofed."
    exit 1
fi
# do stuff with the files
share|improve this answer

others have noted the { compound command ;} grouping, but if you are performing identical tests on a set you might like to use a different kind:

if  ! for f in file1 file2 file3
      do  [ -f "$f" ] || ! break
      done
then  : do stuff
fi

...as is elsewhere demonstrated with { :;}, there is no difficulty involved with nesting compound commands...

Note that the above (typically) tests for regular files. If you're looking only for existing, readable files which are not directories:

if  ! for f in file1 file2 file3
      do  [ ! -d "$f" ] && 
          [   -r "$f" ] || ! break
      done
then  : do stuff
fi

If you don't care whether they are directories or not:

if  ! command <file1 <file2 <file3
then  : do stuff
fi

...works for any readable, accessible file, but will likely hang for fifos w/out writers.

share|improve this answer

To portably negate a complex conditional in shell, you must either apply De Morgan's law and push the negation all the way down inside the [ calls...

if [ ! -f file1 ] || [ ! -f file2 ] || [ ! -f file3 ]
then
    # do stuff
fi

... or you must use then :; else ...

if [ -f file1 ] && [ -f file2 ] && [ -f file3 ]
then :
else
  # do stuff
fi

if ! command is not portably available and neither is [[.

If you don't need total portability, don't write a shell script. You're actually more likely to find /usr/bin/perl on a randomly selected Unix than you are bash.

share|improve this answer
1  
! is POSIX which nowadays is portable-enough. Even systems that still ship with the Bourne shell also have a POSIX sh somewhere else on the filesystem which you can use to interpret your standard syntax. – Stéphane Chazelas 19 hours ago

This is not a full-on answer to your main question, but I noticed that you mention compound testing (readable file) in a comment; e.g.,

if [ -f file1 ] && [ -r file1 ] && [ -f file2 ] && [ -r file2 ] && [ -f file3 ] && [ -r file3 ]

You can consolidate this a little by defining a shell function; e.g.,

readable_file()
{
    [ -f "$1" ]  &&  [ -r "$1" ]
}

Add error handling to (e.g., [ $# = 1 ]) to taste.  The first if statement, above, can now be condensed to

if readable_file file1  &&  readable_file file2  &&  readable_file file3

and you can shorten this even further by shortening the name of the function.  Likewise, you could define not_readable_file() (or nrf for short) and include the negation in the function.

share|improve this answer
    
Nice, but why not just add a loop? readable_files() { [ $# -eq 0 ] && return 1 ; for file in "$@" ; do [ -f "$file" ] && [ -r "$file" ] || return 1 ; done ; } (Disclaimer: I tested this code and it works but it wasn't a one-liner. I added semicolons for posting here but didn't test their placement.) – Wildcard 16 hours ago
    
Good point.  I would raise the minor concern that it hides more of the control (conjunction) logic in the function; somebody who reads the script and sees if readable_files file1 file2 file3 wouldn't know whether the function was doing AND or OR — although (a) I guess it's fairly intuitive, (b) if you're not distributing the script, it's good enough if you understand it, and (c) since I suggested abbreviating the function name into obscurity, I'm in no position to talk about readability.  … (Cont’d) – G-Man 11 hours ago
    
(Cont’d) …  BTW, the function is fine the way you wrote it, but you can replace for file in "$@" ; do with for file do — it defaults to in "$@", and (somewhat counter-intuitively) the ; is not only unnecessary but actually discouraged. – G-Man 11 hours ago

You can negate inside the brace tests also, so to reuse your original code:

if [[ ! -f file1 ]] || [[ ! -f file2 ]] || [[ ! -f file3 ]] ; then
  # do stuff with the files
fi
share|improve this answer
2  
You need || instead of && – cuonglm 22 hours ago
    
The test is not "are any of the files not there", the test is "are none of the files there". Using || would execute the predicate if any of the files are not present, not if and only iff all of the files are not present. NOT( A AND B AND C) is the same as (NOT A) AND (NOT B) AND (NOT C); see the original question. – DopeGhoti 22 hours ago
    
@DopeGhoti, cuonglm is right. It's if not (all files there) so when you split it up this way you need || instead of &&. See my first comment below my question itself. – Wildcard 22 hours ago
2  
@DopeGhoti: not (a and b) is the same as (not a) or (not b). See en.wikipedia.org/wiki/De_Morgan%27s_laws – cuonglm 22 hours ago
1  
It must be Thursday. I never could get the hang of Thursdays. Corrected, and I will leave my foot-in-mouth for posterity. – DopeGhoti 22 hours ago

It's not spawning a subshell. The (...) just groups the commands and runs them in the existing shell.

EDIT: To test whether you're in a subshell you can compare the PID of your current shell with the PID of the interactive shell.

echo $BASHPID; (echo $BASHPID); 

It turns out I was wrong. The commands within the (...) are run in a subshell.

share|improve this answer
6  
() does spawn subshell..perhaps you meant {}.... – heemayl 22 hours ago
    
@heemayl, that's the other part of my question—is there some way I can see or demonstrate on my screen the fact of a subshell being spawned? (That could really be a separate question but would be good to know here.) – Wildcard 22 hours ago
1  
@heemayl You're right! I had done echo $$ && ( echo $$ ) to compare the PID's and they were the same but just before I submitted the comment telling you you were wrong I tried one other thing. echo $BASHPID && ( echo $BASHPID ) gives different PID's – David King 22 hours ago
1  
@heemayl echo $BASHPID && { echo $BASHPID; } give the same PID as you suggested would be the case. Thanks for the education – David King 22 hours ago
    
@DavidKing, thanks for the test commands using $BASHPID, that's the other thing I was missing. – Wildcard 22 hours ago

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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