if-blocks

Next line of code:

if [ "$program" = "ruby" ]; then
  ...
fi

We already know what the bracket syntax does. We also know we need double-quotes to expand our $program variable safely. And the if ... then syntax is likely to be readable even without Bash experience. fi is just the way to close an if statement in Bash.

So the purpose of this if check is to ensure the subsequent code only gets executed if the user typed ruby into the terminal as the program name. We'll review that subsequent code in the next chapter, but there are also some interesting things to note about this line of code.

Bash also supports elif and else in if-blocks. Let's see how they're used.

Experiment- if-blocks in Bash

I write the following script:

#!/usr/bin/env bash

arg="$1"

if [ "$arg" == "foo" ]; then
  echo 'foo';
elif [ "$arg" == "bar" ]; then
  echo 'bar';
else
  echo "Unrecognized param"
fi

We capture the first argument and store it in the variable arg. If arg equals "foo", we print "foo". If it equals "bar", we print "bar". Otherwise, we print "Unrecognized param".

When I run this in my terminal, I get:

$ ./foo foo
foo
$ ./foo bar
bar
$ ./foo baz
Unrecognized param
$ 

Nothing too surprising here.

Comparing numbers in an if-block

One thing to note is that, if we want to compare two numbers in a "greater-than", "less-than", or "equal-to" situation, you should use -gt, -lt, or -eq respectively. You should not use >, < to compare numbers with single bracket syntax.

To demonstrate what happens, I rewrite my foo script to the following:

#!/usr/bin/env bash

arg="$1"

if [ "$arg" > 1 ]; then
  echo '> 1';
elif [ "$arg" < -5 ]; then
  echo '< -5';
else
  echo "Unrecognized param"
fi

Everything works fine when I run ./foo 5, but when I run ./foo -10, I get an unexpected result:

$ ./foo 5
> 1
$ ./foo -10
> 1
$ 

That's because, according to man test, Bash is treating -10 and 5 as strings, not as numbers:

s1 = s2

True if the strings s1 and s2 are identical.

s1 != s2

True if the strings s1 and s2 are not identical.

s1 < s2

True if string s1 comes before s2 based on the binary value of their characters.

s1 > s2

True if string s1 comes after s2 based on the binary value of their characters.

To tell Bash to treat these as integers instead, mansuggests we use -gt and -lt instead:

n1 -eq n2

True if the integers n1 and n2 are algebraically equal.

n1 -ne n2

True if the integers n1 and n2 are not algebraically equal.

n1 -gt n2

True if the integer n1 is algebraically greater than the integer n2.

n1 -ge n2

True if the integer n1 is algebraically greater than or equal to the integer n2.

n1 -lt n2

True if the integer n1 is algebraically less than the integer n2.

n1 -le n2

True if the integer n1 is algebraically less than or equal to the integer n2.

Let's change our script to use -gt and -lt instead:

#!/usr/bin/env bash

arg="$1"

if [ "$arg" -gt 1 ]; then
  echo '> 1';
elif [ "$arg" -lt -5 ]; then
  echo '< -5';
else
  echo "Unrecognized param"
fi

Now, it works as expected:

$ ./foo 5
> 1
$ ./foo -10
< -5
$

Note that < and > do work as number comparators if you use the double-bracket syntax [[ instead of the single-bracket syntax [. Changing foo to this...

#!/usr/bin/env bash

arg="$1"

if [[ "$arg" > 1 ]]; then
  echo '> 1';
elif [[ "$arg" < -5 ]]; then
  echo '< -5';
else
  echo "Unrecognized param"
fi

...results in this when we run it:

$ ./foo 5
> 1
$ ./foo -10
< -5
$ 

But as we'll see below, [[ is not POSIX-compliant, so your script might be less-portable than you want it to be as a result of this change.

Equals signs- single vs. double

One thing I notice in this line of code is the use of single-equals as a comparison check. In Ruby, single-equals are used for assignments, and double-equals are used for comparisons. This doesn't appear to be the case in Bash, at least not inside the [ ... ] brackets.

I Google "double vs single equals bash", and the first result that appears is this StackOverflow post. I learn that the following are all equivalent in Bash:

[[ $x == "$y" ]]
[[ $x = "$y" ]]
[ "$x" == "$y" ]
[ "$x" = "$y" ]

Additionally, we know that [ ... ] and test are all equivalent in Bash, so we can add the following two commands to the above list:

test "$a" = "$b"
test "$a" == "$b"

Brackets- single vs. double

In the above examples from StackOverflow, I notice that some of the code uses single-brackets ([ ... ]), and some use double-brackets ([[ ... ]]). I'm curious if there's any meaningful difference between these two, so I Google "single vs double-brackets bash".

The first result I find is a StackOverflow post titled "When should I use [ vs [[ in Bash (single vs double brackets)?". The answers say that the single-bracket [ ... ] syntax is part of the POSIX standard, and is therefore more portable to other shells.

Only [ (test) is in POSIX. You won't find [[ specified in POSIX except as a reserved word that implementations may use. Bash does indeed use [[ as an enhanced test operator, and if you are writing scripts targeted specifically for bash, always use [[ because it is much more powerful than [ and safer to use as well. If, however, you are targeting POSIX sh, you need to use [, and make sure to quote arguments and not use more than 4 arguments to [.

On the other hand, while [[ ... ]] is not POSIX-compliant (it is used by Bash and a few other shells such as Zsh and Ksh, but not by all shells), it uses syntax which is considered safer and cleaner.

Single bracket is the traditional form, and is often implemented as an external command. Double bracket is a Bash (and Ksh and Zsh) extension, is not in POSIX, and has the advantage of cleaner syntax (see below) and not using a separate process.

The advice seems to be, if you're writing scripts specifically for Bash, use [[ ... ]]. But if you need a guarantee that your script will work with any POSIX-compliant shell, you should use [ ... ] instead.

What is POSIX?

The above answer made a big deal about the POSIX-compatitibility of [ vs. [[. But what even is POSIX?

If we Google "What is POSIX", one of the results is another StackOverflow post. The top-rated answer from that post says:

POSIX is a family of standards, specified by the IEEE, to clarify and make uniform the application programming interfaces (and ancillary issues, such as command line shell utilities) provided by Unix-y operating systems.

When you write your programs to rely on POSIX standards, you can be pretty sure to be able to port them easily among a large family of Unix derivatives (including Linux, but not limited to it!); if and when you use some Linux API that's not standardized as part of Posix, you will have a harder time if and when you want to port that program or library to other Unix-y systems (e.g., MacOSX) in the future.

So POSIX defines the standards that determine how users talk to computers (aka "user-level APIs") and also how one part of the computer talks to another part (aka the "system-level APIs"). Some shells (like Bash and Zsh) are POSIX-compliant, and if you write shell scripts using those languages, you can be confident that they'll run on a variety of machines. Other shells (such as Fish) are not POSIX-compliant, and you have less of a guarantee that scripts written for these shells will be be widely-portable.

Wrapping Up

Next, let's examine the for-loop which lives inside the if block.