39

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
    Commented Dec 3, 2015 at 18:31
  • 1
    You can also negate inside the braces, e. g. if [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
    – DopeGhoti
    Commented 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
    Commented 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
    Commented 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
    Commented Dec 3, 2015 at 21:29

8 Answers 8

52

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.

8
  • 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
    Commented 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
    Commented Dec 3, 2015 at 18:59
  • @don_crissti: In zsh, you can use whitespace
    – cuonglm
    Commented 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
    Commented 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
    Commented 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. Commented 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
    Commented 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
    Commented Feb 10, 2020 at 5:23
  • 2
    Avoid -a/-o binary test operators like the plague, they make for unreliable test expressions and are deprecated. Commented 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
    Commented 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) Commented 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. Commented 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
    This is the one of the most appropriate for Bash since 1998. Related: mywiki.wooledge.org/BashFAQ/061 (Is there a list of which features were added to...)
    – Artfaith
    Commented May 30, 2022 at 18:46
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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented 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
    Commented Sep 10, 2016 at 2:12

You must log in to answer this question.

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