17

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?

  • 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 '15 at 18:31
  • You can also negate inside the braces, e. g. if [[ ! -f file1 ]] && [[ ! -f file2 ]]; then – DopeGhoti Dec 3 '15 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 '15 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 '15 at 21:06
  • 1
    Related question (similar but not as much discussion and answers not so helpful): unix.stackexchange.com/q/156885/135943 – Wildcard Dec 3 '15 at 21:29
28

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.

  • 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 '15 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 '15 at 18:59
  • @don_crissti: In zsh, you can use whitespace – cuonglm Dec 3 '15 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 '15 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 '15 at 21:52
8

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
5

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.

  • 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 Dec 3 '15 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 '15 at 18:19
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.

  • 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 '15 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) – G-Man Dec 4 '15 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. – G-Man Dec 4 '15 at 5:39
0

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
  • 2
    You need || instead of && – cuonglm Dec 3 '15 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 '15 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 '15 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 '15 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 '15 at 18:54
-1

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
$ 
  • 6
    () does spawn subshell..perhaps you meant {}.... – heemayl Dec 3 '15 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 '15 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 '15 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 '15 at 18:44
  • @DavidKing, thanks for the test commands using $BASHPID, that's the other thing I was missing. – Wildcard Dec 3 '15 at 18:52

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.