1. Overview

When we work on the command line, we may need some procedures to run only if a condition is satisfied. Let’s imagine an example: run a list of commands depending on the characteristics of a file, let’s say: if it exists, the size, the name, the type, etc.

GNU/Linux provides us with a very powerful set of tools to help us achieve that.

In this tutorial, we’ll discuss conditional expressions, and we’ll construct some examples to clarify the use of these expressions.

2. Conditional Expressions with && and ||

Remember that every command in our shell is an expression that returns an integer value as a status. Returning 0 is a standard way to indicate success and 1 to indicate failure. Also, several other standard exit codes can be used.

When using a shell, we usually run multiple instructions chained together using tokens like “*;”*, &, &&, or ||.

The && and || tokens allow us to connect our commands with the help of their exit status.

The && token chains the commands executing what is on the right if and only if the instruction on the left has an exit status of zero.

And the || token chains the commands executing what is on the right if and only if the instruction on the left has an exit status different from zero.

Let’s create a quick example to clarify:

$ (exit 0) && echo True
True
$ (exit 1) || echo False
False

Now that we know about these tokens let’s see how to use them.

First, let’s create a file named some_file with the following content:

$ cat << __end > some_file
foo
this string exists
bar
__end

Using the grep command, let’s see how we can display a text if the string is present in the file:

$ grep -q "this string exists" some_file && { echo "Everything"; echo "is all right"; }

As the first instruction has an 0 exit status, the output of this command line will be:

Everything
is all right

Now, let’s try with:

$ grep -q "this string doesn't exist" some_file || { echo "Not"; echo "today"; }

The string we searched for this time doesn’t exist in the file, so the first instruction returns with a 1 – failure exit status. This means the output of this command line will be:

Not
today

We can compose commands by chaining them with these tokens. Let’s try using the same file and the same commands:

$ grep -q "this string doesn't exist" some_file \
&& { echo "Everything"; echo "is all right"; } \
|| { echo "Not"; echo "today"; }

Let’s take a closer look at the expression.

Here, we use the -q parameter to get the exit status, not the usual output in the stdout.

With that exit status:

  • If it’s 0 (if the string is in the file), then the list of commands right of the && token will execute
  • If the exit status is not 0 (if the string is not in the file), then what’s on the right of the token && will not execute; instead, the instructions to the right of the token || will be executed

So, the result of this command will be:

Not
today

Let’s see another example using these tokens to chain commands:

$ ( echo "Here 1"; exit 1 ) \
    && ( echo "Here 2" ) \
    && ( echo "Here 3" ) \
    || ( echo "Here 4"; exit 4 ) \
    && ( echo "Here 5"; exit 5 ) \
    || ( echo "Here 6"; exit 6 ) \
    || ( echo "Here 7"; exit 0 ) \
    && ( echo "Here 8" )

And the output will be:

Here 1
Here 4
Here 6
Here 7
Here 8

The && and the || tokens are similar to the AND and OR logical operations. However, they don’t follow the same precedence rules where AND has higher precedence over OR. The && and || tokens are executed strictly from left to right. These tokens allow us to control the course of an instruction flow and use them as an if-else syntax.

3. Building More Complex Expressions with if

In the previous examples, we saw that we can articulate a list of commands because the commands are conditional expressions. However, this syntax can be tricky for more complex commands.

To keep things clearer and create new ways of articulating instructions flow, shells have the keywords: if, then, elif, else, fi. So, for example, let’s use the if, then, else, and fi keywords:

$ if
    ( echo "Here 1" )
    ( echo "Here 2" )
    ( echo "Here 3" )
then
echo Inside the \"then\" sentence
else
echo Inside the \"else\" sentence
fi

And the output will be:

Here 1
Here 2
Here 3
Inside the "then" sentence

Now, let’s try the following:

$ if
    ( echo "Here 1" )
    ( echo "Here 2"; exit 2 )
    ( echo "Here 3"; exit 3 )
then
    echo Inside the \"then\" sentence
else
    echo Inside the \"else\" sentence
fi

And the output will be:

Here 1
Here 2
Here 3
Inside the "else" sentence

This is because this syntax focuses on the status of the last command executed. So that’s why it prints “Here 3” after a command with a non-zero status and also why the shell entered into the else block.

