-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcal
More file actions
executable file
·217 lines (194 loc) · 7.08 KB
/
cal
File metadata and controls
executable file
·217 lines (194 loc) · 7.08 KB
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
#!/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.4.20250711.0 copyright 2019+ by Adam Katz, GPL v2+"
exit
}
die() { [ $# -gt 0 ] && printf "%s\n" "$@" >&2; exit 2; }
bb() { true; } # we call this to warn about Busybox cal's limited options
if ! command -v ncal >/dev/null; then
exe="/usr/bin/cal"
if [ "$exe" != "$0" ] && [ -x "$exe" ]; then
ncal() { shift; "$exe" "$@"; }
elif command -v busybox >/dev/null && busybox cal --help >/dev/null 2>&1; then
ncal() { shift; busybox cal "$@"; }
bb() { die "Option \`-$OPT\` requires installing \`ncal\`"; }
else
die 'This script wraps either `ncal` or `busybox cal` but you lack both.' \
'Maybe try `apt install ncal` or `apt install busybox`, etc.'
fi
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 $?
}
datefrom() {
out=$(date -d "2000-$2-15" +"%$1") 2>/dev/null # GNU or Busybox date
if [ -z "$out" ]; then
datefrom() {
local out="$(date -jf %m $2 +"%$1")" 2>/dev/null || return 2 # BSD date
printf %s "$out"
}
datefrom "$@" || die 'ERROR: missing `locale` & GNU/busybox/BSD `date`'
fi
printf %s "$out"
}
# Usage: month NAME
# Extract the locale's list of months and print the number for month NAME
month() {
# POSIX requires locale, but here's a fallback using GNU/busybox/BSD date:
{ locale LC_TIME 2>/dev/null \
|| echo && for b in b B; do # %b = abmon (abbreviated) and %B = mon (full)
n=0 d='\n'
while [ $((n+=1)) -le 12 ]; do
printf "$d"
datefrom $b $n
d=';'
done
done
} |awk -v name="$1" '
BEGIN { name = tolower(name); len = length(name) } # lowercase, length
NR == 3 || NR == 4 { # abbreviated month or full month
split(tolower($0), list, ";") # split lowercased list of names
for (m=1; m<13; m++) if (name == list[m]) exit # exact match
if (len > 2) for (m=1; m<13; m++) if (1 == index(list[m], name)) exit
}
END { if (m == 13) exit 1; print m } # exit with error if we failed to match
'
}
# 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* ) bb; push -1 ;;
( 3 | three* ) bb; push -3 ;;
( 9 | nine* ) bb; push -A4 -B4 ;;
( A | after ) bb; needs_arg; push -A "$OPTARG" ;;
( B | before ) bb; needs_arg; push -B "$OPTARG" ;;
( C ) : "ignoring redundant -C" ;;
( d | date ) bb; needs_arg; push -d "$(dateas "@$OPTARG")" ;;
( 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* ) bb; 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