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?
8 Answers 8
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 inbash
) is the need for whitespace after the open curly brace. You mention "you can use other delimiter as well"; any examples?Wildcard– Wildcard2015年12月03日 18:54:47 +00:00Commented 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– cuonglm2015年12月03日 18:59:42 +00:00Commented Dec 3, 2015 at 18:59
-
@don_crissti: In
zsh
, you can use whitespacecuonglm– cuonglm2015年12月03日 19:14:32 +00:00Commented 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– cuonglm2015年12月03日 19:24:12 +00:00Commented Dec 3, 2015 at 19:24 -
2You 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– user797432015年12月03日 21:52:00 +00:00Commented Dec 3, 2015 at 21:52
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
.
-
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.Stéphane Chazelas– Stéphane Chazelas2015年12月03日 22:21:43 +00:00Commented 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– zwol2015年12月04日 18:19:52 +00:00Commented Dec 4, 2015 at 18:19
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
-
2Be aware: According to this document,
-a
,-o
, and(
/)
have been marked obsolescent and should be avoided.David P– David P2020年02月10日 05:23:45 +00:00Commented Feb 10, 2020 at 5:23 -
2Avoid
-a
/-o
binary test operators like the plague, they make for unreliable test expressions and are deprecated.Stéphane Chazelas– Stéphane Chazelas2022年02月03日 08:50:51 +00:00Commented Feb 3, 2022 at 8:50
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.
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– Wildcard2015年12月04日 00:51:05 +00:00Commented Dec 4, 2015 at 0:51 -
1Good 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 Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2015年12月04日 05:39:20 +00:00Commented 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
withfor file do
— it defaults toin "$@"
, and (somewhat counter-intuitively) the;
is not only unnecessary but actually discouraged.G-Man Says 'Reinstate Monica'– G-Man Says 'Reinstate Monica'2015年12月04日 05:39:48 +00:00Commented Dec 4, 2015 at 5:39
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
-
1This 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...)Serious Angel– Serious Angel2022年05月30日 18:46:51 +00:00Commented May 30, 2022 at 18:46
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
-
2You need
||
instead of&&
cuonglm– cuonglm2015年12月03日 18:43:55 +00:00Commented 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– DopeGhoti2015年12月03日 18:47:33 +00:00Commented 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– Wildcard2015年12月03日 18:51:43 +00:00Commented 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_lawscuonglm– cuonglm2015年12月03日 18:52:54 +00:00Commented Dec 3, 2015 at 18:52 -
1It must be Thursday. I never could get the hang of Thursdays. Corrected, and I will leave my foot-in-mouth for posterity.DopeGhoti– DopeGhoti2015年12月03日 18:54:04 +00:00Commented Dec 3, 2015 at 18:54
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– heemayl2015年12月03日 18:36:02 +00:00Commented 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– Wildcard2015年12月03日 18:43:58 +00:00Commented 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'sDavid King– David King2015年12月03日 18:44:02 +00:00Commented 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 educationDavid King– David King2015年12月03日 18:44:58 +00:00Commented 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– Wildcard2016年09月10日 02:12:08 +00:00Commented Sep 10, 2016 at 2:12
if ! [ -f file1 ] || ! [ -f file 2 ] || ! [ -f file3 ] ; then
but I'd like a more general answer.if [[ ! -f file1 ]] && [[ ! -f file2 ]]; then
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 notbash
, I further testedif /bin/test ! 1 -eq 2 && /bin/test ! 2 -eq 3 ; then echo yep; fi
and it works that way also.[ ! -e file/. ] && [ -r file ]
will drop directories. negate it as you like. of course, that's what-d
does.