36

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?

10
  • 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
    Dec 3, 2015 at 18:31
  • 1
    You can also negate inside the braces, e. g. if [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
    – DopeGhoti
    Dec 3, 2015 at 18:39
  • 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
    Dec 3, 2015 at 18:42
  • 1
    @Wildcard - [ ! -e file/. ] && [ -r file ] will drop directories. negate it as you like. of course, that's what -d does.
    – mikeserv
    Dec 3, 2015 at 21:06
  • 2
    Related question (similar but not as much discussion and answers not so helpful): unix.stackexchange.com/q/156885/135943
    – Wildcard
    Dec 3, 2015 at 21:29

8 Answers 8

48

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.

7
  • 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
    Dec 3, 2015 at 18:54
  • @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
    Dec 3, 2015 at 18:59
  • @don_crissti: In zsh, you can use whitespace
    – cuonglm
    Dec 3, 2015 at 19:14
  • @don_crissti: the & is also a separator, you can use { echo 1;:&}, multiple newline can be use as well.
    – cuonglm
    Dec 3, 2015 at 19:24
  • 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.
    – user79743
    Dec 3, 2015 at 21:52
9

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.

2
  • 3
    ! 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. Dec 3, 2015 at 22:21
  • @StéphaneChazelas This is technically true but in my experience it is easier to rewrite a script in Perl than it is to deal with the hassle of locating and activating the POSIX-compliant shell environment starting from /bin/sh. Autoconf scripts do as you suggest, but I think we all know how little of an endorsement that is.
    – zwol
    Dec 4, 2015 at 18:19
7

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
2
  • 2
    Be aware: According to this document, -a, -o, and (/) have been marked obsolescent and should be avoided.
    – David P
    Feb 10, 2020 at 5:23
  • Avoid -a/-o binary test operators like the plague, they make for unreliable test expressions and are deprecated. Feb 3, 2022 at 8:50
5

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.

2

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.

3
  • 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
    Dec 4, 2015 at 0:51
  • 1
    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) Dec 4, 2015 at 5:39
  • 1
    (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. Dec 4, 2015 at 5:39
2

Why not this?

[[ ! ( -f file1 && -f file2 && -f file3 ) ]]

Numerical example:

i=1
j=-1
[[ ! ( i -eq 1 && j -eq -1 ) ]]
echo $?

Output:

1

In the other three combinations of i and j, the output is:

0
1
1

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

# Using || (or)
if [[ ! -f file1 || ! -f file2 || ! -f file3 ]]; then
    # Do stuff with the files
fi

# Using && (and)
if [[ ! ( -f file1 && -f file2 && -f file3 ) ]]; then
    # Do stuff with the files
fi
5
  • 2
    You need || instead of &&
    – cuonglm
    Dec 3, 2015 at 18:43
  • 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
    Dec 3, 2015 at 18:47
  • @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
    Dec 3, 2015 at 18:51
  • 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
    Dec 3, 2015 at 18:52
  • 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
    Dec 3, 2015 at 18:54
0

I want to use them just for grouping, but is it actually spawning a subshell? (How can I tell?)

Yes, the commands within the (...) are run in a subshell.

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); 

Example output, demonstrating that parentheses spawn a subshell and curly braces do not:

$ echo $BASHPID; (echo $BASHPID); { echo $BASHPID;}
50827
50843
50827
$ 
8
  • 6
    () does spawn subshell..perhaps you meant {}....
    – heemayl
    Dec 3, 2015 at 18:36
  • @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
    Dec 3, 2015 at 18:43
  • 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
    Dec 3, 2015 at 18:44
  • 1
    @heemayl echo $BASHPID && { echo $BASHPID; } give the same PID as you suggested would be the case. Thanks for the education
    – David King
    Dec 3, 2015 at 18:44
  • 1
    @Ruslan, David King: I've edited the answer to emphasize the accurate information which I found so helpful, and to remove the inaccurate points.
    – Wildcard
    Sep 10, 2016 at 2:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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