How to add testing capability to a programs
[By Jari Aalto] If you are developing a longer program, the possibility to test (what would it do) before actual use can help to spot problems before they happen in real. Here we define function Run() that is used to proxy all commands. If the TEST mode is on, the commands are not executed for real but only printed to the screen for review. The test mode is activated with program's command line option -t which is read read via Bash's built-in getopt.
The heart of the demonstration is function 'Demo()' where we see how the calls make use of testing. Pay attention to how the quotes are used when command contains any shell meta characters. Notice also how you need the 'Run' call also inside subshell calls. The represetation of subshell calls is limited under test mode as you can see from the last output.
You can add similar testing approach to your programs by: 1) copying the Run() 2) utilizing variable TEST 3) modifying all shell command calls to go through Run(). In practice it is very difficult to completely put shell program under pure testing mode, because programs may use very complex shell structures and depend on outputs that are generated by previous commands. Still, the possibility to improve testing capabilities from nothing gives a better chance to be able to review the program execution before anything is done for real.
The Run function as presented here doesn't work safely. For example,
griffon:/tmp$ Run touch "foo bar" griffon:/tmp$ ls -ld foo* bar* -rw-r--r-- 1 greg greg 0 2009-08-29 12:19 bar -rw-r--r-- 1 greg greg 0 2009-08-29 12:19 foo
It needs to be rewritten. I'd suggest rewriting the entire set of arguments with something like:
local cmd printf -v cmd "%q " "$@"
but that treats | as data, rather than a pipeline operator. But you're the one who wanted to play with eval, so you get to fix it! - GreyCat
The output:
$ bash test-demo.sh -t test-demo.sh -- Demonstrate how testing feature can be implemented in program # DEMO: a command ls -l # DEMO: a command with pipe ls -l | sort # DEMO: a command with pipe and redirection ls -l | sort > /tmp/jaalto.1396-ls.lst # DEMO: a command with pipe and redirection using quotes ls -l | sort > "/tmp/jaalto.1396-ls.lst" # DEMO: a command and subshell call. echo ls -l
# # test-demo.sh -- Demonstrate how testing feature can be implemented in program # # Copyright (C) 2009 Jari Aalto <jari.aalto@cante.net> # # License # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation; either version 2 of # the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with program. If not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # # Visit <http://www.gnu.org/copyleft/gpl.html> # # Description # # To enable debugging and testing capabilities in shell # scripts, add function Run() and use it to proxy all commands. # # Notes # # The functions in the program are "defined before used". It is only # possible to call a function (= command) if it exists (= is defined). # # There is explicit Main() where program starts. This follows # the convention of good programming style. By putting the code # inside functions, it also makes one think about modularity and # reusable components. DESC="$0 -- Demonstrate how testing feature can be implemented in program" TEMPDIR=${TEMPDIR:-/tmp} TEMPPATH=$TEMPDIR/${LOGNAME:-foo}.$$ # This variable is best to be undefined, not TEST="" or anything. unset TEST Help () { echo "\ $DESC Available options: -d Debug. Before command is run, show it. -t Test mode. Show commands, do not really execute. The -t option takes precedence over -d option." exit ${1:-0} } Run () { if [ "$TEST" ]; then echo "$*" return 0 fi eval "$@" } Echo () { echo "# DEMO: $*" } Demo () { Echo "a command" Run ls -l Echo "a command with pipe" Run "ls -l | sort" Echo "a command with pipe and redirection" Run "ls -l | sort > $TEMPPATH-ls.lst" Echo "a command with pipe and redirection using quotes" Run "ls -l | sort > \"$TEMPPATH-ls.lst\"" # You need to put Run() call also into subshell, otherwise # it would be run "for real" and defeat the test mode. Echo "a command and subshell call." Run "echo $( Run ls -l )" } Main () { echo "$DESC" OPTIND=1 local arg while getopts "hdt" arg "$@" do case "$arg" in h) Help ;; t) TEST="test" ;; esac done # Remove found options from command line arguments. shift $(($OPTIND - 1)) # Run the demonstration Demo } Main "$@" # End of file