Bash Hack Var Dump Galore

Posted on August 22, 2013

0


CC-by-SA

Some things I dread a lot in software development. One of them is variable/class field naming conventions. ${DEITY:-FSM} only knows too well the horrors that lie beyond the land of Hungarian notation. It is the type of thing one would never expect to find in some places. Like, in shell scripts, for instance.

And yet, that’s one place where I’ve recently found they make development easier. Bear with me for a moment as I build my case (or go watch the Kardashians or something.)

Prelim

See, imagine for an instance you are debugging a C/C++ program using gdb. On demand you can dump the stack and see all the stuff you have declared and set at that point. In languages running on sophisticated run-times (Java come to mind) that is even easier.

But how about shell scripts? People rarely consider shell scripts the stuff where software engineering tuning is (or should take place). But then, consider this: How many hours have you spent debugging a Baal-blasted bug on a script? See, shell scripts are rarely that truly trivial.

They might be throwaway, one-time-job programs. But that does not make them trivial. Be them Bash shells or PowerShell scripts or shells written in a higher language like Python (as I wrote several months ago), They are, after all, the glue that holds things together, the stuff that typically helps you finish your actual deliverable. They are the humble, typically underestimated enabler of many a non-functional requirement.

So, how do you go about to write a shell script, however trivial it might be, in a manner that helps you debug the damned thing quickly and effectively?

True, there are tools out there like bashdb. Fine tool, but a bit clunky.

You can also initiate a trace of everything executed by the shell. Consider the sample script below used for check memory stats (hint: it starts the shell with tracing on via the ‘-x’ argument):

#!/bin/bash -x
# you can also achieve the same by using "set -x"
MEMTOTAL=`cat /proc/meminfo | grep MemTotal: | awk '{print $2}'`
MEMFREE=`cat /proc/meminfo | grep MemFree: | awk '{print $2}'`
LOWTOTAL=`cat /proc/meminfo | grep LowTotal: | awk '{print $2}'`
LOWFREE=`cat /proc/meminfo | grep LowFree: | awk '{print $2}'`
SWAPTOTAL=`cat /proc/meminfo | grep SwapTotal: | awk '{print $2}'`
SWAPFREE=`cat /proc/meminfo | grep SwapFree: | awk '{print $2}'`

When executed, the script produces the output below:

$ ./check.sh
++ cat /proc/meminfo
++ grep MemTotal:
++ awk '{print $2}'
+ MEMTOTAL=2554352
++ cat /proc/meminfo
++ grep MemFree:
++ awk '{print $2}'
+ MEMFREE=1166252
++ cat /proc/meminfo
++ grep LowTotal:
++ awk '{print $2}'
+ LOWTOTAL=2554352
++ cat /proc/meminfo
++ grep LowFree:
++ awk '{print $2}'
+ LOWFREE=1166228
++ cat /proc/meminfo
++ grep SwapTotal:
++ awk '{print $2}'
+ SWAPTOTAL=688128
++ cat /proc/meminfo
++ grep SwapFree:
++ awk '{print $2}'
+ SWAPFREE=651168

Fine for trivial scripts. Not so for real-world ones. So it would seem that we are in a pickle, but in reality, we are not.

Naming Conventions To The Rescue

The solution just dawned on me, and I just have to blog about it. I’ve been writing shell scripts for 18 years, and I never thought about this one until a few hours ago. Simply use a naming convention for your shell scripts and use the set command to spit them out to the console while filtering out every other variable already in your environment.

Consider the script previously shown now modified to use :

#!/bin/bash
function dump_local_vars
{
	(set -o posix; set | grep 'lv__')
}

lv__MEMTOTAL=`cat /proc/meminfo | grep MemTotal: | awk '{print $2}'`
lv__MEMFREE=`cat /proc/meminfo | grep MemFree: | awk '{print $2}'`
lv__LOWTOTAL=`cat /proc/meminfo | grep LowTotal: | awk '{print $2}'`
lv__LOWFREE=`cat /proc/meminfo | grep LowFree: | awk '{print $2}'`
lv__SWAPTOTAL=`cat /proc/meminfo | grep SwapTotal: | awk '{print $2}'`
lv__SWAPFREE=`cat /proc/meminfo | grep SwapFree: | awk '{print $2}'`

dump_local_vars

When executed, the script will produce the following, more succinct output:

lv__LOWFREE=1178940
lv__LOWTOTAL=2554352
lv__MEMFREE=1178940
lv__MEMTOTAL=2554352
lv__SWAPFREE=651592
lv__SWAPTOTAL=688128

So, How Does It Work?

In this example, I choose a prefix (lv__) I know will almost surely never be among your environment variables. We also switch temporarily to POSIX mode so that the set command displays variables without displaying function definitions (see GNU Bash POSIX Mode, item 36). And while the script is temporarily in POSIX mode, we dump the environment and we grep for our “local” script variables…

… and voila.

Sweet, Low-Hanging-Fruit ROI? You Bet!

I’m sure people might thing this is an over complication for a shell script. And that would be a natural response…

… but in all these years, I’ve always found that putting a little bit of thought in defensive programming and adding some diagnostics capabilities will, on average, save me a lot of hours and pain.

Simple never meant simpleton, and The KISS Principle never meant “do-it-so-half-assed-that-it-makes-debugging-impossible”.

Nothing is trivial if a bug drags you down at the 11th hour because you did not have the foresight to make your stuff “debug-able”.

Easy to use. Easy to implement. Easy to understand. Unquestionable ROI.

What’s not to like??!!!!(10+1)0xB

CC-by-SA

Advertisements