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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
|
#!/bin/zsh
# All arguments are joined with spaces and inserted into the calendar
# file at the appropriate point.
#
# While the function compares the date of the new entry with dates in the
# existing calendar file, it does not do any sorting; it inserts the new
# entry before the first existing entry with a later date and time.
emulate -L zsh
setopt extendedglob # xtrace
local context=":datetime:calendar_add:"
local vdatefmt="%Y%m%dT%H%M%S"
local d='[[:digit:]]'
local calendar newfile REPLY lastline opt text occur
local -a calendar_entries lockfiles reply occurrences
integer my_date done rstat nolock nobackup new_recurring
integer keep_my_uid
local -A reply parse_new parse_old
local -a match mbegin mend
local my_uid their_uid
autoload -U calendar_{parse,read,lockfiles}
while getopts "BL" opt; do
case $opt in
(B)
nobackup=1
;;
(L)
nolock=1
;;
(*)
return 1
;;
esac
done
shift $(( OPTIND - 1 ))
# Read the calendar file from the calendar-file style
zstyle -s $context calendar-file calendar ||
calendar=~/calendar
newfile=$calendar.new.$HOST.$$
local addline="$*"
if ! calendar_parse $addline; then
print "$0: failed to parse date/time" >&2
return 1
fi
parse_new=("${(@kv)reply}")
(( my_date = $parse_new[time] ))
if zstyle -t $context reformat-date; then
local datefmt
zstyle -s $context date-format datefmt ||
datefmt="%a %b %d %H:%M:%S %Z %Y"
strftime -s REPLY $datefmt $parse_new[time]
addline="$REPLY $parse_new[text1]"
fi
if [[ -n $parse_new[rptstr] ]]; then
(( new_recurring = 1 ))
if [[ $parse_new[rptstr] = CANCELLED ]]; then
(( done = 1 ))
elif [[ $addline = (#b)(*[[:space:]\#]RECURRENCE[[:space:]]##)([^[:space:]]##)([[:space:]]*|) ]]; then
# Use the updated recurrence time
strftime -s REPLY $vdatefmt ${parse_new[schedrpttime]}
addline="${match[1]}$REPLY${match[3]}"
else
# Add a recurrence time
[[ $addline = *$'\n' ]] || addline+=$'\n'
strftime -s REPLY $vdatefmt ${parse_new[schedrpttime]}
addline+=" # RECURRENCE $REPLY"
fi
fi
# $calendar doesn't necessarily exist yet.
# Match a UID, a unique identifier for the entry inherited from
# text/calendar format.
local uidpat='(|*[[:space:]])UID[[:space:]]##(#b)([[:xdigit:]]##)(|[[:space:]]*)'
if [[ $addline = ${~uidpat} ]]; then
my_uid=${(U)match[1]}
fi
# start of subshell for OS file locking
(
# start of block for following always to clear up lockfiles.
# Not needed but harmless if OS file locking is used.
{
if (( ! nolock )); then
if zmodload -F zsh/system b:zsystem && zsystem supports flock &&
zsystem flock $calendar 2>/dev/null; then
# locked OK
:
else
calendar_lockfiles $calendar || exit 1
fi
fi
if [[ -f $calendar ]]; then
calendar_read $calendar
if [[ -n $my_uid ]]; then
# Pre-scan to events with the same UID
for line in $calendar_entries; do
calendar_parse $line || continue
parse_old=("${(@kv)reply}")
# Recurring with a UID?
if [[ $line = ${~uidpat} ]]; then
their_uid=${(U)match[1]}
if [[ $their_uid = $my_uid ]]; then
# Deal with recurrences and also some add some
# extra magic for cancellation.
# See if we have found a recurrent version
if [[ -z $parse_old[rpttime] ]]; then
# No, so assume this is a straightforward replacement
# of a non-recurring event.
# Is this a cancellation of a non-recurring event?
# Look for an OCCURRENCE in the form
# OCCURRENCE 20100513T110000 CANCELLED
# although we don't bother looking at the date/time---
# it's one-off, so this should already be unique.
if [[ $new_recurring -eq 0 && \
$parse_new[text1] = (|*[[:space:]\#])"OCCURRENCE"[[:space:]]##([^[:space:]]##[[:space:]]##CANCELLED)(|[[:space:]]*) ]]; then
# Yes, so skip both the old and new events.
(( done = 1 ))
fi
# We'll skip this UID when we encounter it again.
continue
fi
if (( new_recurring )); then
# Replacing a recurrence; there can be only one.
# TBD: do we replace existing occurrences of the
# replaced recurrent event? I'm guessing not, but
# if we keep the UID then maybe we should.
#
# TBD: ick, suppose we're cancelling an even that
# we added to a recurring sequence but didn't replace
# the recurrence. We might get RPT CANCELLED for this.
# That would be bad. Should invent better way of
# cancelling non-recurring event.
continue
else
# The recorded event is recurring, but the new one is a
# one-off event. If there are embedded OCCURRENCE declarations,
# use those.
#
# TBD: We could be clever about text associated
# with the occurrence. Copying the entire text
# of the meeting seems like overkill but people often
# add specific remarks about why this occurrence was
# moved/cancelled.
#
# TBD: one case we don't yet handle is cancelling
# something that isn't replacing a recurrence, i.e.
# something we added as OCCURRENCE XXXXXXXXTXXXXXX <when>.
# If we're adding a CANCELLED occurrence we should
# look to see if it matches <when> and if so simply
# delete that occurrence.
#
# TBD: one nasty case is if the new occurrence
# occurs before the current scheduled time. As we
# never look backwards we'll miss it.
text=$addline
occurrences=()
while [[ $text = (#b)(|*[[:space:]\#])"OCCURRENCE"[[:space:]]##([^[:space:]]##[[:space:]]##[^[:space:]]##)(|[[:space:]]*) ]]; do
occurrences+=($match[2])
text="$match[1] $match[3]"
done
if (( ! ${#occurrences} )); then
# No embedded occurrences. We'll manufacture one
# that doesn't refer to an original recurrence.
strftime -s REPLY $vdatefmt $my_date
occurrences=("XXXXXXXXTXXXXXX $REPLY")
fi
# Add these occurrences, checking if they replace
# an existing one.
for occur in ${(o)occurrences}; do
REPLY=${occur%%[[:space:]]*}
# Only update occurrences that refer to genuine
# recurrences.
if [[ $REPLY = [[:digit:]](#c8)T[[:digit:]](#c6) && $line = (#b)(|*[[:space:]\#])(OCCURRENCE[[:space:]]##)${REPLY}[[:space:]]##[^[:space:]]##(|[[:space:]]*) ]]; then
# Yes, update in situ
line="${match[1]}${match[2]}$occur${match[3]}"
else
# No, append.
[[ $line = *$'\n' ]] || line+=$'\n'
line+=" # OCCURRENCE $occur"
fi
done
# The line we're adding now corresponds to the
# original event. We'll skip the matching UID
# in the file below, however.
addline=$line
# We need to work out which event is next, so
# reparse.
if calendar_parse $addline; then
parse_new=("${(@kv)reply}")
(( my_date = ${parse_new[time]} ))
if zstyle -t $context reformat-date; then
zstyle -s $context date-format datefmt
strftime -s REPLY $datefmt $parse_new[time]
addline="$REPLY $parse_new[text1]"
fi
fi
fi
fi
fi
done
fi
{
for line in $calendar_entries; do
calendar_parse $line || continue
parse_old=("${(@kv)reply}")
if (( ! done && ${parse_old[time]} > my_date )); then
print -r -- $addline
(( done = 1 ))
fi
# We've already merged any information on the same UID
# with our new text, probably.
if [[ $keep_my_uid -eq 0 && -n $my_uid && $line = ${~uidpat} ]]; then
their_uid=${(U)match[1]}
[[ $my_uid = $their_uid ]] && continue
fi
if [[ $parse_old[time] -eq $my_date && $line = $addline ]]; then
(( done )) && continue # paranoia: shouldn't happen
(( done = 1 ))
fi
print -r -- $line
done
(( done )) || print -r -- $addline
} >$newfile
if (( ! nobackup )); then
if ! mv $calendar $calendar.old; then
print "Couldn't back up $calendar to $calendar.old.
New calendar left in $newfile." >&2
(( rstat = 1 ))
fi
fi
else
(( done )) || print -r -- $addline >$newfile
fi
if (( !rstat )) && ! mv $newfile $calendar; then
print "Failed to rename $newfile to $calendar.
Old calendar left in $calendar.old." >&2
(( rstat = 1 ))
fi
} always {
(( ${#lockfiles} )) && rm -f $lockfiles
}
exit $rstat
)
|