Friday, December 18, 2015

Bash Scripting - Best Practices

1 - Readability

1.1 - Indentation

There are 3 commonly used indentation practices for Bash (I prefer the first method, however all 3 are "accepted"):
  • 2 spaces
  • 4 spaces
  • tabs (usually 8 spaces)
All examples will be shown using the first indentation method, however for reference here's a comparison between all 3.
Example:
## 2 spaces
if ...
  command
else ...
  command
fi

## 4 spaces
if ...
    command
else ...
    command
fi

## Tabs
if ...
        command
else ...
        command
fi
Indentation for if conditional statements
if [ test ] ; then
  command
elif [ test ] ; then
  command
else
  command
fi
Indentation for for statements
for a b c in $A ; do
  command
done
Indentation for while and until loops
while [ true ] ; do
  command
done
Indentation for case statements
case $VAR in
  true) command ;;
  false)
    command1
    command2
    ;;
  *)
    if [ test ] ; then
      command
    fi
    ;;
esac
Indentation for functions
_funct_do_var() {
  commands
}

1.2 - Comments

Code Comments

You should always comment your code. This will make it easier for others to understand, as well as for yourself should you need to change the script a few months (or years) later.
Make sure that the comments make sense. Do not try to save on typing as additional comments can save you (or someone else) a lot of time in the future.
Example: Commonly used comments
startTMATE() {
  ## Starts tmate handling
  # Launch tmate in a detached state
  $TMATE_BIN -S $TMT_SOCKET new-session -d

  # Blocks until the SSH connection is established
  $TMATE_BIN -S $TMT_SOCKET wait tmate-ready

  # Prints the SSH connection string
  $TMATE_BIN -S $TMT_SOCKET display -p '#{tmate_ssh}' > $LOG

  # Prints the read-only SSH connection string
  $TMATE_BIN -S $TMT_SOCKET display -p '#{tmate_ssh_ro}' >> $LOG
}
Example: In-line comments
## Sets up package manager aliases based on distro
# Settings for Ubuntu
if [[ "$DISTRO" =~ (Ubuntu|LinuxMint) ]] ; then
  alias aptdate='sudo apt-get update'         # Updates package list
  alias aptgrade='sudo apt-get upgrade'       # Updates all packages
  alias apts='apt-cache search'               # Search for package
  alias aptrm='sudo apt-get remove'           # Removes package

  aptinst() {                                # Install package
    if [ -d "${HOME}/bin/var/log" ] ; then
      LOG=${HOME}/bin/var/log/$(hostname)-package-install.log
    fi

    echo -e "$(date)\t-\tInstalling packages: $*" >> "$LOG"
    sudo apt-get install "$*"
  }
fi

Block Comment

Here Document
You can use a here document (EOF) to "trick" Bash in creating block comments.
Example:
alt text
You can read a bit on my other post here.
Bash Built-in Command
You can also use Bash's bultin :, which does nothing.
Here's the definition from Bash's man page:
: [arguments]
    No effect; the command does nothing beyond expanding arguments and
    performing any specified redirections. A zero exit code is returned.
Example:
: '
 your comments here
'

1.3 - Lines

Breaking Line of Code
When breaking long lines of code with \, indent the new lines.
Example:
[ -f "/etc/cups/cupsd.conf" ] && echo \
  "The CUPS configuration file exists" 
Using Empty Lines
You can use empty lines to keep your code clean, even inside code blocks.
Example:
# Downloads this weeks photos
if [ "$THIS_WEEKS_PHOTOS_URL" ] ; then
  cd ${UNSPLASH_DIR}
  for PHOTO_URL in $THIS_WEEKS_PHOTOS_URL ; do

    # Checks if jpg is in the URL
    if [[ $(echo $PHOTO_URL | grep -qi jpg ; echo $?) -eq 0 ]] ; then
      PHOTO_FILE_NAME=$(echo $PHOTO_URL | awk -F/ '{print $4}')

    # else, let's add it
    else
      PHOTO_FILE_NAME=$(echo $PHOTO_URL | awk -F/ '{print $4}').jpg
    fi

    # Get the photo and save as file name
    wget --progress=bar $PHOTO_URL -O $PHOTO_FILE_NAME
  done
fi

1.4 - Misc

  • Avoid the use of back-ticks ` for command substitution. Use $(...) for better readability

2 - Headers and Sections

2.1 - Script Header

Add descriptive headers to the script outlining it's name, what it does, usage and possibly version.
#!/bin/bash

################################################################################
################################################################################
# Name:          tmate.sh
# Usage:
# Description:   Runs tmate and outputs remote string to file
# Created:       2014-10-31
# Last Modified:
# Copyright 2014, Victor Mendonca - http://wazem.org
# License: Released under the terms of the GNU GPL license
################################################################################
################################################################################

2.2 - Sections

Divide your script into sections.
Put all global script variables in one section
#-------------------------------------------------------------------------------
# Sets variables
#-------------------------------------------------------------------------------
Put all functions in one section
#-------------------------------------------------------------------------------
# Functions
#-------------------------------------------------------------------------------
Put all major work in one section (where functions get called)
#-------------------------------------------------------------------------------
# Starts script
#-------------------------------------------------------------------------------

3 - Variables

3.1 - Naming

  • Give meaningful names to variables
  • When using uppercase, make sure the variable is not already being used
  • Avoid starting variables with _

3.2 - Calling

Double quote variables to prevent globbing or word splitting.

4 - Functions

Try to define your functions at the beginning of the script (see sections).

4.1 - Naming

  • Should start with lower case
  • It's good to start with _
  • Upper and lower case are also good
Example:
_lowerIt () {
  BASH_MAIN_VERSION=$(echo $BASH_VERSION | awk -F. '{print $1}')
  if [[ $BASH_MAIN_VERSION -lt 4 ]] ; then
    echo "Not supported with your version of bash"
    return
  fi

  if [ "$1" ] ; then
    echo ${1,,}
  fi
}

4.2 - Local Variables

Keep your function variables local if they are not being used outside of the function.
Example: This script will output "$2400" only once
_myFunction() {
  local extract
  extract='$2400'
  echo "$extract"
}

_myFunction
echo "$extract"

5- Code Smart

New and Deprecated Features
Get to know your version of Bash and any features it may have.
Example: Old and new arithmetic expansion
# New way for arithmetic expansion
$ echo $((4*25/2))
50

# Old way for arithmetic expansion, which will be deprecated
$ echo $[4*25/2]
50
New features
Bash 4 packs new features (like parameter expansion and output redirects). Get familiar with them as they can save you a lot of time.
Examples: Parameter expansion
$ title="bash best practices"

## Case substitution
$ echo ${title^^}
BASH BEST PRACTICES

## String substitution
$ echo ${title/best/worst}
bash worst practices
Examples: stdout and stderr output redirect
# New
ls 123 &> /dev/null

# Old
ls 123 > /dev/null 2>&1

6 - Software

You can also use software to help with your scripts.

6.1 - Syntax Highlighting

Most of today's editors include syntax highlighting, even VIM. Syntax highlighting will help you catch mistakes like a unclosed bracket or quote.

6.2 - Lint

Some editors, like Sublime Text include plugins for lint software. They can be of great help for you coding.

6.3 - Online Tools

There are also sites that can help checking your script (similar to lint), or a small set of commands.
  • explainshell.com - match command-line arguments to their help text
  • shellcheck.net - automatically detects problems in sh/bash scripts and commands

References

No comments: