Newbie at bash scripting? Here's some advice
Tags: bash, linux, devops
Published: 2019-02-03 11:39:09

---
title: Newbie at bash scripting? Here's some advice
published: true
description: Ideas for bash newbies
cover_image: https://i.imgur.com/DYIuBUO.jpg
tags: bash, linux, devops
---


When I started using bash for scripting I couldn't wrap my head around it first. I'm relatively young and the reason behind my confusion was probably that. Coming from python at that time, it felt as if every time I wanted to do something, I needed some special glyph or incantation to make it happen. Bash is powerful, it is super permissive, and because of that, it is extremely easy to do something stupid with it.

#####Just to be clear, this blogpost is more about good conventions in bash, not <a href="https://devhints.io/bash" target="_blank">it's syntax</a> or how to <a href="https://zwischenzugs.com/2018/01/06/ten-things-i-wish-id-known-about-bash/" target="_blank">do basic stuff</a> with it.

##This is how you start a bash script

I wanted to talk about the set builtin, but I realized that's not how you start a bash script. This is how:
```bash
#!/usr/bin/env bash
```
This is called a *shebang*. By making this the first line, you'll make sure that you will run with the default bash, which is the first reference for it in your `$PATH` environment variable. It's a shortcut, but it's a good convention to follow. (Unless security is a concern, and you are afraid that someone will tamper with your `$PATH` environment variable.)

##Now really. This is how you start a bash script
```bash
#!/usr/bin/env bash
set -euo pipefail
```
The [set builtin](https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html) is a pretty complex and useful element in our bash toolkit. Just to make my point I'll oversimplify it a bit, but here's an explanation: **set** allows you to configure *how bash behaves* in certain key scenarios.
**Let's go through it very briefly:**

* `set -e` option will cause a bash script to exit immediately when a command fails. This is generally a vast improvement upon the default behavior where the script just ignores the failing command and continues with the next line.
* `set -u` causes the bash shell to treat unset variables as an error and exit immediately. 
* `set -o pipefail` sets the exit code of a pipeline to that of the rightmost command to exit with a non-zero status, or to zero if all commands of the pipeline exit successfully. Actually `set -e` itself is kind of set in vain without this option, as it will not exit the script if a pipeline of commands have a rouge failure somewhere in between.

##Ok I did the set thing, and it's killing me. How to deal with it?
Pretty sure you will end up having trouble because of `set -euo pipefail` one way or another. For example, you may decide to provide some parameter optionally, and then `set -u` will kick your ass. That's when you do:
```bash
if [[ -z "${oxygen:-}" ]]; then
    echo "Huston we have a problem!"
fi
```
This conditional checks if the given parameter `oxygen` is unset (or an empty string). The emphasis is on the `:-` expression. Without that, due to `set -u` the script will fail because it will not be able to do the parameter expansion. Using `:-` the expression will be left unset, and we will pass for the error. By the way, this is how you add default parameters. Whatever is behind the `:-` will be used as default if your parameter is unset. Like this: `"${oxigen:-nitrogen}"`

Another usual problem is when you expect something to fail or you want to handle it explicitly. Naturally `set -eo pipefail` will exit your script, if anything dares to exit with a non zero exit status. To handle this you have *#pipepipetrue*:
```bash
echo 'Here comes the rough part'
my_fate=$("${dark_cellar}"/russian_roulette.py --load "${bullet}" || :)
echo 'Keep going, whatever'
```
The expression `|| :` will blot out your error and let your script run through.

##I can do stuff. But am I doing it right?
As we said, there are many ways to do something in bash. Luckily the online community is great, and it's easy to find the [one-liner](http://www.bashoneliners.com/) that does the trick you want. But is that a safe solution? One thing I learned: there is *always* a scenario that you did not think about.
It is good when you have a friend who is a Linux pro and catches all your mistakes. But if you don't have one, you can still have a good *static analyzer*!
####Meet [shellcheck](https://www.shellcheck.net/).
If you use bash, use shellcheck too. It warns you about errors and common bad practices which could result in unseen consequences. It's easy to set up, with apt just like that:
```bash
sudo apt update
sudo apt-get install shellcheck
```
You can simply run it for any of your scripts as `shellcheck myscript.sh` and it will set you straight!

##Permissive doesn't mean that you shouldn't modularise
If you end up doing DevOps stuff at some company you will probably notice that every bash script is basically a list of operations in order. Not like a python script where the community practice pushes you to create functions, and make sure [Single Responsibility Principle](https://en.wikipedia.org/wiki/Single_responsibility_principle) is kept. But who said that you can't do the same in bash? See:
```bash
function get_to_the_choppa() {
    local mate=$1
    echo "${mate}! Get to the choppa!"
}

get_to_the_choppa "Jack"
```
As usually, there are some scenarios when bash screws you over if you don't know how it behaves, so let's see the most common ones:
* use `local` variables. Everything declared in a function is available beyond the scope of the function. To avoid this you must explicitly declare your variables to be local.
* choose a **convention** and be consistent. You can declare a function in several ways. Make sure to stick with one.
```bash
function take_pills() {
    echo "Get better"
}
function dink_tea {
    echo "Get better"
}
rest_in_bed() {
    echo "Get better"
}
```
* you can return with `echo`. Bash has a `return` statement but you can only specify the functions exit status with it. To *acquire* values from your functions the solution could be to set global variables in them without using the local keyword, but better to be explicit! You can just capture what your function echoes.
```bash
function interrogate() {
    echo "It was Hank!"
}
who_to_blame=$(interrogate)
```

##Get to know Linux
If you are a newbie at bash, it's pretty sure Linux is new to you as well. I'm not the best person to explain all the basics about Linux, but I can share an **amazing and free resource**, that I used as well when I started.
If there are still some topics that puzzle you, you can read all about them at [linuxjourney](https://linuxjourney.com/). It has simple and easy-to-grasp descriptions about every basic and advanced concept, and it has a great clean UI that pleases the eye.
This page is so simple and amazing, that it could teach a monkey to work with Linux. **Deffinietly check it out!**

##Last, two advice
Take two advice from me, and apply them when something doesn't work as expected. Back then I found these steps so important, that *I wrote them on post-its and stuck them on the bottom of my monitor*. Here they are:
#### Step One: *Read that code again.*
Something is not working right? Don't panic. I'll tell you what will happen: Some time will pass and you will find the mistake you made. Probably on the tip of your nose. You may think that you did everything fine but some components that you use are conflicting? Go to step two.
#### Step Two: *It's almost always you.*
You may think that some components are not compatible. Or maybe the version that you use does not support some use-cases. But usually no. Usually it was you who messed it up. Go back to step One.