Bash – If Syntax Confusion

bash

So,
Being new to GIT and thus extremely rusty in my bash commands and scripting I've been looking around for different syntax and scripting help. Now, I've found a lot of help and have been able to create the scripts and alias that will make my Git experience more pleasant.

However, I came across some nuances that seem to confuse me, specifically related to the "if" command.

if [ -z $1 ] ; #<- Zero length string
if [[ -z $1 ]] ; #<- Also Zero Length String
if [[ "$1" == -* ]] ; #<- Starts with - (hyphen)


if [ -z $1 ] && [ -z $2 ] ; #<- both param 1 & 2 are zero length
if [[ -z $1 ]] && [[ -z $2 ]] ; #<- Also both param 1 & 2 are zero length
if [[ "$1" == -* ]] || [[ "$2" == -* ]] ; #<- Either param 1 or 2 starts with -

if [ "$1" == -* ] || [ "$2" == -* ] ; #<- Syntax Failure, "bash: ]: too many arguments"

Why the discrepancy? How to know when the [[ (double) is required and when a [ (single) will do?

Thanks
Jaeden "Sifo Dyas" al'Raec Ruiner

Best Answer

First off, note that neither type of bracket is part of the syntax for if. Instead:

  • [ is another name for the shell built-in test;
  • [[ ... ]] is a separate built-in, with different syntax and semantics.

Here are excerpts from the bash documentation:

[ / test

test: test [expr]
    Evaluate conditional expression.

    Exits with a status of 0 (true) or 1 (false) depending on
    the evaluation of EXPR.  Expressions may be unary or binary.  Unary
    expressions are often used to examine the status of a file.  There
    are string operators and numeric comparison operators as well.

    The behavior of test depends on the number of arguments.  Read the
    bash manual page for the complete specification.

    File operators:

      -a FILE        True if file exists.
      (...)

[[ ... ]]

[[ ... ]]: [[ expression ]]
    Execute conditional command.

    Returns a status of 0 or 1 depending on the evaluation of the conditional
    expression EXPRESSION.  Expressions are composed of the same primaries used
    by the `test' builtin, and may be combined using the following operators:

      ( EXPRESSION )    Returns the value of EXPRESSION
      ! EXPRESSION              True if EXPRESSION is false; else false
      EXPR1 && EXPR2    True if both EXPR1 and EXPR2 are true; else false
      EXPR1 || EXPR2    True if either EXPR1 or EXPR2 is true; else false

    When the `==' and `!=' operators are used, the string to the right of
    the operator is used as a pattern and pattern matching is performed.
    When the `=~' operator is used, the string to the right of the operator
    is matched as a regular expression.

    The && and || operators do not evaluate EXPR2 if EXPR1 is sufficient to
    determine the expression's value.

    Exit Status:
    0 or 1 depending on value of EXPRESSION.

More simply said, [ requires one to take the normal care needed for bash expressions, quote to avoid interpolation, etc. So a proper way of testing for $foo being the empty string, or being unset, would be:

[ -z "$foo" ]

or

[[ -z $foo ]]

It's important to quote in the first case, because setting foo="a b" and then testing [ -z $foo ] would result in test -z receiving two arguments, which is incorrect.

The language for [[ .. ]] is different, and properly knows about variables, much in the way one would expect from a higher-level language than bash. For this reason, it is much less error-prone than classic [ / test.

Related Question