-
Notifications
You must be signed in to change notification settings - Fork 2
/
cal
executable file
·219 lines (196 loc) · 7.55 KB
/
cal
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/bin/sh
help() { cat <<'/help'
Wrapper for cal supporting month names, 9mo view, and relative/arbitrary dates
Usage: cal [OPTIONS] [+][-][MONTH] [+][-][YEAR]
-1, --one Display only the target month (default)
-3, --three Display a row of the prior, target, and next months
-9, --nine Display 3 rows centered on the target (like `-A 4 -B 4`)
-A, --after=NUM Show this many months after the target month
-B, --before=NUM Show this many months before the target month
-d, --date=DATE Change the target date from today to DATE (in any format)
-j, --julian Display days of the year (Julian days), e.g. Feb 1 = 32
-m, --month=MONTH Display the specified month. Aborts the wrapper.
-W, --week=MIN The first week of the year must contain MIN days
If just given a month, shows calendar for closest instance of given month.
When a month or year is prefixed with + or - or +-, it is considered an offset
relative to the current date.
/help
version
}
self="${0##*/}"
url='https://github.com/adamhotep/misc-scripts'
version() {
echo "Part of misc-scripts: $url"
echo "cal-wrapper 0.3.20240806.0 copyright 2019+ by Adam Katz, GPL v2+"
exit
}
die() { [ $# -gt 0 ] && printf "%s\n" "$@" >&2; exit 2; }
if ! command -v ncal >/dev/null; then
die 'You do not appear to have the `ncal` program that this wraps.' \
'Maybe try `apt install ncal` or something similar?'
fi
run() {
while [ -n "$arr" ]; do # split (pop) options into $@
OPT="${arr##*$sep}"
arr="${arr%"$OPT"}"
arr="${arr%"$sep"}"
set -- "$OPT" "$@"
done
ncal -C "$@" ${month:+"$month"} ${year:+"$year"}
exit $?
}
# Usage: normalize STRING
# Convert Unicode to ASCII and lowercase the input
normalize() {
local non='[!0-9A-Za-z]'
# While I created https://stackoverflow.com/a/78790871/519360 for POSIX awk
# to properly lowercase Unicode text, converting to ASCII is preferable here.
# We only need to throw an error if non-ASCII-letters are to be analyzed.
case "$1" in
( $non* | ?$non* | ??$non* | ???$non* )
if ! command -v utf2ascii >/dev/null; then
die "utf2ascii is needed for non-ASCII letters" "Get it at: $url"
fi
echo "$*" |utf2ascii
;;
( * ) echo "$*" ;;
esac |tr '[:upper:]' '[:lower:]'
}
# Usage: month NAME
# Prints the number associated with month NAME. (This is locale-aware.)
month() {
# afaict, LC_ALL trumps LANG which trumps LC_TIME, so we read in that order.
local m="$(normalize "$1")" lang=${LC_ALL:-$LANG}
if [ -z "$lang" ]; then lang="${LC_TIME:-en_US.UTF-8}"; fi
# Yes, cal truncates: `cal -m junk` works with FreeBSD 11.2 & ncal 12.1.8
# Shortcut: we always accept certain common abbreviations, incl all English.
# Abbreviations from https://web.library.yale.edu/cataloging/months#page-title
# Supported: en de es fr it pl pt ro (note how they're alphabetical)
case "${lang%%_*}:$m" in
# NOTE: this is after converting Unicode to ASCII -- no accents here!
( *:jan* | es:ene* | it:gen* | pl:sty* | ro:ian* ) m=1 ;;
( *:feb* | fr:fev* | pl:lut* | pt:fev* ) m=2 ;;
( *:mar* ) m=3 ;;
( *:apr* | es:abr* | fr:avr* | pl:kwi* | pt:abr* ) m=4 ;;
( *:may* | *:mai* | it:mag* | pl:maj* ) m=5 ;;
( *:jun* | fr:juin* | it:giu* | pl:cze* | ro:iun* ) m=6 ;;
( *:jul* | fr:juil* | it:lug* | pl:lip* | ro:iul* ) m=7 ;;
( *:aug* | es:ago* | fr:aou* | it:ag* | pl:sie* | pt:ago* ) m=8 ;;
( *:sep* | it:set* | pl:wrz* | pt:set* ) m=9 ;;
( *:oct* | de:okt* | it:ott* | pl:paz* | pt:out* ) m=10 ;;
( *:nov* | pl:lis* | ro:noi* ) m=11 ;;
( *:dec* | de:dez* | es:dic* | it:dic* | pl:gru* | pt:dez* ) m=12 ;;
( * ) [ ! "$m" -le 12 ] 2>/dev/null && die "$self: invalid month '$1'" ;;
esac
echo "$m"
}
# Usage: dateas [DATE_OPTIONS] [+DATESPEC]
# Convert the date (trumped by "$now") to DATESPEC format
# With BSD date, DATESTRING must itself use DATESPEC +%Y-%m-%d, e.g. 2024-07-28
dateas() {
local ymd="%Y-%m-%d" tmp="${1#@}" now
if [ "$1" != "$tmp" ]; then
now="$tmp"
shift
fi
if [ $# = 0 ]; then set -- +"$ymd"; fi
if [ -n "$now" ]; then
# GNU date (any format) or else BSD date (YYYY-MM-DD only)
date -d "$now" "$@" 2>/dev/null || date -jf "$ymd" "$now" "$@"
else
date "$@"
fi
}
if [ $# = 0 ]; then
run
fi
sep="\`$TTY\`$$\`"
arr=""
push() { local i; for i in "$@"; do arr="$arr${arr:+$sep}$i"; done; }
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
abort=
month=
year=
# official options disallowed with `ncal -C`: b e h(!) H(!) J M o p s S w W
while getopts 139A:B:Cd:hjm:vVW:y-: OPT; do
if [ "$OPT" = - ]; then # --long: https://stackoverflow.com/a/28466267/519360
OPT="${OPTARG%%=*}" OPTARG="${OPTARG#"$OPT"}" OPTARG="${OPTARG#=}"
fi
case "$OPT" in
( 1 | one* ) push -1 ;;
( 3 | three* ) push -3 ;;
( 9 | nine* ) push -A4 -B4 ;;
( A | after ) needs_arg; push -A "$OPTARG" ;;
( B | before ) needs_arg; push -B "$OPTARG" ;;
( C ) : "ignoring redundant -C" ;;
( d | date ) needs_arg; now="$(dateas "@$OPTARG")"; push -d "$now" ;;
( h | help ) help ;; # note, the other -h is somehow just for ncal
( j | julian ) push -j ;;
( m | month) abort=abort; push -m "$OPTARG" ;; # bypass script
( [Vv] | ver* ) version ;;
( W | week-min* ) needs_arg; push -W "$OPTARG" ;;
( y | year* ) push -y ;;
( \? ) die ;; # bad short option (error reported via getopts)
( * ) die "Illegal option --$OPT" ;; # bad long option
esac
done
shift $((OPTIND-1))
if [ "$abort" = abort ]; then
run "$@"
fi
# Parse remaining parameters into $month and $year
abs="${1#+}"
if [ $# -gt 2 ]; then # too many arguments
m="$1" y="$2"
shift 2
die "Error: unexpected arguments after month \`$m\` and year \`$y\`: $*"
elif [ -n "$1" ] && ! [ "${abs#-}" -gt 0 ] 2>/dev/null; then # non-numeric month
month="$(month "$1")" year="$2"
elif [ $# = 2 ]; then
month="${1#0}" year="$2" # remove month's leading zero if there is one
elif [ $# = 1 ]; then
if [ "$abs" != "${1#-}" ]; then
month="$1"
else
year="$1"
fi
fi
# year by offset or as 2-digit abbrev (no conflicts: ncal only supports year 1+)
case "$year" in
( [+-]*[!0-9]* ) die "$self: not a valid year '$year'" ;;
( [+-]* ) year=$((${year#+} + $(dateas +%Y))) ;; # year by offset
( \'[0-9][0-9] | [0-9][0-9] ) # 2-digit year
tmp="$(dateas +%Y%j)" # %j is padded to 3 digits, so Feb 1 = 032
year="${tmp%?????}${year#\'}" # use current century for given year
if [ $((tmp + 50183)) -le ${year}000 ]; then # if (now + 50.5y <= $year)
year=$((year - 100))
elif [ $(( ${year}000 + 50000 )) -lt $tmp ]; then
year=$((year + 100))
fi
;;
esac
# Relative month
if [ "$month" != "${month#[+-]}" ]; then # month by offset
# The leading 1 adds 100 to guard against octal interpretation. We then add
# the offset and subtract that 100 and one to zero-index, so Jan=0, Dec=11.
month=$(( 1$(dateas +%m) + ${month#+} - 101 ))
if [ $month -gt 11 ]; then
year=$((year + (month + 12)/12))
elif [ $month -lt 0 ]; then
year=$((year + (month - 12)/12))
fi
# -1 % 12 = -1 so we pad with +10,000y of months: (120000 + -1) % 12 = 11
month=$(( (120000 + month) % 12 + 1 )) # pad, modulo, convert to one-index
fi
if [ -z "$year" ] && [ -n "$month" ]; then # no year: use the closest to $month
now="$(dateas +%Y%m)"
year="${now%%??}" # current year
now_month="${now#????}" # current month
now_month="${now_month#0}" # remove zero-padding (no octal!)
if [ $((month + 6)) -lt $now_month ]; then
year=$((year + 1))
elif [ $((now_month + 6)) -lt $month ]; then
year=$((year - 1))
fi
fi
run