To print a line of dashes with a simple command might sound easy—and it is. But as soon as you think you’ve got a simple script, it begins to grow. What about varying the length of the line of dashes? What about changing the character from a dash to a user-supplied character? Do you see how easily feature creep occurs? Can we write a simple script that takes those extensions into account without getting too complex?
Consider this script:
1 #!/usr/bin/env bash 2 # cookbook filename: dash 3 # dash - print a line of dashes 4 # options: # how many (default 72) 5 # -c X use char X instead of dashes 6 # 7 function usagexit ( ) 8 { 9 printf "usage: %s [-c X] [#] " $(basename $0) 10 exit 2 11 } >&2 12 LEN=72 13 CHAR='-' 14 while (( $# > 0 )) 15 do 16 case $1 in 17 [0-9]*) LEN=$1;; 18 -c) shift 19 CHAR=$1;; 20 *) usagexit;; 21 esac 22 shift 23 done 24 if (( LEN > 4096 )) 25 then 26 echo "too large" >&2 27 exit 3 28 fi 29 # build the string to the exact length 30 DASHES="" 31 for ((i=0; i<LEN; i++)) 32 do 33 DASHES="${DASHES}${CHAR}" 34 done 35 printf "%s " "$DASHES"
The basic task is accomplished by building a string of the required number of dashes (or an alternate character) and then printing that string to standard output (STD-OUT). That takes only the six lines from 30–35. Lines 12 and 13 set the default values. All the other lines are spent on argument parsing, error checking, user messages, and comments.
You will find that it’s pretty typical for a robust, end-user script. Less than 20 percent of the code does more than 80 percent of the work. But that 80 percent of the code is what makes it usable and “friendly” for your users.
In line 9 we use basename
to
trim off any leading pathname characters when displaying this script’s
name. That way no matter how the user invokes the script (for example,
./dashes, /home/username/bin/dashes, or even
../../over/there/dashes), it will still be referred
to as just dashes in the usage message.
The argument parsing is done while there are some arguments to parse (line 14).
As arguments are handled, each shift
built-in
will decrement the number of arguments and eventually get us out of the
while
loop. There are only two
possible allowable arguments: specifying a number for the length (line
17), and a -c
option followed by a
character (see lines 18–19). Anything else (line 20) will result in the
usage message and an early exit.
We could be more careful in parsing the -c
and its argument. By not using more
sophisticated parsing (e.g., getopt Parsing Arguments for Your Shell Script), the option and
it’s argument must be separated by whitespace. (In running the script
one must type -c n
and not -cn
.) We don’t even check to see that the
second argument is supplied at all. Furthermore, it could be not just a
single letter but a whole string. (Can you think of a simple way to
limit this, by just taking the first character of the argument? Do you
need/want to? Why not let the user specify a string instead of a single
character?)
The parsing of the numerical argument could also use some more
sophisticated techniques. The patterns in a case
statement follow the rules of pathname
expansion and are not regular expressions. It might be
tempting to assume that the case
pattern [0-9]*
means only digits, but
that would be the regular expression meaning. In the case
statement it means any string that begins
with a digit. Not catching erroneous input like 9.5
or 612more
will result in errors in the script
later on. The use of an if
statement
with its more sophisticated regular expression matching might be useful
here.
As a final comment on the code: at line 24 the script enforces a maximum length, though it is completely arbitrary. Would you keep or remove such a restriction?
You can see from this example that even simple scripts can be come quite involved, mostly due to error checking, argument parsing, and the like. For scripts that you write for yourself, such techniques are often glossed over or skipped entirely—after all, as the only user of the script you know the proper usage and are willing to use it correctly or have it fail in an ugly display of error messages. For scripts that you want to share, however, such is not the case, and much care and effort will likely be put into toughening up your script.
18.217.150.123