assert_not magic?

Ryan Palo
        def test_code_isnt_magic
          code = [:ruby, :python, :javascript, :html, :css, :bash]
          assert_not code.any? { |language| language.magic? }
        end

        # A programming blog by Ryan Palo
        
Scroll down...

Bash If Statements: Beginner to Advanced

Jan 16, 2019 Buy me a coffee Buy me a coffee

Cover photo by Christy Mills on Unsplash.

Bash is an interesting language. It’s designed primarily to be used interactively on the command line, essentially in a REPL (Read-Evaluate-Print-Loop), working on one user command at a time. It’s designed to handle plain text powerfully and succinctly. This has the side-effect of causing it to be a little more difficult to do things that aren’t interactive or text-based, like arithmetic, control flow, and manipulating variables.

In this article, we’ll go through one of those slightly confusing topics: if statements. We’ll dive into how they work and how we can use those mechanics in really neat ways.

Note: This article is specifically about Bash, although most things will translate to Zsh and other shells. Some of this isn’t POSIX compliant. That’s a thing some people have Opinions about. I won’t address that at all in this article. If you’re thinking about being pedantic about it, you can just sh. 🤫

The Basics

An if-statement in Bash works pretty similarly to most other languages. It follows the basic form:

if [[ "$some_variable" == "good input" ]]; then
  echo "You got the right input."
elif [[ "$some_variable" == "ok input" ]]; then
  echo "Close enough"
else
  echo "No way Jose."
fi

The added syntax of the keyword then, along with the slightly strange closing keyword of fi make it seem a little more exotic than other languages1, but the fundamentals are the same:

If something is true, then do this thing. Otherwise check these other conditions in order and do the same thing. If none of them work out, then do the last thing.

You can leave out one or all of the elif branches, and you can even leave off the else branch if you don’t need it!

The boolean operators ! (not), && (and), and || (or) can be used to combine booleans, just like other languages.

if [[ "$name" == "Ryan" ]] && ! [[ "$time" -lt 2000 ]]; then
  echo "Sleeping"
elif [[ "$day" == "New Year's Eve" ]] || [[ "$coffee_intake" -gt 9000 ]]; then
  echo "Maybe awake"
else
  echo "Probably sleeping"
fi

But here’s the confusing thing. Sometimes you’ll see these with double square brackets like I showed above. Or sometimes they’ll be single square brackets.

if [ "$age" -gt 30 ]; then
  echo "What an oldy."
fi

Or sometimes they’ll be round!

if (( age > 30 )); then
  echo "Hey, 30 is the new 20, right?"
fi

And sometimes they won’t be there at all!

if is_upper "$1"; then
  echo "Stop shouting at me."
fi

How do you know which ones you’re supposed to use? When do you use each version? Why won’t certain ones work at certain times?

What’s Really Happening

Here are the magic words that drive everything about how if works in Bash: exit codes. What’s really happening here is the pattern:

if ANY_COMMAND_YOU_WANT_AT_ALL; then
  # ... stuff to do
fi

That’s right: the stuff immediately after the if can be any command in the whole wide world, as long as it provides an exit code, which is pretty much always. If that command returns with an exit code of 0, which is the Bash exit code for success, then the code inside the then branch gets run. Otherwise Bash moves on to the next branch and tries again.

But wait, does that mean–

