11

I was trying to combine logical AND & OR in a bash script within if condition. Somehow I am not getting the desired output and it is hard to troubleshoot. I am trying to validate the input parameters passed to a shell script for no parameter and the first parameter passed is valid or not.

if [ "$#" -ne 1 ] && ([ "$1" == "ABC" ] || [ "$1" == "DEF" ] || [ "$1" == "GHI" ] || [ "$1" == "JKL" ]) 
then
echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
exit 1
fi

Can anyone point out what is going wrong here?

sijo0703
  • 557
  • 1
  • 8
  • 33

3 Answers3

15

The immediate problem with your statement is one of logic: you probably meant to write:

if [ "$#" -ne 1 ] || ! ([ "$1" = "ABC" ] || [ "$1" = "DEF" ] || [ "$1" = "GHI" ] || [ "$1" = "JKL" ]) 
then
  echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]" >&2
  exit 1
fi

That is: abort, if either more than 1 argument is given OR if the single argument given does NOT equal one of the acceptable values.

Note the ! to negate the expression in parentheses and the use of the POSIX-compliant form of the string equality operator, = (rather than ==).

However, given that you're using Bash, you can make do with a single [[ ... ]] conditional and Bash's regular-expression matching operator, =~:

if [[ $# -ne 1 || ! $1 =~ ^(ABC|DEF|GHI|JKL)$ ]] 
then
  echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]" >&2
  exit 1
fi

If POSIX compliance is not required, [[ ... ]] is preferable to [ ... ] for a variety of reasons. In the case at hand, $# and $1 didn't need quoting, and || could be used inside the conditional.

Note that =~ as used above works in Bash 3.2+, whereas the implicit extglob syntax used in anubhava's helpful answer requires Bash 4.1+;
in earlier versions you can, however, explicitly enable (and restore to its original value after) the extglob shell option: shopt -s extglob.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
8

BASH actually allows use of extended glob inside [[ ... ]] and have && inside as well.

So you can do:

if [[ $# -ne 1 && $1 == @(ABC|DEF|GHI|JKL) ]]; then
   echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
   exit 1
fi
anubhava
  • 761,203
  • 64
  • 569
  • 643
2

A few things:

  • [...] in bash is equivalent to the same test command (check the man page), so those && and || are not logical operators, but rather the shell equivalents
  • Parentheses in POSIX shell are not a grouping operator. They will work here, but they open a subshell, you are better off using standard test options of -a and -o (making your if statement if [ "$#" -ne 1 -a \( "$1" == "ABC" -o "$1" == "DEF" -o "$1" == "GHI" -o "$1" == "JKL" \) ], though based on your logic, it sounds like you actually want something like if [ "$#" -ne 1 -o \( "$1" != "ABC" -a "$1" != "DEF" -a "$1" != "GHI" -a "$1" != "JKL" \) ]. You probably can get better results with a case statement like follows:

usage() {
    echo "Usage: ./myscript.sh [ABC | DEF | GHI | JKL]"
}


if [ "$#" -ne 1 ] 
then
    usage
    exit 1
fi

case "$1" in
    ABC)
        echo "Found ABC"
        ;;
    DEF)
        echo "Found DEF"
        ;;
    GHI)
        echo "Found GHI"
        ;;
    JKL)
        echo "Found JKL"
        ;;
    *)
        usage
        exit 1
        ;;
esac

If you want to pass a set of possible static arguments in, you might want to look at the getopts special shell command.

Taywee
  • 1,313
  • 11
  • 17
  • Good points; quibble: the [POSIX spec. for `test`](http://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html) itself warns against use of `-a`, `-o`, and `()`: "The XSI extensions specifying the -a and -o binary primaries and the '(' and ')' operators have been marked obsolescent. (Many expressions using them are ambiguously defined by the grammar depending on the specific expressions being evaluated.) " – mklement0 Jun 28 '16 at 06:03
  • That's a good quibble. How would you alternately do this with just POSIX? command groups (`{...}`), &&, and ||? – Taywee Jun 28 '16 at 06:47
  • 1
    @Taywee: correct, you would use `[ ... ] && { [ ... ] || [ ... ]; }` – chepner Jun 28 '16 at 12:29
  • 1
    You can also shorten the `case` statement with `ABC|DEF|GHI|JKL) echo "Found $1" ;;`. – chepner Jun 28 '16 at 12:29
  • Command groups are a good idea (to avoid a subshell); a pitfall worth pointing out: with `test`, `-a` has higher precedence than `-o`, but the shell's `&&` and `||` have _equal_ precedence. – mklement0 Jun 28 '16 at 13:34
  • 1
    @chepner I know, I'm operating under the assumption that they'll probably do different things and might want to be handled independently. – Taywee Jun 28 '16 at 20:47