Getting Input From Users In BASH

Command line args and Prompting for Input

by Patrick Horgan

(Back to scripting tutorials.)

Introduction

This article explains a simple way to deal with command line arguments (both options and non-options) in bash (bourne-again shell), a scripting language released in 1989, made to be both backward compatible with and to extend the bourne shell (generally referred to as sh) a scripting language from 1977. I assume that you have some experience with some programming language and so won't tell you things like the difference between an interpreted and a compiled language. I also assume that you know what a command line is, and are comfortable running programs from the command line. Of course I also assume you can use a text editor--all the usual things people assume you already know if you're interested in writing a program in a scripting language.

Getting and dealing with command line arguments

If you have experience with later scripting languages like python or perl, this will look pretty primitive, but remember, people who develop later get to stand on the shoulders of those who came before;)

Our goal will be that when our program (which we'll call prog), is called like this:

prog -arg1 --arg2 --arg3=val3

that we will, from inside the program have access to variables arg1, arg2, arg3. Of course we want the value of arg3, to be val3, and we also arbitrarily go values to arg1, as well as arg2, just giving them their own name as a value, so that if we did the following,

echo "arg1: " $arg1 echo "arg2: " $arg2 echo "arg3: " $arg3

we'd expect the output to be

arg1: arg1 arg2: arg2 arg3: val3

Telling Users Their Argument Isn't Convincing

Before I show you how to make that magic happen, however, I'll show you what to do if things don't go well. I'll start from the end here. If a user passes you arguments on the commandline, you have a responsibility to let them know exactly what's wrong if they don't get it right. The minimum you can do is give them a usage statement and exit.

fromusage="" #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # usage is called when we're called incorrectly. it never returns #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ usage() { echo "Usage: " `basename $0` " [-v] [-a] [-cN] [-dCHOICE] [-q] filename" echo " -v print version number and quit" echo " -a about - tell about us and exit" echo " -c=N set counter to N" echo " -d=CHOICE set d to CHOICE - one of:" echo " choice1, choice2, choice3 echo " filename a specially nice file" echo " -q quiet mode - error code on exit" fromusage='Y' about } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # about - tell about us and exit #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ about() { if [ "$fromusage" != 'Y' ] ; then echo `basename $0` "- do something we really want done" else echo fi echo " Written By A. Clever Programmer" exit 0 }

I feel used!

usage() gives the user a message about how to run the program, and then calls about() to exit when it's done. This is done to simplify things and to avoid having multiple error exit points. There are a lot of other choices that could have been made, for example, instead of usage() call about(), both of them could exit separately, or both of them could call another routine that does the exit. about() can be called from other places and will normally give a one line summary but when called from usage that would be redundant, so we initialize a variable, fromusage to "" and in usage we set it to 'Y' so that we know to leave out the summary. A little later you'll see how we validate the arguments.

getopt_simple() - Getting Command Line Args into variables

An aside about how variables look and are used in bash

Anytime you see something with a ${some stuff in here} or $a_name it is a variable. Whatever comes right after the ${ or $ will be the name of the variable. I'm not going to talk about why some have braces, {}, and some don't since that is outside the scope of this discussion. If you don't know what form you need always us the braces and you will avoid much aggravation.

We'll see two kinds of variables

Below you'll see variables of the form ${parameter:offset} and ${parameter:offset:length}

${tmp:1} means to use the value of tmp starting at offset 1, i.e. the second character through the end. So that if tmp="-foo", then ${tmp:1} would have the value "foo".

${1:0:1} means to use positional parameter 1, starting at offset 0, i.e. the beginning, and going for a length of 1, so if ${1} had the value "-foo", then ${1:0:1} would have the value "-".

You'll also see ${parameter%%word}, which means match the longest thing from the value of parameter which matches word

In particular, you'll see parameter=${tmp%%=*}, which matches from the right everything up to but not including an = sign. If it doesn't find the equal sign, parameter is set to the entire value of $tmp.

That means that if tmp has the value "foo=bar", then ${tmp%%=*} would have the value bar.

If tmp has the value "foo", then ${tmp%%=*} would have the value foo, the entire value of tmp.

You'll also see value=${tmp##*=} which does the opposite, it will get everything from the beginning of the value of tmp up to but not including an = sign. If it doesn't find the equal sign, value is set to the entire value of tmp.

So if tmp has the value "foo=bar", then ${tmp%%=*} would have the value foo, the entire value of tmp.

And if tmp has the value "foo", then ${tmp%%=*} would have the value foo the entire value of tmp.

That means for something like option, both parameter and value will be set to 'option'. For something like option=value, parameter will be set to option, and value will be set to thevalue.

Anything between [ and ] is a test, a condition we're checking, and in fact, it is an alias for the test command. On a linux/unix system, you can do a man [ and you will get the man page for the test program (or perhaps a man page for your shell's builtin programs). The only difference between [ and test is that [ requires that its last argument which will be ignored, is ]. In this we'll use the -z STRING arguments to the test, like this: [ -z STRING ] looking for when we run out of arguments. (-z STRING tests true iff STRING has zero length).

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # getopt_simple() - Orig by Chris Morgan, from ABS Guide and modified a bit #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ getopt_simple() { until [ -z "$1" ] ; do if [ ${1:0:1} = '-' ] ; then tmp=${1:1} # Strip off leading '-' . . . if [ ${tmp:0:1} = '-' ] ; then tmp=${tmp:1} # Allow double - fi parameter=${tmp%%=*} # Extract name. value=${tmp##*=} # Extract value. eval $parameter=$value else filename="$1" fi shift done }

What did they want?

If you've not written a bash script before, this may look arcane, but I'll take you through it. Ignore everything that starts with # as it does not execute, it's just a comment from the author to himself, or to some other programmer that will be reading the code.

The first thing you'll see is that we started an until loop. An until loop continues until the loop condition is false. If you want more than one line to be run inside the loop, (which we do here), all of the lines have to be between do, and done. The loop condition is the [ -z "$1" ]. The positional argument, $1 the first argument on the command line is expanded insided the quotes. If there's nothing in $1, you'll just get "" a zero length string. That's how we tell when we run out of arguments.

So the loop body starts at do and ends at done. In the middle we check for a leading dash, a -. If there is one, we strip it off (and also strip off any second dash so that arguments can start with a single or double dash. Then we get the parameter and value (which will be the same if there is no = sign), and call eval to create a bash variable whose name is the value of parameter, and whose value is the value of values. An option -w will end up as a variable named w with value w, and a variable -option=foo will end up as a variable with name of foo.

This setup allows one argument that doesn't begin with a dash, and it is assigned to the variable filename. If you need more than one argument that isn't an option, you'd just do them one at a time outside the loop. We don't cover that here, but it is not too hard.

At the end of the loop we call the shift command which moves all the positional arguments over by one, $1 get the value of $2, $2 the value of $3, etc. That we we only have to deal with $1 in the loop and the values of all the other arguments shift into it.

Lets try it out.

The next chunk assumes that it follows the previous code. In bash, you can only refer to things in the script that have already been seen.

getopt_simple() "$@"

$@ is what bash uses to mean all of the arguments but with quotes applied. They will become the positional arguments for getopt_simple() which we've seen. According to the usage statement above we're expecting one of

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Our program starts here. This is the equivalent of our main() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Give initial defaults to things so we can tell if they # change version="1.0" fromusage="" a="" c="" q="no" v="" quiet="no" filename="none" getopt_simple "$@" if [ "$v" == 'v' ] ; then # version echo `basename $0` version $version exit 0 fi if [ "$a" == 'a' ] ; then about fi if [ "$filename" == "none" ] ; then usage fi if [ $q != 'no' ] ; then quiet=$q fi if [ $quiet != 'no' ] ; then # anything but no is 'Y' so we don't have to test much. quiet='Y' #close standard out and standard err exec >& /dev/null fi # get filename from first argument srcfile=$(basename "$filename") # determine file directory filedir=$(dirname "$filename") echo 'a' $a echo 'c' $c echo 'q' $q echo 'v' $v echo 'filename' $filename echo 'srcfile' $srcfile echo 'filedir' $outdir

First initialize everything

We need to initialize everything that might get set by

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # getval() # o Set prompt to the prompt you want to give a user # o goodvals to the list of acceptable values # o call getval # o when it returns your value is in outval # o when called in quiet mode it returns default or if none , outval == "FAIL" #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ getval() { flag="notdone" elementcount=${#goodvals[@]} if [ "$quiet" == "Y" ] ; then if [ A$default != "A" ] ; then outval=$default default="" else outval="FAIL" fi else until [ $flag == "done" ] ; do echo -n $prompt " " read inval if [ A$inval == "A" ] ; then # inval is empty if [ A$default != 'A' ] ; then # default is set to something inval=$default default="" else #inval is empty, no default echo You must enter a value index=0 echo -n "Expecting one of : " while [ "$index" -lt "$elementcount" ] ; do echo -n "${goodvals["$index"]}" " " let index++ done echo fi fi if [ A$inval != "A" ] ; then # inval not empty, either they sent us something # or we got it from the default index=0 while [ "$index" -lt "$elementcount" ] ; do # Walk through list of goodvals to see if we got one if [ ${goodvals[$index]} == $inval ] ; then # Yep! We're done. flag="done" outval=${goodvals[$index]} fi let index++ done if [ $flag != "done" ] ; then # inval not in goodvals, let them know index=0 echo -n "Expecting one of : " while [ "$index" -lt "$elementcount" ] ; do echo -n "${goodvals["$index"]}" " " let index++ done echo fi fi done fi } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # getnumval() # o Set prompt to the prompt you want to give a user # o call getnumval # o when it returns your value is in outval # o when called in quiet mode it returns default or if none , outval == "FAIL" # o set option min and/or max to range #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ getnumval() { flag="notdone" if [ "$quiet" == "Y" ] ; then if [ A$default != "A" ] ; then outval=$default default="" else outval="FAIL" fi else until [ $flag == "done" ] ; do echo -n $prompt " " read inval if [ A$inval == "A" ] ; then # inval is empty if [ A$default != 'A' ] ; then # but default is not, so use it inval=$default default="" else # no inval, no default echo "You must enter a value, expecting a positive numeric value" fi fi if [ "A"$inval != 'A' ] ; then # inval set either from user or default case $inval in *[^0-9]*) echo "Error: expecting positive numeric value" ;; * ) minmaxerror='F' if [ A$min != 'A' ] ; then if [ $inval -lt $min ] ; then echo "Error: entered $inval must be >= $min" minmaxerror='T' fi fi if [ A$max != 'A' ] ; then if [ $inval -gt $max ] ; then echo "Error: entered $inval must be <= $max" minmaxerror='T' fi fi if [ $minmaxerror != 'T' ] ; then flag="done" fi ;; esac fi done min="" max="" outval=$inval fi } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # validatearg() # o set inarg to the value of the argument # o set goodvals to the list of acceptable values # o set prompt to the error message you'd like to give, # for example "ERROR: bad value for transparency arg" # this routine will, if not quiet mode, append to it, " expecting: " and # the list of values from goodvals, then call usage # to exit # o set errorval to error code to exit with in case of quiet mode #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ validatearg() { flag="notgood" elementcount=${#goodvals[@]} index=0 if [ "A"$inarg != "A" ] ; then while [ "$index" -lt "$elementcount" ] ; do if [ ${goodvals[$index]} == $inarg ] ; then flag="good" outval=${goodvals[$index]} fi let index++ done fi if [ $flag != "good" ] ; then if [ "$quiet" == "Y" ] ; then exit $errorcode fi index=0 echo -n $prompt echo -n " expecting one of : " while [ "$index" -lt "$elementcount" ] ; do echo -n "${goodvals["$index"]}" " " let index++ done echo echo usage fi } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Our program starts here. This is the equivalent of our main() #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Give initial defaults to things so we can tell if they # change OS=$(uname) fromusage="" alltos="" transparency='no' t='no' resolution=0 r=0 format='none' f='none' quiet='no' q='no' p='no' preview='no' errorcode=0 viewer="none" # set Lilypond PATH if OS is Darwin if [ "$OS" == 'Darwin' ] ; then export PATH="$PATH:/Applications/LilyPond.app/Contents/Resources/bin/" fi # search for default viewers starting with xdg-open and going to eog # then trying evince, add to the list to change. if [ "$OS" == "Darwin" ] ; then viewers=( open preview ) else viewers=( xdg-open eog evince gwenview ) fi elementcount=${#viewers[@]} index=0 flag='notsogood' while [[ ( "$index" -lt "$elementcount") && ( $flag != 'good' ) ]] ; do which >& /dev/null ${viewers[$index]} if [ $? -eq 0 ] ; then flag="good" defaultV=${viewers[$index]} fi let index++ done filename="none" version=1.2 setformatlist # Gets list of all image formats we can convert to if [ $returnstatus -ne 0 ] ; then # Apparently none! echo "Sorry, you have to have the netpbm utilities installed to use this." exit 43 fi # process all the options getopt_simple "$@" if [ "$v" == 'v' ] ; then # version echo `basename $0` version $version exit 0 fi if [ A$V != 'A' ] ; then # if they set one from the command line, use it viewer=$V # default to eog preview='Y' # doesn't make sense to sprecify viewer without viewing elif [ A$defaultV != "A" ] ; then # they didn't set one from the command line, use default if set viewer=$defaultV else # no command line, no default. viewer='none' fi if [ "$a" == 'a' ] ; then about fi if [ "$filename" == "none" ] ; then usage fi if [ $t != 'no' ] ; then # We let them use -t or --transparency, so if they used -t, we shove # the value in $transparency so we don't have to deal with both later transparency=$t fi if [ $transparency != 'no' ] ; then # if transparency is set, make that setting be 'Y' cause that's what # we check for later. transparency='Y' fi if [ $q != 'no' ] ; then quiet=$q fi if [ $quiet != 'no' ] ; then quiet='Y' exec >& /dev/null fi if [ $p != 'no' ] ; then preview="$p" fi if [ $preview != 'no' ] ; then preview="Y" fi # We know $r starts numeric cause we initialize it to 0 if it's not numeric # now the user put something in it not numeric case $r in *[^0-9]*) if [ $quiet != 'no' ] ; then exit 45 else echo "Error: resolution must be postive numeric"; usage fi ;; esac if [ $r -ne 0 ] ; then # same as with -t, two versions of args, -r and --resolution resolution=$r fi # Now check resolution for numeric...if it came from -r it has already # been checked, but no harm checking again case $resolution in *[^0-9]*) if [ $quiet != 'no' ] ; then exit 45 else echo "Error: resolution must be positive numeric"; usage fi ;; esac if [ $f != 'none' ] ; then # fold -f into --format format=$f fi if [ $format != "none" ] ; then # They set format so check it inarg=$format if [ "$format" == "jpg" ] ; then inarg="jpeg" format=$inarg elif [ "$format" == "tif" ] ; then inarg="tiff" format=$inarg fi goodvals=( $alltos ) prompt="Error: format arg incorrect" errorcode=46 validatearg echo "Output format is $format..." fi # get filename from first argument srcfile=$(basename "$filename") # get filename without .ly extension STEM=$(basename "$filename" .ly) # determine output directory OUTDIR=$(dirname "$filename") if [[ $resolution -ne 0 ]] ; then echo "Resolution set to $resolution DPI..." else # ask for output resolution prompt="Enter output resolution in DPI 72, 150, 300, 600, etc...(150): " default=150 min=2 # if resolution is less than two lilypond will fail getnumval if [ outval == "FAIL" ] ; then exit 42 fi resolution=$outval echo "Resolution set to $outval DPI..." fi # ask for desired final output format with a lot of complications based on # whether transparency is set. if [[ ( "$transparency" == "Y" ) || ( "$transparency" == "y" ) ]] ; then echo "Background is set to transparent." if [[ ( "$format" != 'gif') && ( "$format" != 'png' ) ]] ; then # if they ask for transparency and format's set to something other # than gif or png we can't procede--it makes no sense, get them to # resolve it. if [[ "$format" != 'none' ]] ; then echo "You ask for transparency, which doesn't work with" $format fi prompt="Enter desired output format png, gif (png): " default="png" goodvals=("png" "gif") getval if [ outval == "FAIL" ] ; then exit 41 fi FORMAT=$outval echo "Output format is $outval..." else FORMAT=$format fi else # we know transparency's not Y or y, but make any other value be 'no' # so we only have one thing to check for later transparency="no" # transparency's not set, so if they gave us a format on the command # line use that, else ask them for one. if [[ $format != 'none' ]] ; then if [ "$format" == "jpg" ] ; then FORMAT=jpeg elif [ "$format" == "tif" ] ; then FORMAT=tiff else FORMAT=$format fi else prompt="Enter desired output format jpeg, png, tiff, gif, pcx, bmp ... (png): " default='png' goodvals=( $alltos ) getval if [ outval == "FAIL" ] ; then exit 41 fi FORMAT=$outval echo "Output format is $FORMAT..." fi fi cd $OUTDIR #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Okay! - Everything up to here was getting ready, now we'll finally do the job! #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ exit 0
(Back to scripting tutorials.)