4. The [, test, [[, and ((, Tokens

The shells are commonly equipped with the tokens [, test, and [[ builtins that use conditional expressions.

Each of these tokens differs, but we’ll discuss their differences later. Let’s focus on what [, test, and [[ have in common.

Let’s see how we can check if a file is a directory using the standard /home directory:

$ [ -d /home ] && echo "It's a directory"
It's a directory

Here, we’ve used the -d option, which checks if the file exists and is a directory. We’ve surrounded the expression with the [ and ] tokens.

Next, let’s see another example that checks if 2 Strings are equal:

$ [ "this string" = "THIS STRING" ] \
    && echo They are equals \
    || echo They are different

And the output will be:

They are different

We can, of course, build a very similar example that compares integers using any of the options: “-eq,” “-ne,” “-lt,” “-le,” “-gt,” or “-ge.” It’s possible to check these options on the [ or test manual page.

Let’s use the -lt option to compare if one integer is smaller than the other:

$ [ 3 -lt 6 ] && echo "It's less than"
It's less than

*The listed examples can be built with any of the [, [[ or test commands.*

Now, let’s dig into what each token and command does and the differences between some.

4.1. The [ and test Builtins

[ and test are builtins that can help us perform the operations we list in the previous section.

With the [ builtin, we can surround a conditional expression. This will have the same validity as the test built-in.

In other words:

$ [ "string1" = "string2" ]

It’s almost the same as:

$ test "string1" = "string2"

4.2. The [[ Keyword

Compared to the previous tokens, the [[ is a keyword different from a built-in.

A built-in is a command or a function that will be executed in the shell instead of an external executable.

*A keyword is a word that the shell considers special; if the shell finds the keyword [[, then the shell will look for the closing ]].* We can list them by typing compgen -k.

Also, the [[ keyword is an extension of some shells like Bash.

4.3. The (( Token

(( is an extension of some shells that allows us to evaluate an expression as an arithmetic one.

Let’s see an example of using this:

(( var < 10 ))
(( var++ ))
(( var=1; var<=10; var++ ))

This operator will have an exit status of 0 if the value of the expression is non-zero; otherwise, the return status is 1.

4.4. The Differences Between [[, and [

Let’s review some of the main differences between [[ and [.

First, as we saw in the 4.1 and 4.2 sections, [[ is a keyword of our shell, but [, and test, are shell builtins

Another important difference is that [ is POSIX, while [[ is not. The best way to reason about [ is to think about it as a command, which it is. We can even check its manual page:

$ man [

Since it’s a simple command, it would be easier to understand its syntax.

*When using the <* or *> operators, we need to escape them if we want to use them inside [:*

$ [ "a" \< "b" ]

Because it’s a command, the shell parses special characters like < and > as redirection operators before the command is run

This is not the case if we want to use them inside the keyword [[.

Another notable difference is that the [[ keyword sorts lexicographically using the current locale, while [ uses ASCII ordering.

Next, the && and || operators will differ inside each pair of tokens.

While this will work:

$ [[ 1 = 1 && 2 = 2 ]] # returns True

This case will not:

$ [ 1 = 1 && 2 = 2 ] # returns a Bash error

But we can operate in the same way by doing the following:

$ [ 1 = 1 ] && [ 2 = 2 ]

The reason is that [ ] is a command, and && is a shell control operator that operates between commands, not within them. When the shell sees &&, it expects to find a command on both sides of it, not inside a command.

Also, as was mentioned previously, && is used between commands. We can use logical operators inside [], but we should use an option -a for AND and -o for OR. In this case, they will follow all the precedence rules of AND and OR operators.

The tokens also differ regarding word splitting and filename generation upon expansions.

If we define a variable like this:

$ var="a b"

Then, this will return true:

$ [[ $var = "a b" ]]

And this returns a syntax error because here expands as [ a b = ‘a b’ ]:

$ [ $var = "a b" ]
bash: [: too many arguments

This happens because [ treats all the values inside as parameters.

The use of the token “=” also differs:

Let’s create two examples:

$ [ abc = a?? ] # returns False
$ [[ abc = a?? ]] # return True

This behavior is because, using the built-in [, we perform a string comparison. On the other hand,  when using the [[ keyword, bash performs a pattern matching.

Finally, let’s note that [[ have the =~ operator.

While using the [[ token we can use the =~ token, but in [, we aren’t able to use it.

With this operator’s help, we can compare a string and an extended regular expression. The string to the operator’s right will be considered an extended regular expression.

Let’s create two examples where we deal with regular expressions using this operator:

$ [[ Baeldung =~ .*e.* ]] && echo True || echo False 
True 
$ [[ Baeldung2 =~ ^(b|B).*g$ ]] && echo True || echo False 
False

In summary, when to use one or the other depends on what we want to achieve.

As we can see, the [[ keyword has more options than the [ and test builtins, but the builtins are POSIX, so, using these, we can avoid losing portability.

5. A Quick Note About Spaces

We can use these options to evaluate arguments, but let’s remember that we need to follow an easy syntax that requires us to add spaces between tokens.

For example, this example will work fine:

[ -e file ]

But each element of the following list will not:

[ -e file]
[-e file]
[-efile]

Let’s see what happens if we don’t separate each token with spaces:

$ [ 1=1] && echo True
bash: [: missing `]'

Now let’s try with the token [[:

$ [[ 1=1]] && echo True 
bash: conditional binary operator expected 
bash: syntax error near `True'

So, we must pay a little attention when we construct our sentences.

6. Conclusion

In this article, we’ve covered conditional expression and how we can build expressions using different constructs and keywords.