One of the main purposes of creating shell scripts is to drop complex command line sequences into files, making them replicable and easy to tweak. It should be no surprise, then, that user commands are sprawled across this book. What is surprising? That we haven’t written a wrapper for every single command on our Linux, Solaris, and OS X systems.
Linux/Unix is the only major operating system where you can decide that you don’t like the default flags of a command and fix it forever with just a few keystrokes, or where you can emulate the behavior of your favorite utilities from other operating systems by using an alias or a dozen lines of script. That’s what makes Unix so tremendously fun—and what led to writing this book in the first place!
Windows and Mac users have appreciated simple utilities like Stickies for years, the streamlined applications that let you keep tiny notes and reminders stuck on your screen. They’re perfect for jotting down phone numbers or other reminders. Unfortunately, there’s no analog if you want to take notes while working on a Unix command line, but the problem is easily solved with this pair of scripts.
The first script, remember (shown in Listing 3-1), lets you easily save your snippets of information into a single rememberfile in your home directory. If invoked without any arguments, it reads standard input until the end-of-file sequence (^D) is given by pressing CTRL-D. If invoked with arguments, it just saves those arguments directly to the data file.
The other half of this duo is remindme, a companion shell script shown in Listing 3-2, which either displays the contents of the whole rememberfile when no arguments are given or displays the results of searching through it using the arguments as a pattern.
#!/bin/bash # remember--An easy command line-based reminder pad rememberfile="$HOME/.remember" if [ $# -eq 0 ] ; then # Prompt the user for input and append whatever they write to # the rememberfile. echo "Enter note, end with ^D: " ➊ cat - >> $rememberfile else # Append any arguments passed to the script on to the .remember file. ➋ echo "$@" >> $rememberfile fi exit 0
Listing 3-1: The remember shell script
Listing 3-2 details the companion script, remindme.
#!/bin/bash # remindme--Searches a data file for matching lines or, if no # argument is specified, shows the entire contents of the data file rememberfile="$HOME/.remember" if [ ! -f $rememberfile ] ; then echo "$0: You don't seem to have a .remember file. " >&2 echo "To remedy this, please use 'remember' to add reminders" >&2 exit 1 fi if [ $# -eq 0 ] ; then # Display the whole rememberfile when not given any search criteria. ➌ more $rememberfile else # Otherwise, search through the file for the given terms, and display # the results neatly. ➍ grep -i -- "$@" $rememberfile | ${PAGER:-more} fi exit 0
Listing 3-2: The remindme shell script, a companion to the remember shell script in Listing 3-1
The remember shell script in Listing 3-1 can work as an interactive program, requesting the user to enter the details to remember, or it could actually be scripted since it can also accept anything to store simply as a command line argument. If a user does not pass any arguments to the script, then we do a little tricky coding. After printing a user-friendly message on how to enter an item, we read the data from the user with cat ➊:
cat - >> $rememberfile
In previous chapters, we have used the read command to get input from the user. This line of code reads from stdin (the - in the command is shorthand for stdin or stdout, depending on the context) using cat until the user presses CTRL-D, which tells the cat utility that the file has ended. As cat prints the data it reads from stdin, and appends this data to the rememberfile.
If an argument is specified to the script, however, all arguments are simply appended as is to the rememberfile ➋.
The remindme script in Listing 3-2 cannot work if the rememberfile doesn’t exist, so we first check if the rememberfile exists before attempting to do anything. If the rememberfile doesn’t exist, we exit immediately after printing a message to the screen alerting the user why.
If no arguments are passed to the script, we assume the user just wants to see the contents of the rememberfile. Using the more utility to allow paging through the rememberfile, we simply display the contents to the user ➌.
Otherwise, if arguments are passed to the script, we perform a case-insensitive grep to search for any matching terms in the rememberfile, and then display these results with paging as well ➍.
To use the remindme utility, first add notes, phone numbers, or anything else to the rememberfile with the remember script, as in Listing 3-3. Then search this freeform database with remindme, specifying as long or short a pattern as you’d like.
$ remember Southwest Airlines: 800-IFLYSWA $ remember Enter note, end with ^D: Find Dave's film reviews at http://www.DaveOnFilm.com/ ^D
Listing 3-3: Testing the remember shell script
Then, when you want to remember that note months later, Listing 3-4 shows how you can find the reminder.
$ remindme film reviews
Find Dave's film reviews at http://www.DaveOnFilm.com/
Listing 3-4: Testing the remindme shell script
Or if there’s an 800 number you can’t quite recall, Listing 3-5 demonstrates locating a partial phone number.
$ remindme 800
Southwest Airlines: 800-IFLYSWA
Listing 3-5: Locating a partial phone number with the remindme script
While certainly not any sort of shell script programming tour de force, these scripts neatly demonstrate the extensibility of the Unix command line. If you can envision something, the odds are good that there’s a simple way to accomplish it.
These scripts could be improved in any number of ways. For instance, you could introduce the concept of records: each remember entry is timestamped, and multiline input can be saved as a single record that can be searched for using regular expressions. This approach lets you store phone numbers for a group of people and retrieve them all just by remembering the name of one person in the group. If you’re really into scripting, you might also want to include edit and delete capabilities. Then again, it’s pretty easy to edit the ~/.remember file by hand.
If you’ll remember, scriptbc (Script #9 on page 34) allowed us to invoke floating-point bc calculations as inline command arguments. The logical next step is to write a wrapper script to turn this script into a fully interactive command line–based calculator. The script (shown in Listing 3-6) ends up being really short! Ensure that the scriptbc script is in the PATH, otherwise this script will fail to run.
#!/bin/bash
# calc--A command line calculator that acts as a frontend to bc
scale=2
show_help()
{
cat << EOF
In addition to standard math functions, calc also supports:
a % b remainder of a/b
a ^ b exponential: a raised to the b power
s(x) sine of x, x in radians
c(x) cosine of x, x in radians
a(x) arctangent of x, in radians
l(x) natural log of x
e(x) exponential log of raising e to the x
j(n,x) Bessel function of integer order n of x
scale N show N fractional digits (default = 2)
EOF
}
if [ $# -gt 0 ] ; then
exec scriptbc "$@"
fi
echo "Calc--a simple calculator. Enter 'help' for help, 'quit' to quit."
/bin/echo -n "calc> "
➊ while read command args
do
case $command
in
quit|exit) exit 0 ;;
help|\?) show_help ;;
scale) scale=$args ;;
*) scriptbc -p $scale "$command" "$args" ;;
esac
/bin/echo -n "calc> "
done
echo ""
exit 0
Listing 3-6: The calc command line calculator shell script
Perhaps the most interesting part of this code is the while read statement ➊, which creates an infinite loop that displays the calc> prompt until the user exits, either by entering quit or by entering an end-of-file sequence (^D). The simplicity of this script is what makes it extra wonderful: shell scripts don’t need to be complex to be useful!
This script uses scriptbc, the floating-point calculator we wrote in Script #9, so make sure you have that script available in your PATH as scriptbc (or set a variable like $scriptbc to the script’s current name) before running it. By default, this script runs as an interactive tool that prompts the user for the desired actions. If invoked with arguments, those arguments are passed along to the scriptbc command instead. Listing 3-7 shows both usage options at work.
$ calc 150 / 3.5 42.85 $ calc Calc--a simple calculator. Enter 'help' for help, 'quit' to quit. calc> help In addition to standard math functions, calc also supports: a % b remainder of a/b a ^ b exponential: a raised to the b power s(x) sine of x, x in radians c(x) cosine of x, x in radians a(x) arctangent of x, in radians l(x) natural log of x e(x) exponential log of raising e to the x j(n,x) Bessel function of integer order n of x scale N show N fractional digits (default = 2) calc> 54354 ^ 3 160581137553864 calc> quit $
Listing 3-7: Testing the calc shell script
WARNING
Floating-point calculations, even those that are easy for us humans, can be tricky on computers. Unfortunately, the bc command can reveal some of these glitches in unexpected ways. For example, in bc, set scale=0 and enter 7 % 3. Now try it with scale=4. This produces .0001, which is clearly incorrect.
Whatever you can do in bc on a command line you can do in this script, with the caveat that calc.sh has no line-to-line memory or state retention. This means you could add more mathematical functions to the help system, if you were so inclined. For example, the variables obase and ibase let you specify input and output numeric bases, though since there’s no line-by-line memory, you’d have to either modify scriptbc (Script #9 on page 34) or learn to enter the setting and the equation all on a single line.
The script in Listing 3-8, which marks the first use of sophisticated mathematics in this book, can translate any temperature between Fahrenheit, Celsius, and Kelvin units. It uses the same trick of piping an equation to bc as we used in Script #9 on page 34.
#!/bin/bash # convertatemp--Temperature conversion script that lets the user enter # a temperature in Fahrenheit, Celsius, or Kelvin and receive the # equivalent temperature in the other two units as the output if [ $# -eq 0 ] ; then cat << EOF >&2 Usage: $0 temperature[F|C|K] where the suffix: F indicates input is in Fahrenheit (default) C indicates input is in Celsius K indicates input is in Kelvin EOF exit 1 fi ➊ unit="$(echo $1|sed -e 's/[-[:digit:]]*//g' | tr '[:lower:]' '[:upper:]' )" ➋ temp="$(echo $1|sed -e 's/[^-[:digit:]]*//g')" case ${unit:=F} in F ) # Fahrenheit to Celsius formula: Tc = (F - 32) / 1.8 farn="$temp" ➌ cels="$(echo "scale=2;($farn - 32) / 1.8" | bc)" kelv="$(echo "scale=2;$cels + 273.15" | bc)" ;; C ) # Celsius to Fahrenheit formula: Tf = (9/5)*Tc+32 cels=$temp kelv="$(echo "scale=2;$cels + 273.15" | bc)" ➍ farn="$(echo "scale=2;(1.8 * $cels) + 32" | bc)" ;; ➎ K ) # Celsius = Kelvin - 273.15, then use Celsius -> Fahrenheit formula kelv=$temp cels="$(echo "scale=2; $kelv - 273.15" | bc)" farn="$(echo "scale=2; (1.8 * $cels) + 32" | bc)" ;; *) echo "Given temperature unit is not supported" exit 1 esac echo "Fahrenheit = $farn" echo "Celsius = $cels" echo "Kelvin = $kelv" exit 0
Listing 3-8: The convertatemp shell script
At this point in the book, most of the script is probably clear, but let’s have a closer look at the math and regular expressions that do all the work. “Math first,” as most school-age children would undoubtedly not appreciate hearing! Here is the formula for converting degrees Fahrenheit to degrees Celsius:
Converted into a sequence that can be fed to bc and solved, it looks like the code at ➌. The reverse conversion, Celsius to Fahrenheit, is at ➍. The script also converts the temperature from Celsius to Kelvin ➎. This script demonstrates one big reason to use mnemonic variable names: it makes the code a whole lot easier to read and debug.
The other bits of code here that are interesting are the regular expressions, the gnarliest of which is easily the one at ➊. What we’re doing is pretty straightforward, if you can unwrap the sed substitution. Substitutions always look like s/old/new/; the old pattern here is zero or more occurrences of -, followed by any of the set of digits (recall that [:digit:] is the ANSI character set notation for any digit and * matches zero or more occurrences of the previous pattern). The new pattern then is what we want to replace the old pattern with, and in this case it is simply //, which signifies an empty pattern; this pattern is useful when you just want to remove the old one. This substitution effectively removes all the digits so that inputs like -31f turn into just f, giving us the type of units. Finally, the tr command normalizes everything to uppercase so, for example, -31f turns into F.
The other sed expression does the opposite ➋: it removes anything that isn’t numeric by using the ^ operator to negate matches for any characters in the class [:digit:]. (Most languages use ! as negation.) This provides us with the value we eventually convert using the appropriate equation.
This script has a nice, intuitive input format, even if it is pretty unusual for a Unix command. Input is entered as a numeric value, with an optional suffix that indicates the units of the temperature entered; when no suffix is given, the code assumes the units are Fahrenheit.
To see the Celsius and Kelvin equivalents of 0° Fahrenheit, enter 0F. To see what 100° Kelvin is in Fahrenheit and Celsius, use 100K. And to get 100° Celsius in Kelvin and Fahrenheit, enter 100C.
You’ll see this same single-letter suffix approach again in Script #60 on page 190, which converts currency values.
Listing 3-9 shows conversion across many different temperatures.
$ convertatemp 212 Fahrenheit = 212 Celsius = 100.00 Kelvin = 373.15 $ convertatemp 100C Fahrenheit = 212.00 Celsius = 100 Kelvin = 373.15 $ convertatemp 100K Fahrenheit = -279.67 Celsius = -173.15 Kelvin = 100
Listing 3-9: Testing the convertatemp shell script with a few conversions
You can add a few input flags to generate succinct output for only one conversion at a time. Something like convertatemp -c 100F could output just the Celsius equivalent of 100° Fahrenheit, for example. This approach will help you use converted values in other scripts as well.
Another common calculation users might deal with is estimation of loan payments. The script in Listing 3-10 also helps answer the question “What can I do with that bonus?” and the related question “Can I finally afford that new Tesla?”
While the formula to calculate payments based on the principal, interest rate, and duration of the loan is a bit tricky, some judicious use of shell variables can tame the mathematical beast and make it surprisingly understandable.
#!/bin/bash # loancalc--Given a principal loan amount, interest rate, and # duration of loan (years), calculates the per-payment amount # Formula is M = P * ( J / (1 - (1 + J) ^ -N)), # where P = principal, J = monthly interest rate, N = duration (months). # Users typically enter P, I (annual interest rate), and L (length, years). ➊ . library.sh # Start by sourcing the script library. if [ $# -ne 3 ] ; then echo "Usage: $0 principal interest loan-duration-years" >&2 exit 1 fi ➋ P=$1 I=$2 L=$3 J="$(scriptbc -p 8 $I / \( 12 \* 100 \) )" N="$(( $L * 12 ))" M="$(scriptbc -p 8 $P \* \( $J / \(1 - \(1 + $J\) \^ -$N\) \) )" # Now a little prettying up of the value: ➌ dollars="$(echo $M | cut -d. -f1)" cents="$(echo $M | cut -d. -f2 | cut -c1-2)" cat << EOF A $L-year loan at $I% interest with a principal amount of $(nicenumber $P 1 ) results in a payment of \$$dollars.$cents each month for the duration of the loan ($N payments). EOF exit 0
Listing 3-10: The loancalc shell script
Exploring the formula itself is beyond the scope of this book, but it’s worth noting how a complex mathematical formula can be implemented directly in a shell script.
The entire calculation could be solved using a single long input stream to bc, because that program also supports variables. However, being able to manipulate the intermediate values within the script itself proves beyond the capabilities of the bc command alone. Also, frankly, breaking up the equation into a number of intermediate equations ➋ also facilitates debugging. For example, here’s the code that splits the computed monthly payment into dollars and cents and ensures that it’s presented as a properly formatted monetary value:
dollars="$(echo $M | cut -d. -f1)" cents="$(echo $M | cut -d. -f2 | cut -c1-2)"
The cut command proves useful here ➌. The second line of this code grabs the portion of the monthly payment value that follows the decimal point and then chops off anything after the second character. If you would prefer to round this number to the next nearest cent instead, just add 0.005 to the value before truncating the cents at two digits.
Notice also how at ➊, the script library from earlier in the book is neatly included with the . library.sh command in the script, ensuring that all the functions (for our purposes in this script, the nicenumber() function from Chapter 1) are then accessible to the script.
This minimalist script expects three parameters: the amount of the loan, the interest rate, and the duration of the loan (in years).
Say you’ve been eyeing a new Tesla Model S, and you’re curious about how much your payments would be if you bought the car. The Model S starts at about $69,900 out the door, and the latest interest rates are running at 4.75 percent for an auto loan. Assuming your current car is worth about $25,000 and that you can trade it in at that price, you’ll be financing the difference of $44,900. If you haven’t already had second thoughts, you’d like to see what the difference is in total payments between a four-year and five-year car loan—easily done with this script, as Listing 3-11 shows.
$ loancalc 44900 4.75 4 A 4-year loan at 4.75% interest with a principal amount of 44,900 results in a payment of $1028.93 each month for the duration of the loan (48 payments). $ loancalc 44900 4.75 5 A 5-year loan at 4.75% interest with a principal amount of 44,900 results in a payment of $842.18 each month for the duration of the loan (60 payments).
Listing 3-11: Testing the loancalc shell script
If you can afford the higher payments on the four-year loan, the car will be paid off sooner, and your total payments (monthly payment times number of payments) will be significantly less. To calculate the exact savings, we can use the interactive calculator from Script #23 on page 82, as shown here:
$ calc '(842.18 * 60) - (1028.93 * 48)'
1142.16
This seems like a worthwhile savings: $1,142.16 could buy a nice laptop!
This script could really do with a way to prompt for each field if the user doesn’t provide any parameters. An even more useful version of this script would let a user specify any three parameters of the four (principal, interest rate, number of payments, and monthly payment amount) and automatically solve for the fourth value. That way, if you knew you could afford only $500 per month in payments and that the maximum duration of a 6 percent auto loan was 5 years, you could ascertain the largest amount of principal that you could borrow. You could accomplish this calculation by implementing flags that users can use to pass in the values they want.
This is actually a pair of scripts that together implement a simple calendar program, similar to our reminder utility from Script #22 on page 80. The first script, addagenda (shown in Listing 3-12), enables you to specify a recurring event (with either a day of the week for weekly events or a day and month for annual ones) or a one-time event (with the day, month, and year). All the dates are validated and saved, along with a one-line event description, in an .agenda file in your home directory. The second script, agenda (shown in Listing 3-13), checks all known events to show which ones are scheduled for the current date.
This kind of tool is particularly useful for remembering birthdays and anniversaries. If you have trouble remembering events, this handy script can save you a lot of grief!
#!/bin/bash # addagenda--Prompts the user to add a new event for the agenda script agendafile="$HOME/.agenda" isDayName() { # Return 0 if all is well, 1 on error. case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in sun*|mon*|tue*|wed*|thu*|fri*|sat*) retval=0 ;; * ) retval=1 ;; esac return $retval } isMonthName() { case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in jan*|feb*|mar*|apr*|may|jun*) return 0 ;; jul*|aug*|sep*|oct*|nov*|dec*) return 0 ;; * ) return 1 ;; esac } ➊ normalize() { # Return string with first char uppercase, next two lowercase. /bin/echo -n $1 | cut -c1 | tr '[[:lower:]]' '[[:upper:]]' echo $1 | cut -c2-3| tr '[[:upper:]]' '[[:lower:]]' } if [ ! -w $HOME ] ; then echo "$0: cannot write in your home directory ($HOME)" >&2 exit 1 fi echo "Agenda: The Unix Reminder Service" /bin/echo -n "Date of event (day mon, day month year, or dayname): " read word1 word2 word3 junk if isDayName $word1 ; then if [ ! -z "$word2" ] ; then echo "Bad dayname format: just specify the day name by itself." >&2 exit 1 fi date="$(normalize $word1)" else if [ -z "$word2" ] ; then echo "Bad dayname format: unknown day name specified" >&2 exit 1 fi if [ ! -z "$(echo $word1|sed 's/[[:digit:]]//g')" ] ; then echo "Bad date format: please specify day first, by day number" >&2 exit 1 fi if [ "$word1" -lt 1 -o "$word1" -gt 31 ] ; then echo "Bad date format: day number can only be in range 1-31" >&2 exit 1 fi if [ ! isMonthName $word2 ] ; then echo "Bad date format: unknown month name specified." >&2 exit 1 fi word2="$(normalize $word2)" if [ -z "$word3" ] ; then date="$word1$word2" else if [ ! -z "$(echo $word3|sed 's/[[:digit:]]//g')" ] ; then echo "Bad date format: third field should be year." >&2 exit 1 elif [ $word3 -lt 2000 -o $word3 -gt 2500 ] ; then echo "Bad date format: year value should be 2000-2500" >&2 exit 1 fi date="$word1$word2$word3" fi fi /bin/echo -n "One-line description: " read description # Ready to write to data file ➋ echo "$(echo $date|sed 's/ //g')|$description" >> $agendafile exit 0
Listing 3-12: The addagenda shell script
The second script, in Listing 3-13, is shorter but is used more often.
#!/bin/sh # agenda--Scans through the user's .agenda file to see if there # are matches for the current or next day agendafile="$HOME/.agenda" checkDate() { # Create the possible default values that will match today. weekday=$1 day=$2 month=$3 year=$4 ➌ format1="$weekday" format2="$day$month" format3="$day$month$year" # And step through the file comparing dates... IFS="|" # The reads will naturally split at the IFS. echo "On the agenda for today:" while read date description ; do if [ "$date" = "$format1" -o "$date" = "$format2" -o \ "$date" = "$format3" ] then echo " $description" fi done < $agendafile } if [ ! -e $agendafile ] ; then echo "$0: You don't seem to have an .agenda file. " >&2 echo "To remedy this, please use 'addagenda' to add events" >&2 exit 1 fi # Now let's get today's date... ➍ eval $(date '+weekday="%a" month="%b" day="%e" year="%G"') ➎ day="$(echo $day|sed 's/ //g')" # Remove possible leading space. checkDate $weekday $day $month $year exit 0
Listing 3-13: The agenda shell script, a companion to the addagenda script in Listing 3-12
The addagenda and agenda scripts support three types of recurring events: weekly events (“every Wednesday”), annual events (“every August 3”), and one-time events (“January 1, 2017”). As entries are added to the agenda file, their specified dates are normalized and compressed so that 3 August becomes 3Aug and Thursday becomes Thu. This is accomplished with the normalize function in addagenda ➊.
This function chops any value entered down to three characters, ensuring that the first character is uppercase and the second and third are lowercase. This format matches the standard abbreviated day and month name values from the date command output, which will be critical for the correct functioning of the agenda script. The rest of the addagenda script has nothing particularly complex happening in it; the bulk of it is devoted to error tests for bad data formats.
Finally, at ➋ it saves the now normalized record data to the hidden file. The ratio of error-checking code to actual functional code is pretty typical of a well-written program: clean up the data on input and you’ll be able to confidently make assumptions about its formatting in subsequent apps.
The agenda script checks for events by transforming the current date into the three possible date string formats (dayname, day+month, and day+month+year) ➌. It then compares these date strings to each line in the .agenda data file. If there’s a match, that event is shown to the user.
The coolest hack in this pair of scripts is probably how an eval is used to assign variables to each of the four date values needed ➍.
eval $(date "+weekday=\"%a\" month=\"%b\" day=\"%e\" year=\"%G\"")
It’s possible to extract the values one by one (for example, weekday="$(date +%a)"), but in very rare cases, this method can fail if the date rolls over to a new day in the middle of the four date invocations, so a succinct single invocation is preferable. Plus, it’s just cool.
Since date returns a day as a number with either a leading zero or a leading space, neither of which are desired, the next line of code at ➎ strips both from the value, if present, before proceeding. Go have a peek to see how that works!
The addagenda script prompts the user for the date of a new event. Then, if it accepts the date format, the script prompts for a one-line description of the event.
The companion agenda script has no parameters and, when invoked, produces a list of all events scheduled for the current date.
To see how this pair of scripts works, let’s add a number of new events to the database, as Listing 3-14 shows.
$ addagenda Agenda: The Unix Reminder Service Date of event (day mon, day month year, or dayname): 31 October One-line description: Halloween $ addagenda Agenda: The Unix Reminder Service Date of event (day mon, day month year, or dayname): 30 March One-line description: Penultimate day of March $ addagenda Agenda: The Unix Reminder Service Date of event (day mon, day month year, or dayname): Sunday One-line description: sleep late (hopefully) $ addagenda Agenda: The Unix Reminder Service Date of event (day mon, day month year, or dayname): march 30 17 Bad date format: please specify day first, by day number $ addagenda Agenda: The Unix Reminder Service Date of event (day mon, day month year, or dayname): 30 march 2017 One-line description: Check in with Steve about dinner
Listing 3-14: Testing the addagenda script and adding many agenda items
Now the agenda script offers a quick and handy reminder of what’s happening today, detailed in Listing 3-15.
$ agenda
On the agenda for today:
Penultimate day of March
sleep late (hopefully)
Check in with Steve about dinner
Listing 3-15: Using the agenda script to see what our agenda items are for today
Notice that it matched entries formatted as dayname, day+month, and day+month+year. For completeness, Listing 3-16 shows the associated .agenda file, with a few additional entries:
$ cat ~/.agenda
14Feb|Valentine's Day
25Dec|Christmas
3Aug|Dave's birthday
4Jul|Independence Day (USA)
31Oct|Halloween
30Mar|Penultimate day of March
Sun|sleep late (hopefully)
30Mar2017|Check in with Steve about dinner
Listing 3-16: The raw contents of the .agenda file storing the agenda items
This script really just scratches the surface of this complex and interesting topic. It’d be nice to have it look a few days ahead, for example; this could be accomplished in the agenda script by doing some date math. If you have the GNU date command, date math is easy. If you don’t, well, enabling date math solely in the shell requires a complex script. We’ll look more closely at date math later in the book, notably in Script #99 on page 330, Script #100 on page 332, and Script #101 on page 335.
Another (easier) hack would be to have agenda output Nothing scheduled for today when there are no matches for the current date, rather than the sloppier On the agenda for today: followed by nothing.
This script could also be used on a Unix box for sending out systemwide reminders about events like backup schedules, company holidays, and employee birthdays. First, have the agenda script on each user’s machine additionally check a shared read-only .agenda file. Then add a call to the agenda script in each user’s .login or similar file that’s invoked on login.
NOTE
Rather surprisingly, date implementations vary across different Unix and Linux systems, so if you try something more complicated with your own date command and it fails, make sure to check the man page to see what your system can and cannot do.