Minimal safe Bash script template


Bash scripts. Nearly anybody desires to jot down one at some point soon. Nearly no one says “yeah, I admire writing them”. And that’s why nearly all people is inserting low attention whereas writing them.

I received’t are attempting to form you a Bash professional (since I’m no longer a one either), nonetheless I will camouflage you a minimal template that could form your scripts safer. You don’t should thank me, your future self will thanks.

Why scripting in Bash?

Basically the most productive summary of Bash scripting looked right now on my Twitter feed:

The opposite of “or no longer it’s admire utilizing a motorcycle” is “or no longer it’s admire programming in bash”.

A phrase which means that regardless of how usually you manufacture something, you’ve to re-be taught it every single time.

— Jake Wharton (@JakeWharton) December 2, 2020

However Bash has something in basic with but one more broadly beloved language. Proper admire JavaScript, it received’t lumber away easily. While we are able to hope that Bash received’t change into the necessary language for literally every little thing, it’s repeatedly someplace shut to.

Bash inherited the shell throne and could possibly fair also be realized on nearly every Linux, including Docker photographs. And right here’s the atmosphere by which many of the backend runs. So if or no longer it’s fundamental to script the server utility startup, a CI/CD step, or integration test urge, Bash is there for you.

To connect few commands together, lumber output from one to but one more, and honorable starting up some executable, Bash is the very top and most native solution. While it makes ultimate sense to jot down bigger, more sophisticated scripts in other languages, it’s seemingly you’ll possibly possibly’t ask to private Python, Ruby, fish, or whatever but one more interpreter you insist is the final note, available in every single keep. And also you possibly ought to still insist twice and then over as soon as more earlier than adding it to a few prod server, Docker picture, or CI atmosphere.

But Bash is far from ultimate. The syntax is a nightmare. Error facing is sophisticated. There are landmines in every single keep. And we now should take care of it.

Bash script template

With out extra ado, right here it’s miles.

#!/usr/bin/env bash

plot -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

utilization() {
  cat &2 -e "${1-}"
}

die() {
  local msg=$1
  local code=${2-1} # default exit status 1
  msg "$msg"
  exit "$code"
}

