if in Bash is similar to if conditional statements in other programming languages. The statement checks the values, such as equality or empty value. The one difference is the syntax of how if statement looks.

1   Syntax

The syntax in Bash is, from bash(1):

if list; then list; [ elif list; then list; ] ... [ else list; ] fi

Where list is, from bash(1)

a sequence of one or more pipelines separated by one of the operators ;, &, &&, or , and optionally terminated by one of ;, &, or <newline>.

and A pipeline is, from bash(1)

a sequence of one or more commands separated by one of the control operators | or |&. The format for a pipeline is:

[time [-p]] [ ! ] command [ [||&] command2 \... ]

If you understand above, you may ask where does that put [ ] and [[ ]] or even (( ))? Almost all of the code you will read would look like:

if [[ -z "$foobar" ]]; then
  do_something
fi

Those three are actually commands, first one is the shell builtin (= test), last two are compound commands (and keywords). What does that mean? It means that [[ ]] and other two are not part of if syntax.

I believe many of you are like me used to think [[ ]] is part of if syntax, therefore you may code like this:

command
ret=$?
if (( $? == 0 )); then :; fi

You can see the better way to code it in next section.

2   Checking exit status

if command; then :; fi

if relies on the last command in the list whose exit status (return value) will be used to decide which branch it should go next. When command returns 0, it runs the code; or runs code in else when command returns other than 0.

Sometimes, you may want to want to run code when the command fails, i.e. returns non-zero exit status:

if ! command; then :; fi

Again, ! is not part of if syntax, but is part of the pipeline, from bash(1):

If the reserved word ! precedes a pipeline, the exit status of that pipeline is the logical negation of the exit status as described above.

If you are crazy enough, you can

if
  command1
  command2
  command3
  ...
  command99
then
  :
fi

Only the exit status of command99 will be checked. I know nobody code like this, but you can do like that. To be more practical, it maybe more practical for while:

while prepare_something; command_to_check; do :; done

3   Checking values

There are plenty of pages about checking values, I am not going to write about it. Read bash(1), it should be enough, actually. One thing I want to mention is how to check multiple conditions:

if [[ "$foo" == "foo" ]] && [[ "$bar" == "bar" ]]; then command; fi

There is a shortcut for this, this can be rewritten as

[[ "$foo" == "foo" ]] && [[ "$bar" == "bar" ]] && command

3.1   test and [ ] vs. [[ ]]

One may ask the difference, to be perfectly honest, I dont know completely. The former is shell builtin command, the latter is shell reserved keyword. Shell built-in command is faster than external command and I believe reserved keyword may be faster, because from what I know about shell built-in command is loadable extension. (I have written one of my own for prompt) So reversed keyword may be faster because it may not need to access data through another layer. But I dont know what exact the implementation detail is, so dont quote me on this.

What I am certain is that [ ] will give you more compatibility. But the thumb of rule is to code with the one you are comfortable with.

3.2   (( )) vs. [[ ]]

[[ 1 -gt 0 ]] and (( 1 > 0 )) test exactly the same thing, however, there is slight performance difference, the latter, Arithmetic Evaluation, is a bit of faster, but hardly noticeable unless you do at million times scale.