banner

For a list of BASHing data 2 blog posts see the index page.    RSS


Extreme reformatting: a vertical calendar

I've used the cal program for many years, especially with the -3 option, which returns the current month and the two on either side:

cal3

Once upon a time the current day was highlighted in this output, but a Debian developer decided to remove the highlighting some years ago. Something else I don't like is the side-by-side arrangement of the three months. I'd prefer a vertical calendar, like this, with the current day highlighted:

calm

This post explains how the "calm" script (below) is built. It's a bit ugly. I decided to start with ncal rather than code something from scratch, but a lot more reformatting was needed than I expected.

#!/bin/bash
 
last=$(ncal -3 | head -1 | awk '{print substr($1,0,3)}')
curr=$(ncal -3 | head -1 | awk '{print substr($3,0,3)}')
next=$(ncal -3 | head -1 | awk '{print substr($5,0,3)}')
 
temp1=$(ncal -3 | tail +2 | sed -E 's/([0-9])\x20\x20+/\1\x20\x20/g')
 
temp2=$(for i in {1..15}; do awk -v FPAT="..." -v fld="$i" '{print $fld}' <<<"$temp1" | paste -d"\0" - - - - - - -; done)
 
temp3=$(awk -v pre="$last" -v now="$curr" -v fut="$next" '/ 1 / {f=1; c1++} f && c1==1 {sub($0,$0pre);f=0} f && c1==2 {sub($0,$0now); f=0} f && c1==3 {sub($0,$0fut); f=0} 1' <<<"$temp2")
 
today=$(date +"%-d")
 
tr '\n' '@' <<<"$temp3" | sed "s/@$today \| $today /\x1b[1;31m&\x1b[0m/2" | tr '@' '\n'
 
exit 0


Here's the ncal -3 output with whitespaces made visible with spacevis:

ncal3spaces

From the output I take the first line (ncal -3 | head -1) and extract the first 3 letters of each month name with AWK (e.g., awk '{print substr($1,0,3)}'), saving the previous month's letters in the variable "last", the current month's letters in "curr" and the next month's letters in "next".

Next I take all the ncal -3 output except the first line (ncal -3 | tail +2) and "squeeze" the months together by replacing with sed any number followed by more than two spaces with the same number followed by exactly two spaces. In the sed command I've used the hexadecimal escape "\x20" to make the spaces more clearly visible in the code. The result is stored in the variable "temp1".

ncal3squeeze

If you look closely at the visible-spaces result above, you can see that each calendar line can be broken into 3-character pieces. Each 3-character piece contains either a 2-letter weekday followed by a space, or 3 spaces, or a single-digit number with a space on each side, or a 2-digit number followed by a space. Using AWK's "FPAT" variable, I define a field as 3 characters, then put the AWK command in a for loop that pulls out fields 1 to 15. Why 15? Because that's the maximum number of 3-character pieces in a line in my "squeezed" ncal horizontal calendar.

I then paste each individual field output as a 7-item string with no separator between items (paste -d"\0" - - - - - - -). Here's how that works with just one representative field, field 7:

ncal3vert1

Running the for loop converts the horizontal calendar into a vertical one, which I store as "temp2":

ncal3vert2

The next job is to append 3-letter month strings to the first week in each month. I do this again with an AWK command. First I store each of the shell-variable month strings as AWK variables: -v pre="$last" -v now="$curr" -v fut="$next". AWK then runs through the vertical calendar in "temp2" looking for a "1" with a space on each side (/ 1 /). Note that this will include end-of-line "1"s, because all the vertical calendar lines end with a space:

ncal3vert3

Each time AWK finds one of these " 1 "s it sets a flag "f" and increments a count "c1"
{f=1; c1++}). If the flag is set and the count is 1 (the first "1" in the calendar), AWK replaces the line with the whole line plus the last-month string, then turns off the flag (f && c1==1 {sub($0,$0pre);f=0}). If a similar command is applied to the next two months (counts 2 and 3), the result is just what I wanted:

ncal3vert4

This output is stored in the variable "temp3".

The last job is to highlight the current date. I get the day number with the date command, formatted to remove the leading zero on single-digit dates, and store the result in the "today" variable: today=$(date +"%-d"). That day number will occur three times in the vertical calendar, once in each month, but I only want the second occurrence (in the middle month).

To highlight that second occurrence (only) I do a trick. I first use tr to convert "temp3" into a single long line, with the character "@" replacing newlines. A sed command replaces the second occurrence of "today" in the long line with a highlighted "today", and then tr replaces all the "@" characters with newlines, restoring the vertical format:

tr '\n' '@' <<<"$temp3" | sed "s/@$today \| $today /\x1b[1;31m&\x1b[0m/2" | tr '@' '\n'

Note that single-digit day numbers will always have a space before and after in the single long line. Double-digit day numbers at the first position of the 7-day week will have a "@" in front, so sed looks for either "@$today " or " $today ":

ncal3vert5

Next post:
2025-04-25   How to add trailing spaces and zeroes


Last update: 2025-04-18
The blog posts on this website are licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License