The Ops Community ⚙️

Meir Gabay
Meir Gabay

Posted on • Updated on • Originally published at meirg.co.il

Writing Bash Scripts Like A Pro - Part 1 - Styling Guide

Writing Bash scripts can be challenging if you don't know the quirks and perks. In my mother tongue, we use the Yiddish word for quirks and perks; it's called "Shtickim" (plural of "Shtick"). Are you ready to learn more about Bash's "Shtickim"?

This blog post is part of a series that I'm working on to preserve the knowledge for future me that forgets stuff, to assist new colleagues, and indulge programmers like you who wish to love Bash as I do. So let's begin, shall we?

It's A Scripting Language

It's important to remember that Bash is a scripting language, which means it doesn't offer the standard functionalities that a programming language has to offer, such as:

As you already guessed, "Bash programmers" (if there is such a thing) face many challenges. The above list is merely the tip of the iceberg.

Here are great blog posts that share the same feelings as I do:

Now that we've covered the fact that I'm in love with Bash, I want to share that feeling with you; here goes.

Variables Naming Convention

Here's how I name variables in my Bash scripts

Type Scope Convention
Environment Global MY_VARIABLE
Global Global _MY_VARIABLE
Local Function my_variable

In my older Bash scripts, the names of the variables were hard to interpret. Changing to this naming convention helped me a lot to understand the scope of variables and their purpose.

Good Vibes Application

And of course, we gotta' see some practical example, so here's how I implement the above naming convention in my good_vibes.sh application.

good_vibes.sh

#!/usr/bin/env bash
# ^ This is called a Shebang
# I'll cover it in future blog posts


# Global variables are initialized by Env Vars.
# I'm setting a default value with "${VAR_NAME:-"DEFAULT_VALUE"}"
_USER_NAME="${USER_NAME:-"$USER"}"
_USER_AGE="${USER_AGE:-""}"


complement_name(){
  local name="$1"
  echo "Wow, ${name}, you have a beautiful name!"
}


complement_age(){
  local name="$1"
  local age="$2"
  if [[ "$age" -gt "30" ]]; then
    echo "Seriously ${name}? I thought you were $((age-7))"
  else
    echo "Such a weird age, are you sure it's a number?"
  fi
}


main(){
  # The only function that is not "pure"
  # This function is tightly coupled to the script
  complement_name "$_USER_NAME"
  complement_age "$_USER_NAME" "$_USER_AGE"
}


# Invokes the main function
main
Enter fullscreen mode Exit fullscreen mode

good_vibes.sh - Execution and output

export USER_NAME="Julia" USER_AGE="36" && \
bash good_vibes.sh

# Output
Wow, Julia, you have a beautiful name!
Seriously Julia? I thought you were 29
Enter fullscreen mode Exit fullscreen mode

Let's break down the good_vibes.sh application to a "set of rules" that can be implemented in your scripts.

Code block spacing

Two (2) blank rows between each block of code make the script more readable.

Indentation

I'm using two (2) spaces, though it's totally fine to use four (4) spaces for indentation. Just make sure you're not mixing between the two.

Curly braces

If it's a ${VARIABLE} concatenated with string, use curly braces as it makes it easier to read.

In case it's a "$LONELY_VARIABLE" there's no need for that, as it will help you realize faster if it's "lonely" or not.

The primary purpose for curly braces is for performing a Shell Parameter Expansion, as demonstrated in the Global variables initialization part.

Squared brackets

Using double [[ ]] squared brackets makes it easier to read conditional code blocks. However, do note that using double squared brackets is not supported in Shell sh; instead, you should use single brackets [ ].

To demonstrate the readability, here's a "complex" conditional code block:

if [[ "$USER_NAME" = "Julia" || "$USER_NAME" = "Willy" ]] \
   && [[ "$USER_AGE" -gt "30" ]]; then
  echo "Easy to read right?"
fi

# Mind that `||` is replaced with `-o`, see https://acloudguru.com/blog/engineering/conditions-in-bash-scripting-if-statements
# Thank you William Pursell
if [ "$USER_NAME" = "Julia" -o "$USER_NAME" = "Willy" ] \
   && [ "$USER_AGE" -gt "30" ]; then
  echo "No idea why but I feel lost with single brackets."
fi
Enter fullscreen mode Exit fullscreen mode

In case you didn't notice, you've just learned that || stands for OR and && stands for AND. And the short -gt expression means greater than when using numbers. Finally, the \ character allows breaking rows in favor of making the code more readable.

Shtick: Using \ with an extra space \ <- extra space can lead to weird errors. Make sure there are no trailing spaces after \.

I assume that using [[ ]] feels more intuitive since most conditional commands are doubled && ||.

Variable initialization

Global variables are initialized with Environment Variables and are set with default values in case of empty Environment variables.

As mentioned in the good_vibes.sh comments, I'm setting a default value with

"${VAR_NAME:-"DEFAULT_VALUE"}"
Enter fullscreen mode Exit fullscreen mode

In the above snippet, the text DEFAULT_VALUE is hardcoded, and it's possible to replace it with a variable. For example

_USER_NAME="${USER_NAME:-"$USER"}"
Enter fullscreen mode Exit fullscreen mode

Functions and local function variables

Functions names and local function variables names are snake_cased. You might want to change functions names to lowerCamelCase, and of course, it's your call.

Coupling a function to the script is a common mistake, though I do sin from time to time, and you'll see Global/Environment variables in my functions, but that happens when I know that "this piece of code won't change a lot".

Oh, and make sure you don't use $1 or any other argument directly; always use local var_name="$1".

_USER_NAME="${USER_NAME:-"$USER"}"

# Bad - coupled
coupled_username(){
  echo "_USER_NAME = ${_USER_NAME}"
}

# Good - decoupled
decoupled_username(){
  local name="$1"
  echo "name = ${name}" 
}

# Usage
coupled_username  
decoupled_username "$_USER_NAME"
Enter fullscreen mode Exit fullscreen mode

Functional Programming

This topic relates to Functions and local function variables, where functions are as "pure" as possible. As you can see in good_vibes.sh, almost everything is wrapped in a function, except for Initializing Global variables.

I don't see the point of writing the init_vars function, whose purpose is to deal with Global variables. However, I do find myself adding a validate_vars function from time to time, which goes over the Global variables and validates their values. I'm sure there's room for debate here, so feel free to comment with your thoughts.

Final Words

The "Good Vibes Application" mostly covered how to write a readable Bash script following the Functional Programming paradigm.

If you feel that there's a need to change how you name variables and functions, go for it! As long as it's easy to understand your code, you're on the right track.

The next blog posts in this series will cover the following topics:

  • Error handling
  • Retrieving JSON data from an HTTP endpoint
  • Background jobs and watching file for changes with fswatch
  • Git Repository structure - adding Bash scripts to existing repositories or creating a new repository with a Bash CLI application
  • Publishing a Bash CLI as a Docker image

And more, and more ... I'm just going to spit it all out to blog posts. Feel free to comment with questions or suggestions for my next blog posts.

Top comments (0)