Getting Input From Users In BASH
Command line args and Prompting for Input
by Patrick Horgan
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
- positional variables - used as unnamed arguments to scripts and
functions
known as their positions numbers starting at 0. The first one will have
the name
${0}, the second
${1}, the third
${2}, the fourth
${3}, etc. This is how you see the
command line arguments inside a bash script.
${0} is the name of the
script as the user typed it, and the arguments to the program start with
${1}, so if the user typed:
./myscript.sh --arg1 -foo=3 --bar -and dog
${0} would be
./myscript.sh
${1} would be
--arg1
${2} would be
-foo=3
${3} would be
--bar
${4} would be
-and
${5} would be
dog.
All further positional arguments would have empty strings in them.
N.B. There's nothing magical to bash about whether the arguments to scripts
that we ask bash to run have no dash,
one dash, or two dashes. To bash, each of the arguments is a sequence
of characters that get passed to the program as strings. It is up to
the program to decide if a leading dash or two has some special
meaning to it. (That's why MSDOS scripts use \, instead of -, a different
guy came up with a different idea. In Unix, the \ was already used to
escape some characters, so it was not available to indicate a command line
option.)
- named variables - used for us humans to remember what things are for
they have names like
${tmp},
${parameter},
and
${value}.
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
- -v - verbose
- -a - about
- -c - a choice, one of choice1, choice2, and choice3
- -q - quiet mode
- filename - the name of a wonderful file
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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