Bash tips for novices
Table of Contents
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.
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.