24

Don't you just love it when two commands each do one thing you want but neither do both?

This is what cal does. Nice formatting. Lacks week numbers though:

$ cal
    January 2012      
Su Mo Tu We Th Fr Sa  
 1  2  3  4  5  6  7  
 8  9 10 11 12 13 14  
15 16 17 18 19 20 21  
22 23 24 25 26 27 28  
29 30 31              

This is what ncal does. Weird formatting, but with week numbers:

$ ncal -w
    January 2012      
Su  1  8 15 22 29   
Mo  2  9 16 23 30   
Tu  3 10 17 24 31   
We  4 11 18 25      
Th  5 12 19 26      
Fr  6 13 20 27      
Sa  7 14 21 28      
    1  2  3  4  5   

The kind of output I want, actually a crossbreed between cal and ncal -w:

$ cal --magik-calendar-week-option
      January 2012      
   Su Mo Tu We Th Fr Sa  
1   1  2  3  4  5  6  7  
2   8  9 10 11 12 13 14  
3  15 16 17 18 19 20 21  
4  22 23 24 25 26 27 28  
5  29 30 31

11 Answers 11

20

If neither of these commands suit your needs you can use gcal to do what you want instead.

Example

$ gcal -K

      April 2014
 Su Mo Tu We Th Fr Sa CW
        1  2  3  4  5 13
  6  7  8  9 10 11 12 14
 13 14 15 16 17 18 19 15
 20 21 22 23 24 25 26 16
 27 28 29 30          17
                        

Prints the week number in the last column to the right.

NOTE: gcal is available in most package managers, for e.g., brew install gcal, apt install gcal, dnf install gcal, etc.

References

2
  • gcal --starting-day=Monday --with-week-number fits my needs more yet this tool is great.
    – k0pernikus
    Dec 21, 2016 at 10:48
  • apt install gcal, 3,4 mb disk size needed, however I do not see the advant. to ncal -w.
    – Timo
    Feb 26, 2021 at 14:36
10

This highlights today's date, and can display any month via $1 in the form: YYYY-mm-dd ... It defaults to today's date

It is set up to show ISO week numbers, and the first weekday being Monday.

