Bash tips for novices

These are some small pointers I on scripting I’ve notices working for me. Most of these are useful for people who either do not write scripts very often or are fairly new to shell scripting.

Some of these tips may seem simple and/or obvious but have cause many hours of happy debugging for me and others in the past.

In general

Write code for maintainability and portability. This for me means:

Avoid smartypants-ness

Sometimes “dumber” is better. You maybe a shell scripting god but you will go on holiday or even to better ventures. Your elegantly space one-liner will be butchered by that other guy. If the code is hard to understand the carnage will be memorable.

Comment

Whether it is a four line wrapper or an extensive application, comments are what keeps code maintainable.

Know when to quit

Bash can be coaxed to do a lot of things but at a certain point other languages provide more and better support. Once you notice you are using temporary files, chunks of inline awk that would make Kernighan cringe and more pipes than an oil refinery switching to python or perl may prove useful.

Style

Pick a style and stick with it. Whether camel-casing your functions and/or variables or using uppercase for global variables and lowercase for local variables. Pick one. Its frustrating to see one type of style in the beginning of a script and another at the end.

The same goes for structuring your scripts. Having a predictable structure makes it easier to maintain. My preference is:

  • header with name, description, options, author, changelog, etc.
  • environment variables (PATH, LC_ALL, etc.)
  • global variables
  • functions
  • main script

Indents; two or four spaces per indent. No tabs. Tabs is about as sensible as letting your mother do your clothes shopping. Sounds like a cost saving, time saving plan at first. But instead of trotting around in fashionable threads you hobble around in a pony-print pink sweater getting laughed at by eight year old girls. Do not use tabs.

Line length; try to keep it at 80 character. Cut longer lines with backslashes.

Versions

Bash is not always bash. The code that runs fine on one platform and platform version will not automatically run on another.

This code snippet for example will work on RedHat 6 (bash v4.1.2) but will fail on OSX El Capitan (bash v3.2.57).

for i in {0..10..2};
do
    echo "$i"
done

Usually its best to keep the code compatible with whatever oldest version you are running. It might not be sexy but you do not always have a choice in shell versions.

Environment

Make sure variables like PATH, LANG, LC_ALL and LD_LIBRARY_PATH are set correctly if applicable. At the very least make sure that the directories you need are in the PATH setting. Correctly setting the PATH variable enables you to avoid using absolute paths in commands.

Variables like LC_ALL can impact the format of your output. The sar tool is a great tool for analyzing a systems performance. However depending on the language set in the environment it will change its output format.

So native English output looks like this:

# LC_ALL=en_US sar -r 1 5
Linux 2.6.32-573.3.1.el6.i686 (home.biomechs.fake)      11/21/2015      _i686_(2 CPU)

12:01:10 AM kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit
12:01:11 AM     79020   1850084     95.90    407268   1120596    646788     10.56
12:01:12 AM     79020   1850084     95.90    407268   1120596    646788     10.56
12:01:13 AM     79020   1850084     95.90    407268   1120596    646788     10.56
12:01:14 AM     79020   1850084     95.90    407268   1120596    646788     10.56
12:01:15 AM     79020   1850084     95.90    407268   1120596    646788     10.56
Average:        79020   1850084     95.90    407268   1120596    646788     10.56

But generic C will look like this:

# LC_ALL=C sar -r 1 5
Linux 2.6.32-573.3.1.el6.i686 (home.biomechs.fake)      11/21/15        _i686_(2 CPU)

00:02:03    kbmemfree kbmemused  %memused kbbuffers  kbcached  kbcommit   %commit
00:02:04        79012   1850092     95.90    407268   1120596    646784     10.56
00:02:05        79012   1850092     95.90    407268   1120596    646784     10.56
00:02:06        79012   1850092     95.90    407268   1120596    646784     10.56
00:02:07        79012   1850092     95.90    407268   1120596    646784     10.56
00:02:08        79012   1850092     95.90    407268   1120596    646784     10.56
Average:        79012   1850092     95.90    407268   1120596    646784     10.56

Tip

When in doubt set LC_ALL to C.

Commands

Do not use absolute paths unless forced. Seriously, a good way to kneecap portability is to add absolute paths. Between Linux and UNIX platforms and versions the differences in locations of the same commands are staggering.

Variables

Using abstract variables is not recommended. In small pieces its not an issue but once the amount of variables and lines stack up it becomes hard to read.

Here using i as the counter is not a problem, the loop is small and the scope is limited.

for i in {0..10}
do
   echo $i
done

This however is a bit of a problem. Its unclear what is is the purpose of the variables and some assignment look like the author repeatedly bangs his butt cheeks on the keyboard.

typeset -i y E
let y=1
X=$(uname -s)
Z=$(uname -n | cut -d\. -f 1)
Q="/mimir/vol0/adm/platforms"
if [ $X == "Linux" ]
then
  Y[${#Y[*]}]="Linux"
  if [ -e /etc/redhat-release ]
  then
    Y[${#Y[*]}]="RedHat"
    if grep -qi centos /etc/redhat-release
    then
      Y[${#Y[*]}]="CentOS"
    fi
  fi
  [[ -e /etc/mandrake-release ]] && Y[${#Y[*]}]="Mandrake"
  [[ -e /etc/SuSE-release ]] && Y[${#Y[*]}]="SuSE"
  [[ -e /etc/fedora-release ]] && Y[${#Y[*]}]="Fedora"
  [[ -e /etc/debian_version ]] && Y[${#Y[*]}]="Debian"
  [[ -e /etc/wrs-release ]] && Y[${#Y[*]}]="WindRiver"
  [[ -e /etc/snow-release ]] && Y[${#Y[*]}]="Snow"
else
  Y[${#Y[*]}]=$X
fi
Y[${#Y[*]}]=$Z
let E=$((${#Y[@]} - 1))
while ((y<=E)); do
  PLATFORM="${Y[$y]}"
  FILENAME="${Q}/${PLATFORM}.dat"
  let y++
  [[ -f "${FILENAME}" ]] && \
    dbimport $FILENAME || \
    echo "platform ${PLATFORM} has no data"
done

Variables can be used like this:

$APPDIR

Like this:

"$APPDIR"

Or even like this:

"${APPDIR}"

The latter has preference because it delimits the content of the variable explicitly.

Consider this:

APPDIR="/usr/contrib/foo"

if [ ! "x$APPDIRx" = "xx" ]
then
   rm -rf /$APPDIR
fi

It would seem like “$APPDIR” is being tested but “$APPDIRx” is being tested. The value of which is not “xx” but nothing. This script effectively wipes the system. This is a common nooby mistake.

Tip

use clear explicit variable names.

Tip

when in doubt use quotes and curly brackets to delimit the variables.

Functions

A function is a good way to handle repetition, but also to bring a large block of code back to more manageable chunks. The latter is underestimated. If the code block is more then 200 lines it becomes a bit of a pain to read. Once your block exceeds 1000 lines its takes forever to read and mistakes are easy.