parse_params() {
  # default values of variables set from params
  flag=0
  param=''

  while :; do
    case "${1-}" in
    -h | --help) usage ;;
    -v | --verbose) set -x ;;
    --no-color) NO_COLOR=1 ;;
    -f | --flag) flag=1 ;; # example flag
    -p | --param) # example named parameter
      param="${2-}"
      shift
      ;;
    -?*) die "Unknown option: $1" ;;
    *) break ;;
    esac
    shift
  done

  args=("$@")

  # check required params and arguments
  [[ -z "${param-}" ]] && die "Missing required parameter: param"
  [[ ${#args[@]} -eq 0 ]] && die "Missing script arguments"

  return 0
}

parse_params "$@"
setup_colors

# script logic here

msg "${RED}Read parameters:${NOFORMAT}"
msg "- flag: ${flag}"
msg "- param: ${param}"
msg "- arguments: ${args[*]-}"

The idea was to not make it too long. I don’t want to scroll 500 lines to the script logic. At the same time, I want some strong foundations for any script. But Bash is not making this easy, lacking any form of dependencies management.

One solution would be to have a separate script with all the boilerplate and utility functions and execute it at the beginning. The downside would be to have to always attach this second file everywhere, losing the “simple Bash script” idea along the way. So I decided to put in the template only what I consider to be a minimum to keep it possible short.

Now let’s look at it in more detail.

Choose Bash

#!/usr/bin/env bash

Script traditionally starts with a shebang. For the best compatibility, it references /usr/bin/env, not the /bin/bash directly. Although, if you read comments in the linked StackOverflow question, even this can fail sometimes.

Fail fast

set -Eeuo pipefail

The set command changes script execution options. For example, normally Bash does not care if some command failed, returning a non-zero exit status code. It just happily jumps to the next one. Now consider this little script:

#!/usr/bin/env bash
cp important_file ./backups/
rm important_file

What will happen, if the backups directory does not exist? Exactly, you will get an error message in the console, but before you will be able to react, the file will be already removed by the second command.

For details on what options exactly set -Eeuo pipefail changes and how they will protect you, I refer you to the article I have in my bookmarks for a few years now.

Although you should know that there are some arguments against setting those options.

Get the location

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)

This line does its most productive to define the script’s location list, and then we cd to it. Why?

Usually our scripts are working on paths relative to the script location, copying files and executing commands, assuming the script list can be a working list. And it’s miles, as prolonged as we attain the script from its list.

However if, let’s yelp, our CI config executes script admire this:

/opt/ci/project/script.sh

then our script is working no longer in project dir, nonetheless some fully diversified workdir of our CI instrument. We can repair it, by going to the list earlier than executing the script:

cd /opt/ci/project && ./script.sh

However it absolutely’s necessary nicer to resolve this on the script aspect. So, if the script reads some file or executes but one more program from the identical list, call it admire this:

cat "$script_dir/my_file"

At the identical time, the script does no longer trade the workdir location. If the script is accomplished from every other list and the user supplies a relative course to a few file, we are able to still be in a position to read it.

Strive and tidy up

trap cleanup SIGINT SIGTERM ERR EXIT

cleanup() {
  trap - SIGINT SIGTERM ERR EXIT
  # script cleanup right here
}

Think the trap admire of a at closing block for the script. At the close of the script – identical earlier, brought on by an error or an external signal – the cleanup() feature shall be accomplished. That is a plot the keep it’s seemingly you’ll possibly possibly, as an illustration, are attempting to eradicate all short files created by the script.

Proper set in mind that the cleanup() could possibly even fair also be known as no longer easiest at the close nonetheless as neatly having the script done any section of the work. Now no longer essentially the total sources you are trying to cleanup will exist.

Expose important relief

utilization() {
  cat 

Having the usage() relatively close to the top of the script, it will act in two ways:

  • to display help for someone who does not know all the options and does not want to go over the whole script to discover them,
  • as a minimal documentation when someone modifies the script (for example you, 2 weeks later, not even remembering writing it in the first place).

I don’t argue to document every function here. But a short, nice script usage message is a required minimum.

Print nice messages

setup_colors() {
  if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" !="dumb" ]]; then
    NOFORMAT='33[0m' RED='33[0;31m' GREEN='33[0;32m' ORANGE='33[0;33m' BLUE='33[0;34m' PURPLE='33[0;35m' CYAN='33[0;36m' YELLOW='33[1;33m'
  else
    NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
  fi
}

msg() {
  echo>&2 -e "${1-}"
}

In the starting up, take away the setup_colors() feature if you happen to don’t should use colors in text anyway. I support it because I do know I would use colors more usually if I wouldn’t should google codes for them every time.

Secondly, those colors are intended to be frail with the msg() feature easiest, no longer with the echo mumble.

The msg() feature is intended to be frail to print every little thing that is no longer any longer a script output. This entails all logs and messages, no longer easiest the errors. Citing the wide 12 Component CLI Apps article:

In transient: stdout is for output, stderr is for messaging.

Jeff Dickey, who knows slightly about constructing CLI apps

That’s why most continuously you shouldn’t use colors for stdout anyway.

Messages printed with msg() are sent to stderr trudge and supplies a increase to particular sequences, admire colors. And colors are disabled anyway if the stderr output is no longer any longer an interactive terminal or one in every of the fashioned parameters is handed.

Usage:

msg "That is a ${RED}fundamental${NOFORMAT} message, nonetheless no longer a script output rate!"

To test the procedure in which it behaves when the stderr is no longer any longer an interactive terminal, add a line admire above to the script. Then attain it redirecting stderr to stdout and piping it to cat. Pipe operation makes the output now no longer being sent without lengthen to the terminal, nonetheless to the next mumble, so the colors ought to still be disabled now.

$ ./test.sh 2>&1 | cat
Thi

Read More

Recent Content