ShellCommando - evanx/vellum GitHub Wiki
This article is the first installment of the Bin Bash series :)
Introduction
We write lots of cron scripts to perform routine maintenance tasks on our systems, so...
We might want a script to perform various tasks in succession, but also be able to invoke those individually in an adhoc fashion e.g. to test them.
Also one often wishes to cut and paste commonly used commands into a shell script for later reference, and reuse.
So we introduce an approach for dynamically invoking parameterized functions in a bash script.
Template
The following is a basic template, with one custom command for illustration, namely command1_dfh
.
#!/bin/bash
set -o nounset
command1_dfh() { # volume
volume=$1
df -h | grep $1
}
command0_help() {
echo 'The following commands with required arguments, are available:'
cat $0 | grep '^command[0-9]_.*#' | sed 's/^command\([0-9]\)_\(.*\)() { #\(.*\)/\2:\3/' | sort
}
command0_default() {
echo "Sorry but the default invocation of this script has no functionality yet."
command0_help
}
invoke() {
if [ $# -gt 0 ]
then
command=$1
shift
else
command=default
fi
command$#_$command $@
}
invoke $@
Firstly, we always set the bash option nounset
so that if our script encounters an unset variable, this is treated as an inherently unsafe bug, and the script will abort.
We implement custom commands in the script as per the command1_dfh
example, using a specific naming convention whereby the function name is prefixed by "command" and a digit which is the number of arguments that are expected, courtesy of "$#"
in invoke()
.
If no command-line arguments are provided, we will invoke the function command0_default()
.
Help
The above implementation of command0_help()
lists all the functions at our command, where we document the expected arguments in the script itself via a comment next to the function declaration.
evanx@beethoven:~$ sh scripts/command.sh
Sorry but the default invocation of this script has no functionality yet.
The following commands are available:
dfh: volume
where command0_help()
will parse the script itself for command functions, and print any comments regarding their usage.
We might provide help on a specific command as follows.
command1_help() {
command0_help | grep $1
}
where we have "overloaded" our help
function per the number of arguments.
Let's try command1_help
i.e. with 1 argument.
evanx@beethoven:~$ sh scripts/command.sh help dfh
dfh: volume
Invocation
Consider our first custom function, using df
.
command1_dfh() { # volume
volume=$1
df -h | grep $1
}
We invoke the command1_dfh
with one argument, which is a pattern to grep the output of df
.
evanx@beethoven:~$ sh scripts/command.sh dfh home
/dev/sdb2 128G 109G 13G 90% /home
If we provide an invalid command, the script will abort as follows.
evans@beethoven:~$ sh scripts/command.sh dff home
scripts/command.sh: 25: command1_dff: not found
If we do not provide the correct number of arguments that the function is expecting, the script will abort as follows.
evanx@beethoven:~$ sh scripts/command.sh dfh
scripts/command.sh: 25: command0_dfh: not found
where the intentioned function is actually prefixed by command1
since it requires 1 argument.
However we can overload functions e.g. let's introduce command0_dfh()
with no arguments, as follows.
command0_dfh() { # with no args, executes 'df -h'
df -h
}
Our help now shows the overloaded dfh
commands as follows.
evanx@beethoven:~$ sh scripts/command.sh help dfh
dfh: volume
dfh: with no args, executes 'df -h'
One can implement a command0
as follows...
command0_dfh() {
echo "Incorrect usage! Arguments are required for this command, e.g. the volume to grep."
}
where this is just help for the usage of same command with arguments :)
Example
As an example, let's implement a new command to print yesterday's date in our preferred format.
command0_yesterday() { # print yesterday's date in YYYY-MM-DD format
date -d 'yesterday' +'%Y-%m-%d'
}
Let's check the usage.
evans@beethoven:~$ sh scripts/command.sh help yesterday
yesterday: print yesterday's date in YYYY-MM-DD format
And let's invoke it.
evans@beethoven:~$ sh scripts/command.sh yesterday
2013-04-26
Minimal
Below is a slightly different minimal template for dynamic invocation, without our command0_help
functionality.
set -u
command0_() {
echo "No default functionality implemented yet"
}
if [ $# -gt 0 ]
then
command=$1
shift
command$#_$command
else
command0_
fi
evans@beethoven:~$ sh scripts/template.sh
No default functionality implemented yet
evans@beethoven:~$ sh scripts/template.sh help
scripts/template.sh: line 15: command0_help: command not found
Conclusion
We introduce a template for a convenient menu-esque approach for dynamically invoking parameterized commands in a bash script.
The implementation will ensure that "command functions" are invoked with the correct number of arguments, by virtue of the using the number of arguments in the function name to be invoked e.g. command1
will prefix functions requiring one argument only.
Furthermore this allows us to "overload" functions, according to the number of arguments, e.g. to have a more general implementation which takes no arguments, alongside an implementation which takes a number of arguments in order to be more specific.
Sneak preview
In an upcoming article, we'll use the above approach for LSI MegaRAID commands as follows.
$ sh scripts/megaraid.sh
commands:
alarmDsply:
alarmSilence:
battery:
events:
getProp: prop
help: command
megahelp: (MegaCli64 -help)
latestEvents:
ldGetProp: ld
ldInfo:
ldInfo: ld
ldInfo_state: ld
ldSetProp: ld prop
pdGetMissing:
pdInfo:
pdInfo: slot
pdList:
pdMakeGood: slot
pdRbld_showProg: slot
pdRbld_start: slot
pdReplaceMissing: slot array row
setProp: prop
state_notify: ld
where for example the pdInfo
function with 1 argument is implemented as follows.
enclosure=14
command1_pdInfo() { # slot
/opt/MegaRAID/CmdTool2/CmdTool264 -pdInfo -physdrv [$enclosure:$1] -a0
}
where our script contains some context for our environment e.g. the enclosure number (14).
Actually, we can implement context based on the hostname, as follows.
context_beethoven() {
enclosure=14
}
context_`hostname -s`
Naturally this could be explicitly implemented using if-elif
constructs, but nevertheless the above dynamic invocation approach is quite neat.
$ ./megaraid.sh pdInfo 5 | grep Error
Media Error Count: 0
Other Error Count: 0
Such scripts provide a useful reference for such commands, if nothing else :)
Resources
See the Bin Bash page.