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
7 function usagexit ( )
9 printf "usage: %s [-c X] [#]\n" $(basename $0)
10 exit 2
11 } >&2
14 while (( $# > 0 ))
16 case $1 in
17 [0-9]*) LEN=$1;;
18 -c) shift
20 *) usagexit;;
24 if (( LEN > 4096 ))
26 echo "too large" >&2
27 exit 3
29 # build the string to the exact length
31 for ((i=0; i<LEN; i++))
35 printf "%s\n" "$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 (STDOUT).
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 number (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 , 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
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.