#!/bin/bash
# Input reference date is expected in  'YYYY-mm-dd' format
#
today=($(date '+%Y %m %d')); Y=0; m=1; d=2                # establish today's date
[[ -z $1 ]] && ref=(${today[@]}) || ref=(${1//-/ })       # get input date
dNbA=$(date --date="$(date +%Y-%m-01)" +'%u')             # day-number of 1st day of reference month
today[m]=$((10#${today[m]})); ref[m]=$((10#${ref[m]}))    # remove leading zero (octal clash)
today[d]=$((10#${today[d]})); ref[d]=$((10#${ref[d]}))    # remove leading zero (octal clash)
nxtm=$(( ref[m]==12 ?1       :ref[m]+1 ))                 # month-number of next month
nxtY=$(( ref[m]==12 ?ref[Y]+1:ref[Y]   ))                 # year-number of next month
nxtA="$nxtY-$nxtm-1"                                      # date of 1st day of next month
refZ=$(date --date "$(date +$nxtA) yesterday" +%Y-%m-%d)  # date of last day of reference  month
days=$(date --date="$refZ" '+%d')                         # days in reference month

h1="$(date --date="${ref[Y]}-${ref[m]}-${ref[d]}" '+%B %Y')" # header 1 
h2="Mo Tu We Th Fr Sa Su"                                    # header 2 
printf "    %$(((${#h2}-${#h1}-1)/2))s%s\n" " " "$h1"
printf "    %s\n" "$h2"
# print week rows   
printf "%2d  " "$((10#$(date -d "$(date +${ref[Y]}-${ref[m]}-01)" +'%V')))" # week-number (of year) with suppressed leading 0
printf "%$(((dNbA-1)*3))s"  # lead spaces (before start of month)
dNbW=$dNbA  # day-number of week
dNbM=1      # day-number of month
while ((dNbM <= days)) ;do
    if (( today[Y]==ref[Y] &&  
          today[m]==ref[m] && 
          today[d]==dNbM )) ;then
        printf "\x1b[7m%2d\x1b[0m " "$dNbM" # highlight today's date 
    else
        printf "%2d " "$dNbM"
    fi
    ((dNbM++))
    if ((dNbW  >=7)) ;then
        cdate=$((10#$(date -d "$(date +${ref[Y]}-${ref[m]}-$dNbM)" +'%V'))) # remove leading zero (octal clash)
        printf "\n%2d  " "$cdate" # week-number of year
        dNbW=0
    fi
    ((dNbW++))
done
printf "%$(((8-dNbW)*3))s\n" # trailing spaces (after end of month)

Here is this month's display (with 20 highlighted)

       January 2012
    Mo Tu We Th Fr Sa Su
52                     1 
 1   2  3  4  5  6  7  8 
 2   9 10 11 12 13 14 15 
 3  16 17 18 19 20 21 22 
 4  23 24 25 26 27 28 29 
 5  30 31                
3
  • I didn't even notice the iso-issue. That will do :)
    – k0pernikus
    Jan 20, 2012 at 11:18
  • 1
    @k0pernikus: I've just modified the script.. (about 35 minutes after you posted the above comment).. It was getting caught up on 08 and 09 being interpreted as octal instead of decimal .. It should be fine now...
    – Peter.O
    Jan 20, 2012 at 12:05
  • Iso week date is shown here
    – Timo
    Feb 26, 2021 at 14:50
7

You can use nl to number the lines (that's the program purpose :). But you need to extract the first week in the month from somewhere. It can be done from ncal itself:

$ ncal -w 2 2012 | tail -1 | awk '{print $1}'
5

We insert this as a parameter to nl's option -v (starting line number), and tell it to only number lines with numbers or spaces.

$ cal 2 2012 | nl -bp'^[0-9 ]\+$' -w2 -s'  ' -v$(ncal -w 2 2012 | tail -1 | awk '{print $1}')
       February 2012
    Su Mo Tu We Th Fr Sa
 5            1  2  3  4
 6   5  6  7  8  9 10 11
 7  12 13 14 15 16 17 18
 8  19 20 21 22 23 24 25
 9  26 27 28 29

This is all awfully fragile though. Anyway, if you aren't going to need cal's more advanced options, it will work. You can put it in a file and replace "$@" where I put 2 2012.


EDIT: But this is WRONG! I just noticed that the first week in January can have number 52 or 53! So we just either have to make an exception for January, or just extract all the week numbers from ncal and apply them to the output of cal.

This is the solution I thought originally, but I thought (erroneously) I would simplify it using nl. It uses paste, which merges files side-by-side. Since there isn't any file, we have to use the bashism <(...); that's what I was trying to avoid.

Our first "file" will be a list of the week numbers, with two empty lines at the beginning:

$ printf '   \n   \n' && printf '%2d \n' $(ncal -w 1 2011 | tail -1)


52
 1
 2
 3
 4
 5

The second one, just the output of cal. All together, as parameters to paste:

$ paste -d' ' <(printf '   \n   \n' && printf '%2d \n' $(ncal -w 1 2011 | tail -1)) <(cal 1 2011)
        January 2011
    Su Mo Tu We Th Fr Sa
52                     1
 1   2  3  4  5  6  7  8
 2   9 10 11 12 13 14 15
 3  16 17 18 19 20 21 22
 4  23 24 25 26 27 28 29
 5  30 31

Much messier and incompatible that the other one. En fin...

1
  • Not so great :-/. See my edit.
    – angus
    Jan 19, 2012 at 12:01
7

Generate the weeks sequence with ncal and use paste for having both outputs side by side.

$ paste <(echo; echo; ncal -w | tail -1 | xargs -n1 printf '%2d\n') <(cal)

If you don't like having tabs as delimiters just add something like sed 's/\t/ /'


Edit: way simpler, no need to care about tabs:

$ paste -d' ' <((echo -n '   '; ncal -w | tail -1 )| fold -w 3) <(cal)
1
  • Elegant and embodies many of ESR's 17 Unix Rules. Should be the accepted answer as it truly combines the output of cal and ncal (other approaches replicate behavior of either cal or ncal).
    – bishop
    Nov 30, 2016 at 14:53
4

One way using Perl (my output language of the cal command is Spanish, but I hope result doesn't vary from English):

$ cal | perl -pe 'if ( m/\A\s*\d/ ) { s/\A/++$i . qq[ ] x 2/e } else { s/\A/qq[ ] x 3/e }'

Output:

       enero de 2012   
   lu ma mi ju vi sá do
1                     1
2   2  3  4  5  6  7  8
3   9 10 11 12 13 14 15
4  16 17 18 19 20 21 22
5  23 24 25 26 27 28 29
6  30 31

Explanation:

-pe                     # For every input line from previous pipe, execute  next
                        # instructions and print to output.
if ( m/\A\s*\d/ )       # If line begins with a digit omitting spaces...
s/\A/++$i . qq[ ] x 2/e # Insert at the beginning of the line a counter plus two spaces.
else                    # else...
s/\A/qq[ ] x 3/e        # Insert three spaces at the beginning of the line.
5
  • how will this work for subsequent months. February weeks do not start from again 1, they start from either 5/6 similarly March will be from 9/10. Jan 19, 2012 at 10:41
  • cal 02 2012, will it have this failed?? Jan 19, 2012 at 10:42
  • @Nikhil: I don't understand what you mean. Can you try to explain it more in depth? Does the script fail with cal 02 2012? It seemed to work in my test.
    – Birei
    Jan 19, 2012 at 10:52
  • @Nikhil: Ah, ok. I missunderstood the question. It means absolute week numbers and not relative to each month. I will delete my wrong answer in a while.
    – Birei
    Jan 19, 2012 at 10:55
  • 1
    cool..do not delete the answer, please keep it for reference. Jan 19, 2012 at 11:09
4
$ ncal -bw
  • -b Use oldstyle format for ncal output.
  • -w Print the number of the week below each week column.
3

There are three calendar programs. The original cal (from BSD), an ncal (in Linux) and a gcal.

All are able to print week numbers (with the right options).

The cal from util-linux 2.36.1 works with cal -w, ncal works with ncal -bw and gcal works with gcal -K:

$ cal -w
       April 2021       
   Su Mo Tu We Th Fr Sa  
14              1  2  3  
15  4  5  6  7  8  9 10  
16 11 12 13 14 15 16 17  
17 18 19 20 21 22 23 24  
18 25 26 27 28 29 30


$ ncal -bw
       April 2021          
 w| Su Mo Tu We Th Fr Sa   
13|              1  2  3   
14|  4  5  6  7  8  9 10   
15| 11 12 13 14 15 16 17   
16| 18 19 20 21 22 23 24   
17| 25 26 27 28 29 30 


$ gcal -K
     April 2021
 Su Mo Tu We Th Fr Sa CW
              1  2  3 13
  4  5  6  7  8  9 10 14
 11 12 13 14 15 16 17 15
 18 19 20 21 22 23 24 16
 25 26 27 28 29 30    17
0

Here is my solution which is shorter in code and works fine for other dates than the current month. Sorry for the US people, this uses ISO format, i.e. 1st day of week is monday. This is done with the -m option for cal and the %V option for date

#!/bin/bash
if [ "$1" = "" ]
then
  when="now"
else
  when="$1"
fi
y=$(date --date "$when" +%Y )
if [ $? -ne 0 ]
then
  exit
fi
m=$(date --date "$when" +%m )
w=$(date --date $(printf %4d%02d01 $y $m) +%V)
cal -m $m $y |
  awk -v w=$w '
    NR>2{ww=w++}
    $0!=""{printf "%2s %s\n", ww , $0}
  '
0

I did this script to generate on-duty calendar for teams. It generates calendar with week numbers and assigning names to them (simply round-robin). After output is printed you just need to copy it to excel and do "text to column" by ; and once again perform "text to column" on first column by using "fixed" option. The benefit of this calendar (comparing with horizontal orientation) is that you can use auto-filter (Alt+d f f) and find only your weeks.

#!/usr/bin/ksh
y=2017
set -A team1 Petars Justas Richard Jukka Jesper
set -A team2 Modestas Timo Mats Andreas

i1=0
i2=0
wn=1
own=1

echo "WN     Month        Mo Tu We Th Fr Sa Su ;Team1 ;Team2"
ycal=`for m in 1 2 3 4 5 6 7 8 9 10 11 12
do
IFS=
        cal -m $m $y|egrep -v "Mo|^$"|while read line
        do
                echo $line|grep -q $y && mname=$line || print $mname $line|sed 's/'$y'//'
        done
done`
IFS=
echo "$ycal"|while read line2
do
IFS=
        echo "$line2"|grep -v "1  2  3  4  5  6  7"|egrep -q "* 1 |* 1$" || let wn++
        [ $own -ne $wn ] && { \
                [ $i1 -eq ${#team1[@]}-1 ] && i1=0 || let i1++;\
                [ $i2 -eq ${#team2[@]}-1 ] && i2=0 || let i2++; }
        printf '%2s %s ;%s ;%s\n' $wn $line2 ${team1[$i1]} ${team2[$i2]}
        own=$wn
done

Sorry, code a bit nasty... and no comments... but it works well ;)

0

I don't really want to install gcal, and I could not get it to work properly on my system. So without gcal, funollet's answer is indeed the best solution. I just changed it a tiny bit so that it also preserves formatting such as day highlight, colors etc. using the script command. I also removed redundant characters, making it suitable as an alias, as such:

alias today="paste -d' ' <((echo ' '{,};ncal -w|tail -1)|fold -w3) <(script /dev/null -qc cal)"

Or replace cal with a new function. However, with this you cannot do things like cal -3. You get the correct weeks but they are not aligned properly. A simple but not complete solution is to check if cal is called without arguments, like so:

function cal {
    if [[ $# -eq 0 ]]; then
        paste -d' ' <((echo ' '{,};ncal -w $@|tail -1)|fold -w3) <(script /dev/null -qc "cal $@")
    else
        /usr/bin/cal "$@"
    fi
}
0

Update: the current version of cal supports this directly (this is on linux):

bash-5.0$ cal -w
       July 2020       
   Mo Tu We Th Fr Sa Su
27        1  2  3  4  5
28  6  7  8  9 10 11 12
29 13 14 15 16 17 18 19
30 20 21 22 23 24 25 26
31 27 28 29 30 31      
                       
bash-5.0$ cal --version
cal from util-linux 2.35.2
1
  • Not supported in Ubuntu 20.04 wolf@linux:~$ cal -w cal -w Usage: cal [general options] [-jy] [[month] year] cal [general options] [-j] [-m month] [year] ncal -C [general options] [-jy] [[month] year] ncal -C [general options] [-j] [-m month] [year] ncal [general options] [-bhJjpwySM] [-H yyyy-mm-dd] [-s country_code] [[month] year] ncal [general options] [-bhJeoSM] [year] General options: [-31] [-A months] [-B months] [-d yyyy-mm] wolf@linux:~$
    – Wolf
    Jan 2, 2021 at 14:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .