3
CREATING UTILITIES

image

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!

#22 A Reminder Utility

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.

The Code

   #!/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

How It Works

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 .

Running the Script

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.

The Results

$ 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

Hacking the 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.

#23 An Interactive Calculator

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.

The Code

   #!/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

How It Works

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!

Running the Script

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.

The Results

$ 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.

Hacking the Script

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.

#24 Converting Temperatures

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.

The Code

   #!/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

How It Works

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:

image

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.

Running the Script

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.

The Results

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

Hacking the Script

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.

#25 Calculating Loan Payments

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.

The Code

   #!/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

How It Works

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.

Running the Script

This minimalist script expects three parameters: the amount of the loan, the interest rate, and the duration of the loan (in years).

The Results

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!

Hacking the Script

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.

#26 Keeping Track of Events

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!

The Code

   #!/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

How It Works

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!

Running the Script

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.

The Results

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

Hacking the Script

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.