A shell script for blank calendars

Generally speaking, I dislike writing shell scripts. The operators are cryptic (yes, I’ve enjoyed writing Perl), whitespace matters way more than it should (yes, I’ve enjoyed writing Python), and I just always feel I’m one step away from disaster. A lot of my scripts start out as shell scripts but get changed into Perl or Python once they get more than a few lines long. The script we’ll discuss below is an exception.

I wanted a script to help me print out blank monthly calendars. The program I’ve always used for this is pcal, which is pretty easy to use. For example,

pcal -e -S 10 2023 3

will create a three monthly calendars starting with this coming October. The -e option tell pcal to make empty calendars,1 and the -S tells it not to include mini-calendars for the preceding and succeeding months.2 The result looks like this:

Blank October calendar

The thing about pcal is that the p stands for PostScript, a great file format but one that’s been superseded3 by PDF. So to get pcal’s output into a more modern format, I pipe its output to ps2pdf:

pcal -e -S 10 2023 3 | ps2pdf - -

The first hyphen tells ps2pdf to get the PostScript from standard input and the second hyphen tells it to write the resulting PDF to standard output. Of course, I really don’t want the PDF code spewing out into my Terminal, so I use Apple’s very handy open command to pipe it into Preview:

pcal -e -S 10 2023 3 | ps2pdf -  - | open -f -a Preview

The -f option tells open to take what being piped in through standard input and the -a Preview tells it to open that content in the Preview application.

This isn’t the most complicated command pipeline in the world, but I have trouble remembering both the -S option and the order of the month, year, and count arguments. So I decided to whip up a quick little shell script to replace my faulty memory.

You should know first that my main use of this command is to print a few upcoming months for my wife. She’s always preferred paper calendars but decided last December that 2023 would be different, so I didn’t get a 2023 calendar for her for Christmas. Partway through the year, she changed her mind. There’s a lot less selection for calendars in spring, and it would kill her to waste money on a full year when she’d only use eight months, so she asked me to print her a few months at a time.

My first thought was to make a script that takes just two arguments: the starting month and the number of months—I could have the script figure out the year. That thinking led to this simple script, which I called bcal:

1:  #!/usr/bin/env bash
3:  y=$(date +%Y)
4:  pcal -e -S $1 $y $2 | ps2pdf -  - | open -f -a Preview

This worked fine, but you’ve probably already seen the problem. What happens at the end of the year, when it’s December and she wants calendars for the first few months of the following year?

I could use date to get the current month, date +%m, and if it’s 12, add one to $y. But what if I wanted to print out the upcoming January calendar in November? Instead of trying to have the program guess what I wanted, it seemed better for me to tell it what I wanted. That meant adding an option to bcal to let me tell it I wanted next year instead of this year.

At this point, I was tempted to give up on bash and move to Python. I know how to handle options, dates, and external calls in Python, so the switch would have been fairly easy. But I had an itch to learn how to do options in bash. Couldn’t be too hard, could it?

It wasn’t. The key command is getopts, and it’s easy to find examples of its use. And once I had getopts working, I expanded the script to add a help/usage message and one bit of error handling. Here’s the final version of bcal:

 1:  #!/usr/bin/env bash
 3:  # Make PDF file with blank calendar starting on month of first argument
 4:  # and continuing for second argument months
 6:  usage="Usage: bcal [-n] m c
 7:  Arguments:
 8:    m  starting month number (defaults to this year)
 9:    c  count of months to print
10:  Option:
11:    -n  use next year instead of this year"
13:  # Current year
14:  y=$(date +%Y)
16:  # If user asks for next year (-n), add one to the year
17:  while getopts "nh" opt; do
18:    case ${opt} in
19:      n) y=$((y + 1));;
20:      h) echo "$usage"; exit 0;;
21:      ?) echo "$usage"; exit 1;;
22:    esac
23:  done
25:  # Skip over any options to the required arguments
26:  shift $(($OPTIND - 1))
28:  # Exit with usage message if there aren't two arguments
29:  if (($# < 2)); then
30:    echo "Needs two arguments"
31:    echo "$usage"
32:    exit 1
33:  fi
35:  # Make the calendar, convert to PDF, and open in Preview
36:  pcal -e -S $1 $y $2 | ps2pdf -  - | open -f -a Preview

Lines 17–23 handle the options. I decided on -n as the option for “next year” and you can see in the case statement that giving that option adds one to the current year. Any other options lead to the usage message and a halt to the script.

Line 26 uses shift to skip over the options to the required arguments. $OPTIND is the option index, which gets increased by one with each option processed by getopts, so this command makes $1 point to the month and $2 point to the count, just as if there were no options.

The error handling in Lines 29–33 is limited to just making sure there are two required arguments. If the arguments are letters or negative numbers, the script will continue through this section and fail in a clumsy way. I’m not especially worried about that because this is a script for me, and I’m unlikely to invoke it as bcal hello world.

Anyway, now I can get the next three months with

bcal 10 3

and the first two months of next year with

bcal -n 1 2

When Preview opens, it shows me a temporary file.

Calendar in Preview

Usually I just print it out and the temporary file is deleted when I quit Preview. This is the nice thing about piping into open: the script doesn’t create any files that I have to clean up later. But I can save the file if I think there’s a need to.

I should mention that pcal can be installed through Homebrew, and ps2pdf is typically installed as part of the Ghostscript suite, which is also in Homebrew.

Now that I kind of know how to use getopts, I’ll probably extend my shell scripts before bailing out to Perl or Python. I’m not sure that’s a good thing.

  1. By default, pcal looks in your home directory for a file named .calendar and parses it to print entries on the appropriate days. Back when I was a Linux user, this was how I kept track of my calendar. Whenever I added a new entry, I’d print out an updated calendar on the back of a sheet I pulled out of the recycling bin. It worked pretty well in those pre-smartphone days. 

  2. English has more spelling anomalies than there are stars in the sky, but right now the one that’s bothering me the most is that succeeding has a doubled E and preceding doesn’t. 

  3. No doubled E!