Yep. “[” is a command. It’s actually syntactic sugar for the built-in command test which checks and compares its arguments. The “]” is actually an argument to the [ command that tells it to stop checking for arguments!

if [ "$price" -lt 10 ]; then
  echo "What a deal!"
fi

In this example, the [ command takes arguments "$price" (which gets substituted right away with the value of the variable), -lt, 10, and ]. Now that you know that -lt, -gt, and similar numerical comparisons are actually arguments, doesn’t the strange syntax make a little more sense? They kind of look like options! That’s why > and < get weird inside single square brackets – Bash actually thinks you’re trying to do an input or output redirect inside a command!

What About the Double Square Brackets?

Strangely enough, the [[ double square brackets ]] and (( double parens )) are not exactly commands. They’re actually Bash language keywords, which is what makes them behave a little more predictably. That being said, they still return an exit code depending on their contents. The [[ double square brackets ]] work essentially the same as [ single square brackets ], albeit with some more superpowers like more powerful regex support.

If you want to find out more about the different bracket punctuation in Bash and what they do, check out this reference I put together.

What About the Double Parentheses?

The (( double parentheses )) are actually a construct that allow arithmetic inside Bash. You don’t even need to use them with an if statement. I use this a lot to quickly increment counters and update numeric variables in-place.

count=0
(( count++ ))
echo "$count"
# => 1
(( count += 4 ))
echo "$count"
# => 5

What you’re not seeing is that the arithmetic parens are actually returning an exit code each time they’re run. If the results inside are zero, it returns an exit code of 1. (Essentially, zero is “falsey.”) If it’s any other number, it’ll be “truthy” and the exit code will be 0. Here’s a weird example:

if (( -57 + 30 + 27 )); then
  echo "First one"
elif (( 2 + 2 )); then
  echo "Second one"
else
  echo "Third one"
fi
# => "Second one"

Luckily for us, the greater and less-than symbols work just fine inside arithmetic parens. If the comparison is true, the result will be a 1. Otherwise, it’ll be a zero.

if (( 5 > 3 )); then
  echo "Numbers make sense."
elif (( 3 <= 2 )); then
  echo "3 is less than or equal to 2. wat."
else
  echo "Hwwaaa"
fi
# => "Numbers make sense"

A weird but fun offshoot of that functionality is this:

echo $(( (5 > 3) + (0 == 0) ))
# => 2
# Each comparison is true, so we're effectively echoing
# 1 + 1.  Fun, right?

Using Commands Instead of Brackets

Much of the time, you’ll be using some kind of brackets with your if statements. However, since commands and their exit codes can be used, the entire power of the command line stands behind your ifs.

Let’s say we only wanted to do something only if a line was found in a file. What command would you normally reach for to search files for text? grep!

echo "Hello, welcome to bean house." >> dialogue.txt
echo "Would you like some coffee?" >> dialogue.txt

if grep -q coffee dialogue.txt; then
  echo "Found coffee, boss."
else
  echo "No coffee."
fi
# => "Found coffee, boss."

If grep finds what it’s looking for, it exits with exit code 0 for success. If it doesn’t, it exits with exit code 1 for failure. We use that to drive our if statement! The -q option to grep stands for --quiet, and it keeps grep from outputting any lines it finds. If we take it off, our output will look like this:

Would you like some coffee?
Found coffee, boss.

Do you have other commands that you use this way? Let me know! I had a hard time thinking of examples, and I’m sure there’s some really powerful use cases out there.

Using Your Own Functions

The best part about this whole thing is that you can write your own functions! This helps you encapsulate your logic into something with a higher-level name that makes your scripts more readable and clarifies your intent. And if there’s somewhere where we could use some extra clarity, it’s in the “throwaway” script that Jerry wrote last year that we’re still using in the build pipeline.

I need to write a whole ‘nother post about writing functions in Bash, because they’re great and I love them. For now, the Spark Notes version is that they work just like little mini-scripts of their own. Any arguments you pass them can be accessed positionally by the special variables, $1, $2, $3, etc. The number of arguments is in the variable $#.

function is_number {
  if [[ "$1" =~ ^[[:digit:]]+$ ]]; then
    return 0
  else
    return 1
  fi
}

Notice that we use return here instead of exit. They do the same thing, except that exit will kill the whole script instead of just finishing the function. This function can be used like this:

function is_number {
  if [[ "$1" =~ ^[[:digit:]]+$ ]]; then
    return 0
  else
    return 1
  fi
}

age="$1"

if ! is_number "$age"; then
  echo "Usage: dog_age NUMBER"
  exit 1
fi

See how the programmer’s intent and logic becomes way more clear without the implementation details and bulky regexes getting in the way? Bash functions are Good Things.

In fact, if a function doesn’t explicitly return a value, the return value of the last command in the function is used implicitly, so you can shorten your function to:

function is_number {
  [[ "$1" =~ ^[[:digit:]]+$ ]]
}

If the regex works out, the return code of the double square brackets is 0, and thus the function returns 0. If not, everything returns 1. This is a really great way to name regexes.

if is Good

There you go! Go forth, cleaning up your Bash scripts with your newfound powers of sane, idiomatic branching. And share your use-cases with me! I love seeing other people’s neat Bash-isms.

  1. Also, it’s fun to read in your head. FI! 



Like my stuff? Have questions or feedback for me? Want to mentor me or get my help with something? Get in touch! To stay updated, subscribe via RSS