1
0
Fork 0
mirror of git://git.code.sf.net/p/zsh/code synced 2025-10-23 16:40:24 +02:00

c.f. 23023: new calendar function system.

This commit is contained in:
Peter Stephenson 2006-12-01 10:23:06 +00:00
parent ab8b8026dc
commit 6b1b34d1da
23 changed files with 1787 additions and 8 deletions

View file

@ -0,0 +1,6 @@
DISTFILES_SRC='
.distfiles
age
calendar calendar_add calendar_lockfiles calendar_read
calendar_scandate calendar_show calendar_sort
'

73
Functions/Calendar/age Normal file
View file

@ -0,0 +1,73 @@
# Match the age of a file, for use as a glob qualifer. Can
# take one or two arguments, which can be supplied by one of two
# ways (always the same for both arguments):
#
# print *(e:age 2006/10/04 2006/10/09:)
#
# Match all files modified between the start of those dates.
#
# print *(e:age 2006/10/04)
#
# Match all files modified on that date. If the second argument is
# omitted it is taken to be exactly 24 hours after the first argument
# (even if the first argument contains a time).
#
# print *(e:age 2006/10/04:10:15 2006/10/04:10:45)
#
# Supply times. All the time and formats handled by calendar_scandate
# are allowed, but whitespace must be quoted to ensure age receives
# the correct arguments.
#
# AGEREF1=2006/10/04:10:15
# AGEREF2=2006/10/04:10:45
# print *(+age)
#
# The same example using the other form of argument passing. The
# dates stay in effect until unset, but will be overridden if
# any argument is passed in the first format.
emulate -L zsh
integer mystat disable_stat
zmodload -i zsh/stat
# Allow the builtin stat to be hidden.
zmodload -i zsh/parameter
if [[ $builtins[stat] != defined ]]; then
(( disable_stat = 1 ))
enable stat
fi
autoload -U calendar_scandate
local -a vals
[[ -e $REPLY ]] || return 1
stat -A vals +mtime $REPLY || return 1
if (( $# >= 1 )); then
local AGEREF=$1
# if 1 argument given, never use globally defined AGEREF2
local AGEREF2=$2
fi
integer mtime=$vals[1] date1 date2
local REPLY
if calendar_scandate $AGEREF; then
date1=$REPLY
if [[ -n $AGEREF2 ]] && calendar_scandate $AGEREF2; then
date2=$REPLY
else
(( date2 = date1 + 24 * 60 * 60 ))
fi
(( date1 <= mtime && mtime <= date2 ))
else
mystat=1
fi
# If the builtin stat was previously disabled, disable it again.
(( disable_stat )) && disable stat
return $mystat

356
Functions/Calendar/calendar Normal file
View file

@ -0,0 +1,356 @@
emulate -L zsh
setopt extendedglob
# standard ctime date/time format
local ctime="%a %b %d %H:%M:%S %Z %Y"
local line REPLY REPLY2 userange pruned
local calendar donefile sched newfile warnstr mywarnstr
integer time start stop today ndays y m d next=-1 shown done nodone
integer verbose warntime mywarntime t tsched i rstat remaining
integer showcount icount
local -a calendar_entries
local -a times calopts showprog lockfiles match mbegin mend
zmodload -i zsh/datetime || return 1
zmodload -i zsh/zutil || return 1
autoload -U calendar_{read,scandate,show,lockfiles}
# Read the calendar file from the calendar-file style
zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
newfile=$calendar.new.$HOST.$$
zstyle -s ':datetime:calendar:' done-file donefile || donefile="$calendar.done"
# Read the programme to show the message from the show-prog style.
zstyle -a ':datetime:calendar:' show-prog showprog ||
showprog=(calendar_show)
# Amount of time before an event when it should be flagged.
# May be overridden in individual entries
zstyle -s ':datetime:calendar:' warn-time warnstr || warnstr="0:05"
if [[ -n $warnstr ]]; then
if [[ $warnstr = <-> ]]; then
(( warntime = warnstr ))
elif ! calendar_scandate -ar $warnstr; then
print >&2 \
"warn-time value '$warnstr' not understood; using default 5 minutes"
warnstr="5 mins"
(( warntime = 5 * 60 ))
else
(( warntime = REPLY ))
fi
fi
[[ -f $calendar ]] || return 1
# We're not using getopts because we want +... to refer to a
# relative time, not an option, and allow some other additions
# like handling -<->.
integer opti=0
local opt optrest optarg
while [[ ${argv[opti+1]} = -* ]]; do
(( opti++ ))
opt=${argv[opti][2]}
optrest=${argv[opti][3,-1]}
[[ -z $opt || $opt = - ]] && break
while [[ -n $opt ]]; do
case $opt in
########################
# Options with arguments
########################
([CnS])
if [[ -n $optrest ]]; then
optarg=$optrest
optrest=
elif (( opti < $# )); then
optarg=$argv[++opti]
optrest=
else
print -r "$0: option -$opt requires an argument." >&2
return 1
fi
case $opt in
(C)
# Pick the calendar file, overriding style and default.
calendar=$optarg
;;
(n)
# Show this many remaining events regardless of date.
showcount=$optarg
if (( showcount <= 0 )); then
print -r "$0: option -$opt requires a positive integer." >&2
return 1
fi
;;
(S)
# Explicitly specify a show programme, overriding style and default.
# Colons in the argument are turned into space.
showprog=(${(s.:.)optarg})
;;
esac
;;
###########################
# Options without arguments
###########################
(d)
# Move out of date items to the done file.
(( done = 1 ))
;;
(D)
# Don't use done; needed with sched
(( nodone = 1 ))
;;
(r)
# Show all remaining options in the calendar, i.e.
# respect start time but ignore end time.
# Any argument is treated as a start time.
(( remaining = 1 ))
;;
(s)
# Use the "sched" builtin to scan at the appropriate time.
sched=sched
(( done = 1 ))
;;
(v)
# Verbose
verbose=1
;;
(<->)
# Shorthand for -n <->
showcount=$opt
;;
(*)
print "$0: unrecognised option: -$opt" >&2
return 1
;;
esac
opt=$optrest[1]
optrest=$optrest[2,-1]
done
done
calopts=($argv[1,opti])
shift $(( opti ))
# Use of donefile requires explicit or implicit option request, plus
# no explicit -D. It may already be empty because of the style.
(( done && !nodone )) || donefile=
if (( $# > 1 || ($# == 1 && remaining) )); then
if [[ $1 = now ]]; then
start=$EPOCHSECONDS
elif [[ $1 = <-> ]]; then
start=$1
else
if ! calendar_scandate -a $1; then
print "$0: failed to parse date/time: $1" >&2
return 1
fi
start=$REPLY
fi
shift
else
# Get the time at which today started.
y=${(%):-"%D{%Y}"} m=${(%):-"%D{%m}"} d=${(%):-"%D{%d}"}
strftime -s today -r "%Y/%m/%d" "$y/$m/$d"
start=$today
fi
# day of week of start time
strftime -s wd "%u" $start
if (( $# && !remaining )); then
if [[ $1 = +* ]]; then
if ! calendar_scandate -ar ${1[2,-1]}; then
print "$0: failed to parse relative time: $1" >&2
return 1
fi
(( stop = start + REPLY ))
elif [[ $1 = <-> ]]; then
stop=$1
else
if ! calendar_scandate -a $1; then
print "$0: failed to parse date/time: $1" >&2
return 1
fi
stop=$REPLY
fi
if (( stop < start )); then
strftime -s REPLY $ctime $start
strftime -s REPLY2 $ctime $stop
print "$0: requested end time is before start time:
start: $REPLY
end: $REPLY2" >&2
return 1
fi
shift
else
# By default, show 2 days. If it's Friday (5) show up to end
# of Monday (4) days; likewise on Saturday show 3 days.
# If -r, this is calculated but not used. This is paranoia,
# to avoid an unusable value of stop; but it shouldn't get used.
case $wd in
(5)
ndays=4
;;
(6)
ndays=3
;;
(*)
ndays=2
;;
esac
stop=$(( start + ndays * 24 * 60 * 60 ))
fi
if (( $# )); then
print "Usage: $0 [ start-date-time stop-date-time ]" >&2
return 1
fi
autoload -Uz matchdate
[[ -n $donefile ]] && rm -f $newfile
if (( verbose )); then
print -n "start: "
strftime $ctime $start
print -n "stop: "
if (( remaining )); then
print "none"
else
strftime $ctime $stop
fi
fi
# start of block for following always to clear up lockfiles.
{
if [[ -n $donefile ]]; then
# Attempt to lock both $donefile and $calendar.
# Don't lock $newfile; we've tried our best to make
# the name unique.
calendar_lockfiles $calendar $donefile || return 1
fi
calendar_read $calendar
for line in $calendar_entries; do
# This call sets REPLY to the date and time in seconds since the epoch,
# REPLY2 to the line with the date and time removed.
calendar_scandate -as $line || continue
(( t = REPLY ))
# Look for specific warn time.
pruned=${REPLY2#(|*[[:space:],])WARN[[:space:]]}
(( mywarntime = warntime ))
mywarnstr=$warnstr
if [[ $pruned != $REPLY2 ]]; then
if calendar_scandate -ars $pruned; then
(( mywarntime = REPLY ))
mywarnstr=${pruned%%"$REPLY2"}
fi
fi
if (( verbose )); then
print "Examining: $line"
print -n " Date/time: "
strftime $ctime $t
if [[ -n $sched ]]; then
print " Warning $mywarntime seconds ($mywarnstr) before"
fi
fi
(( shown = 0 ))
if (( t >= start && (remaining || t <= stop || icount < showcount) ))
then
$showprog $start $stop "$line"
(( shown = 1, icount++ ))
elif [[ -n $sched ]]; then
(( tsched = t - mywarntime ))
if (( tsched >= start && tsched <= stop)); then
$showprog $start $stop "due in ${mywarnstr}: $line"
fi
fi
if [[ -n $sched ]]; then
if (( t - mywarntime > EPOCHSECONDS )); then
# schedule for a warning
(( tsched = t - mywarntime ))
else
# schedule for event itself
(( tsched = t ))
fi
if (( (tsched > EPOCHSECONDS || ! shown) &&
(next < 0 || tsched < next) )); then
(( next = tsched ))
fi
fi
if [[ -n $donefile ]]; then
if (( t <= EPOCHSECONDS && shown )); then
# Done and dusted.
# TODO: handle repeated times from REPLY2.
if ! print -r $line >>$donefile; then
if (( done != 3 )); then
(( done = 3 ))
print "Failed to append to $donefile" >&2
fi
elif (( done != 3 )); then
(( done = 2 ))
fi
else
# Still not over.
if ! print -r $line >>$newfile; then
if (( done != 3 )); then
(( done = 3 ))
print "Failed to append to $newfile" >&2
fi
elif (( done != 3 )); then
(( done = 2 ))
fi
fi
fi
done
if [[ -n $sched ]]; then
if [[ $next -ge 0 ]]; then
# Remove any existing calendar scheduling.
# Luckily sched doesn't delete its schedule in a subshell.
sched | while read line; do
if [[ $line = (#b)[[:space:]]#(<->)[[:space:]]##*[[:space:]]'calendar -s'* ]]; then
# End of pipeline run in current shell, so delete directly.
sched -1 $match[1]
fi
done
$sched $next calendar "${calopts[@]}" $next $next
else
$showprog $start $stop \
"No more calendar events: calendar not rescheduled.
Run \"calendar -s\" again if you add to it."
fi
fi
if (( done == 2 )); then
if ! mv $calendar $calendar.old; then
print "Couldn't back up $calendar to $calendar.old.
New calendar left in $newfile." >&2
(( rstat = 1 ))
elif ! mv $newfile $calendar; then
print "Failed to rename $newfile to $calendar.
Old calendar left in $calendar.old." >&2
(( rstat = 1 ))
fi
elif [[ -n $donefile ]]; then
rm -f $newfile
fi
} always {
(( ${#lockfiles} )) && rm -f $lockfiles
}
return $rstat

View file

@ -0,0 +1,69 @@
#!/bin/env 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
local calendar newfile REPLY lastline
local -a calendar_entries lockfiles
integer newdate done rstat
autoload -U calendar_{read,lockfiles}
# Read the calendar file from the calendar-file style
zstyle -s ':datetime:calendar_add:' calendar-file calendar ||
calendar=~/calendar
newfile=$calendar.new.$HOST.$$
if ! calendar_scandate -a "$*"; then
print "$0: failed to parse date/time" >&2
return 1
fi
(( newdate = $REPLY ))
# $calendar doesn't necessarily exist yet.
# start of block for following always to clear up lockfiles.
{
calendar_lockfiles $calendar || return 1
if [[ -f $calendar ]]; then
calendar_read $calendar
{
for line in $calendar_entries; do
if (( ! done )) && calendar_scandate -a $line && (( REPLY > newdate )); then
print -r -- "$*"
(( done = 1 ))
elif [[ $REPLY -eq $newdate && $line = "$*" ]]; then
(( done = 1 ))
fi
print -r -- $line
done
(( done )) || print -r -- "$*"
} >$newfile
if ! mv $calendar $calendar.old; then
print "Couldn't back up $calendar to $calendar.old.
New calendar left in $newfile." >&2
(( rstat = 1 ))
fi
else
print -r -- $line >$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
}
return $rstat

View file

@ -0,0 +1,43 @@
# Lock the given files.
# Append the names of lockfiles to the array lockfiles.
local file lockfile msgdone
# Number of attempts to lock a file. Probably not worth stylising.
integer lockattempts=3
# The lockfile name is not stylised: it has to be a fixed
# derivative of the main fail.
for file; do
lockfile=$file.lockfile
for (( i = 0; i < lockattempts; i++ )); do
if ln -s $file $lockfile >/dev/null 2>&1; then
lockfiles+=($lockfile)
break
fi
if zle && [[ -z $msgdone ]]; then
msgdone="${lockfile}: waiting to acquire lock"
zle -M $msgdone
fi
sleep 1
done
if [[ -n $msgdone ]]; then
zle -M ${msgdone//?/ }
msgdone=
fi
if [[ ${lockfiles[-1]} != $lockfile ]]; then
msgdone="Failed to lock $file; giving up after $lockattempts attempts.
Another instance of calendar may be using it.
Delete $lockfiles if you believe this to be an error."
if zle; then
zle -M $msgdone
else
print $msgdone >&2
fi
# The parent should take action to delete any lockfiles
# already locked. Typically this won't be necessary, since
# we will always lock the main calendar file first.
return 1
fi
done
return 0

View file

@ -0,0 +1,35 @@
# Utility for "calendar" to read entries into the array calendar_entries.
# This should be local to the caller.
# The only argument is the file to read. We expect options etc. to
# be correct.
#
# This is based on Emacs calendar syntax, which has two implications:
# - Lines beginning with whitespace are continuation lines.
# Hence we have to read the entire file first to determine entries.
# - Lines beginning with "&" are inhibited from producing marks in
# Emacs calendar window. This is irrelevant to us, so we
# we simply remove leading ampersands. This is necessary since
# we expect the date to start at the beginning of the line.
#
# TODO: Emacs has some special handling for entries where the first line
# has only the date and continuation lines indicate times. Actually,
# it doesn't parse the times as far as I can see, but if we want to
# handle that format sensibly we would need to here. It could
# be tricky to get right.
local calendar=$1 line
local -a lines
lines=(${(f)"$(<$calendar)"})
calendar_entries=()
# ignore blank lines
for line in $lines; do
if [[ $line = [[:space:]]* ]]; then
if (( ${#calendar_entries} )); then
calendar_entries[-1]+=$'\n'$line
fi
else
calendar_entries+=(${line##\&})
fi
done

View file

@ -0,0 +1,519 @@
# Scan a line for various common date and time formats.
# Set REPLY to the number of seconds since the epoch at which that
# time occurs. The time does not need to be matched; this will
# produce midnight at the start of the date.
#
# Absolute times
#
# The rules below are fairly complicated, to allow any natural (and
# some highly unnatural but nonetheless common) combination of
# time and date used by English speakers. It is recommended that,
# rather than exploring the intricacies of the system, users find
# a date format that is natural to them and stick to it. This
# will avoid unexpected effects. Various key facts should be noted,
# explained in more detail below:
#
# - In particular, note the confusion between month/day/year and
# day/month/year when the month is numeric; this format should be
# avoided if at all possible. Many alternatives are available.
# - However, there is currently no localization support, so month
# names must be English (though only the first three letters are required).
# The same applies to days of the week if they occur (they are not useful).
# - The year must be given in full to avoid confusion, and only years
# from 1900 to 2099 inclusive are matched.
# - Although timezones are parsed (complicated formats may not be recognized),
# they are then ignored; no time adjustment is made.
#
# The following give some obvious examples; users finding here
# a format they like and not subject to vagaries of style may skip
# the full description. As dates and times are matched separately
# (even though the time may be embedded in the date), any date format
# may be mixed with any format for the time of day provide the
# separators are clear (whitespace, colons, commas).
# 2007/04/03 13:13
# 2007/04/03:13:13
# 2007/04/03 1:13 pm
# 3rd April 2007, 13:13
# April 3rd 2007 1:13 p.m.
# Apr 3, 2007 13:13
# Tue Apr 03 13:13:00 2007
# 13:13 2007/apr/3
#
# Times are parsed and extracted before dates. They must use colons
# to separate hours and minutes, though a dot is allowed before seconds
# if they are present. This limits time formats to
# HH:MM[:SS[.FFFFF]] [am|pm|a.m.|p.m.]
# HH:MM.SS[.FFFFF] [am|pm|a.m.|p.m.]
# in which square brackets indicate optional elements, possibly with
# alternatives. Fractions of a second are recognised but ignored.
# Unless -r is given (see below), a date is mandatory but a time of day is
# not; the time returned is at the start of the date.
#
# Time zones are not handled, though if one is matched following a time
# specification it will be removed to allow a surrounding date to be
# parsed. This only happens if the format of the timezone is not too
# wacky:
# +0100
# GMT
# GMT-7
# CET+1CDT
# etc. are all understood, but any part of the timezone that is not numeric
# must have exactly three capital letters in the name.
#
# Dates suffer from the ambiguity between DD/MM/YYYY and MM/DD/YYYY. It is
# recommended this form is avoided with purely numeric dates, but use of
# ordinals, eg. 3rd/04/2007, will resolve the ambiguity as the ordinal is
# always parsed as the day of the month. Years must be four digits (and
# the first two must be 19 or 20); 03/04/08 is not recognised. Other
# numbers may have leading zeroes, but they are not required. The
# following are handled:
# YYYY/MM/DD
# YYYY-MM-DD
# YYYY/MNM/DD
# YYYY-MNM-DD
# DD[th|st|rd] MNM[,] YYYY
# DD[th|st|rd] MNM[,] current year assumed
# MNM DD[th|st|rd][,] YYYY
# MNM DD[th|st|rd][,] current year assumed
# DD[th|st|rd]/MM[,] YYYY
# DD[th|st|rd]/MM/YYYY
# MM/DD[th|st|rd][,] YYYY
# MM/DD[th|st|rd]/YYYY
# Here, MNM is at least the first three letters of a month name,
# matched case-insensitively. The remainder of the month name may appear but
# its contents are irrelevant, so janissary, febrile, martial, apricot,
# etc. are happily handled.
#
# Note there are only two cases that assume the current year, the
# form "Jun 20" or "14 September" (the only two commonly occurring
# forms, apart from a "the" in some forms of English, which isn't
# currently supported). Such dates will of course become ambiguous
# in the future, so should ideally be avoided.
#
# Times may follow dates with a colon, e.g. 1965/07/12:09:45; this
# is in order to provide a format with no whitespace. A comma
# and whitespace are allowed, e.g. "1965/07/12, 09:45".
# Currently the order of these separators is not checked, so
# illogical formats such as "1965/07/12, : ,09:45" will also
# be matched. Otherwise, a time is only recognised as being associated
# with a date if there is only whitespace in between, or if the time
# was embedded in the date.
#
# Days of the week are not scanned, but will be ignored if they occur
# at the start of the date pattern only.
#
# For example, the standard date format:
# Fri Aug 18 17:00:48 BST 2006
# is handled by matching HH:MM:SS and removing it together with the
# matched (but unused) time zone. This leaves the following:
# Fri Aug 18 2006
# "Fri" is ignored and the rest is matched according to the sixth of
# the standard rules.
#
# Relative times
# ==============
#
# The option -r allows a relative time. Years (or ys, yrs, or without s),
# months (or mths, mons, mnths, months, or without s --- "m", "ms" and
# "mns" are ambiguous and are not handled), weeks (or ws, wks, or without
# s) and days (or ds, dys, days, or without s), hours (or hs, hrs, with or
# without s), minutes (or mins, with or without s) and seconds (or ss,
# secs, with or without s) are understood. Spaces between the numbers
# are optional, but are required between items, although a comma
# may be used (with or without spaces).
#
# Note that a year here is 365.25 days and a month is 30 days. TODO:
# improve this by passing down base time and adjusting. (This will
# be crucial for events repeating monthly.) TODO: it then makes
# sense to make PERIODly = 1 PERIOD (also for PERIOD = dai!)
#
# This allows forms like:
# 30 years 3 months 4 days 3:42:41
# 14 days 5 hours
# 4d,10hr
# In this case absolute dates are ignored.
emulate -L zsh
setopt extendedglob
zmodload -i zsh/datetime || return 1
# separator characters before time or between time and date
# allow , - or : before the time: this allows spaceless but still
# relatively logical dates like 2006/09/19:14:27
# don't allow / before time ! the above
# is not 19 hours 14 mins and 27 seconds after anything.
local tschars="[-,:[:space:]]"
# start pattern for time when anchored
local tspat_anchor="(${tschars}#)"
# ... when not anchored
local tspat_noanchor="(|*${tschars})"
# separator characters between elements. comma is fairly
# natural punctuation; otherwise only allow whitespace.
local schars="[.,[:space:]]"
local daypat="${schars}#(sun|mon|tue|wed|thu|fri|sat)[a-z]#${schars}#"
# Start pattern for date: treat , as space for simplicity. This
# is illogical at the start but saves lots of minor fiddling later.
# Date start pattern when anchored at the start.
# We need to be able to ignore the day here, although (for consistency
# with the unanchored case) we don't remove it until later.
# (The problem in the other case is that matching anything before
# the day of the week is greedy, so the day of the week gets ignored
# if it's optional.)
local dspat_anchor="(|(#B)${daypat}(#b)${schars}#)"
# Date start pattern when not anchored at the start.
local dspat_noanchor="(|*${schars})"
# end pattern for relative times: similar remark about use of $schars.
local repat="(|s)(|${schars}*)"
# not locale-dependent! I don't know how to get the months out
# of the system for the purpose of finding out where they occur.
# We may need some completely different heuristic.
local monthpat="(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]#"
# days, not handled but we need to ignore them. also not localized.
integer year month day hour minute second
local opt line orig_line mname MATCH MBEGIN MEND tz
local -a match mbegin mend
# Flags that we found a date or a time (maybe a relative time)
integer date_found time_found
# Indices of positions of start and end of time and dates found.
# These are actual character indices as zsh would normally use, i.e.
# line[time_start,time_end] is the string for the time.
integer time_start time_end date_start date_end
integer anchor anchor_end debug relative reladd setvar
while getopts "aAdrs" opt; do
case $opt in
(a)
# anchor
(( anchor = 1 ))
;;
(A)
# anchor at end, too
(( anchor = 1, anchor_end = 1 ))
;;
(d)
# enable debug output
(( debug = 1 ))
;;
(r)
(( relative = 1 ))
;;
(s)
(( setvar = 1 ))
;;
(*)
return 1
;;
esac
done
shift $(( OPTIND - 1 ))
line=$1 orig_line=$1
local dspat tspat
if (( anchor )); then
# Anchored at the start.
dspat=$dspat_anchor
if (( relative )); then
tspat=$tspat_anchor
else
# We'll test later if the time is associated with the date.
tspat=$tspat_noanchor
fi
else
dspat=$dspat_noanchor
tspat=$tspat_noanchor
fi
# Look for a time separately; we need colons for this.
case $line in
# with seconds, am/pm: don't match / in front.
((#ibm)${~tspat}(<0-12>):(<0-59>)[.:]((<0-59>)(.<->|))[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
hour=$match[2]
minute=$match[3]
second=$match[5]
[[ $match[7] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
time_found=1
;;
# no seconds, am/pm
((#ibm)${~tspat}(<0-12>):(<0-59>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
hour=$match[2]
minute=$match[3]
[[ $match[4] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
time_found=1
;;
# no colon, even, but a.m./p.m. indicator
((#ibm)${~tspat}(<0-12>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
hour=$match[2]
minute=0
[[ $match[3] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
time_found=1
;;
# 24 hour clock, with seconds
((#ibm)${~tspat}(<0-24>):(<0-59>)[.:]((<0-59>)(.<->|))(*))
hour=$match[2]
minute=$match[3]
second=$match[5]
time_found=1
;;
# 24 hour clock, no seconds
((#ibm)${~tspat}(<0-24>):(<0-59>)(*))
hour=$match[2]
minute=$match[3]
time_found=1
;;
esac
(( hour == 24 )) && hour=0
if (( time_found )); then
# time was found
time_start=$mbegin[2]
time_end=$mend[-2]
# Remove the timespec because it may be in the middle of
# the date (as in the output of "date".
# There may be a time zone, too, which we don't yet handle.
# (It's not in POSIX strptime() and libraries don't support it well.)
# This attempts to remove some of the weirder forms.
if [[ $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z]|[-+][0-9][0-9][0-9][0-9])([[:space:]]|(#e))* || \
$line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>)([[:space:]]|(#e))* || \
$line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>[A-Z][A-Z][A-Z])([[:space:]]|(#e))* ]]; then
(( time_end += ${mend[-1]} ))
tz=$match[1]
fi
line=$line[1,time_start-1]$line[time_end+1,-1]
(( debug )) && print "line after time: $line"
fi
if (( relative == 0 )); then
# Date.
case $line in
# Look for YEAR[-/.]MONTH[-/.]DAY
((#bi)${~dspat}((19|20)[0-9][0-9])[-/](<1-12>)[-/](<1-31>)*)
year=$match[2]
month=$match[4]
day=$match[5]
date_start=$mbegin[2] date_end=$mend[5]
date_found=1
;;
# Same with month name
((#bi)${~dspat}((19|20)[0-9][0-9])[-/]${~monthpat}[-/](<1-31>)*)
year=$match[2]
mname=$match[4]
day=$match[5]
date_start=$mbegin[2] date_end=$mend[5]
date_found=1
;;
# Look for DAY[th/st/rd] MNAME[,] YEAR
((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)[[:space:]]##((19|20)[0-9][0-9])*)
day=$match[2]
mname=$match[4]
year=$match[6]
date_start=$mbegin[2] date_end=$mend[6]
date_found=1
;;
# Look for MNAME DAY[th/st/rd][,] YEAR
((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)[[:space:]]##((19|20)[0-9][0-9])*)
mname=$match[2]
day=$match[3]
year=$match[6]
date_start=$mbegin[2] date_end=$mend[6]
date_found=1
;;
# Look for DAY[th/st/rd] MNAME; assume current year
((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)([[:space:]]##*|))
day=$match[2]
mname=$match[4]
strftime -s year "%Y" $EPOCHSECONDS
date_start=$mbegin[2] date_end=$mend[5]
date_found=1
;;
# Look for MNAME DAY[th/st/rd]; assume current year
((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)([[:space:]]##*|))
mname=$match[2]
day=$match[3]
strftime -s year "%Y" $EPOCHSECONDS
date_start=$mbegin[2] date_end=$mend[5]
date_found=1
;;
# Now it gets a bit ambiguous.
# Look for DAY[th/st/rd][/]MONTH[/ ,]YEAR
((#bi)${~dspat}(<1-31>)(|th|st|rd)/(<1-12>)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
day=$match[2]
month=$match[4]
year=$match[7]
date_start=$mbegin[2] date_end=$mend[7]
date_found=1
;;
# Look for MONTH[/]DAY[th/st/rd][/ ,]YEAR
((#bi)${~dspat}(<1-12>)/(<1-31>)(|th|st|rd)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
month=$match[2]
day=$match[3]
year=$match[7]
date_start=$mbegin[2] date_end=$mend[7]
date_found=1
;;
esac
fi
if (( date_found )); then
# date found
# see if there's a day at the start
if [[ ${line[1,$date_start-1]} = (#bi)${~daypat} ]]; then
date_start=$mbegin[1]
fi
line=${line[1,$date_start-1]}${line[$date_end+1,-1]}
if (( time_found )); then
# If we found a time, it must be associated with the date,
# or we can't use it. Since we removed the time from the
# string to find the date, however, it's complicated to
# know where both were found. Reconstruct the date indices of
# the original string.
if (( time_start <= date_start )); then
# Time came before start of date; add length in.
(( date_start += time_end - time_start + 1 ))
fi
if (( time_start <= date_end )); then
(( date_end += time_end - time_start + 1 ))
fi
if (( time_end + 1 < date_start )); then
# If time wholly before date, OK if only separator characters
# in between. (This allows some illogical stuff with commas
# but that's probably not important.)
if [[ ${orig_line[time_end+1,date_start-1]} != ${~schars}# ]]; then
# Clearly this can't work if anchor is set. In principle,
# we could match the date and ignore the time if it wasn't.
# However, that seems dodgy.
return 1
else
# Form massaged line by removing the entire date/time chunk.
line="${orig_line[1,time_start-1]}${orig_line[date_end+1,-1]}"
fi
elif (( date_end + 1 < time_start )); then
# If date wholly before time, OK if only time separator characters
# in between. This allows 2006/10/12:13:43 etc.
if [[ ${orig_line[date_end+1,time_start-1]} != ${~tschars}# ]]; then
# Here, we assume the time is associated with something later
# in the line. This is pretty much inevitable for the sort
# of use we are expecting. For example,
# 2006/10/24 Meeting from early, may go on till 12:00.
# or with some uses of the calendar system,
# 2006/10/24 MR 1 Another pointless meeting WARN 01:00
# The 01:00 says warn an hour before, not that the meeting starts
# at 1 am. About the only safe way round would be to force
# a time to be present, but that's not how the traditional
# calendar programme works.
#
# Hence we need to reconstruct.
(( time_found = 0, hour = 0, minute = 0, second = 0 ))
line="${orig_line[1,date_start-1]}${orig_line[date_end+1,-1]}"
else
# As above.
line="${orig_line[1,date_start-1]}${orig_line[time_end+1,-1]}"
fi
fi
if (( debug )); then
print "Time string: $time_start,$time_end:" \
"'$orig_line[time_start,time_end]'"
print "Date string: $date_start,$date_end:" \
"'$orig_line[date_start,date_end]'"
print "Remaining line: '$line'"
fi
fi
fi
if (( relative )); then
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(y|yr|year)${~repat} ]]; then
(( reladd += ((365*4+1) * 24 * 60 * 60 * ${match[2]} + 1) / 4 ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(mth|mon|mnth|month)${~repat} ]]; then
(( reladd += 30 * 24 * 60 * 60 * ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(w|wk|week)${~repat} ]]; then
(( reladd += 7 * 24 * 60 * 60 * ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(d|dy|day)${~repat} ]]; then
(( reladd += 24 * 60 * 60 * ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(h|hr|hour)${~repat} ]]; then
(( reladd += 60 * 60 * ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(min|minute)${~repat} ]]; then
(( reladd += 60 * ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(s|sec|second)${~repat} ]]; then
(( reladd += ${match[2]} ))
line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
time_found=1
fi
fi
if (( relative )); then
# If no date was found, we're in trouble unless we found a time.
if (( time_found )); then
if (( anchor_end )); then
# must be left with only separator characters
if [[ $line != ${~schars}# ]]; then
return 1
fi
fi
(( REPLY = reladd + (hour * 60 + minute) * 60 + second ))
[[ -n $setvar ]] && REPLY2=$line
return 0
fi
return 1
elif (( ! date_found )); then
return 1
fi
if (( anchor_end )); then
# must be left with only separator characters
if [[ $line != ${~schars}# ]]; then
return 1
fi
fi
local fmt nums
if [[ -n $mname ]]; then
fmt="%Y %b %d %H %M %S"
nums="$year $mname $day $hour $minute $second"
else
fmt="%Y %m %d %H %M %S"
nums="$year $month $day $hour $minute $second"
fi
strftime -s REPLY -r $fmt $nums
[[ -n $setvar ]] && REPLY2=$line
return 0

View file

@ -0,0 +1,24 @@
integer start=$1 stop=$2
shift 2
[[ -o zle ]] && zle -I
print -r "$*"
local -a cmd
zmodload -i zsh/parameter || return
# Use xmessage to display the message if the start and stop time
# are the same, indicating we have been scheduled to display it.
# Don't do this if there's already an xmessage for the same user.
# HERE: this should be configurable and we should be able to do
# better if xmessage isn't available, e.g. wish.
if [[ -n $DISPLAY && $start -eq $stop ]]; then
if [[ -n ${commands[xmessage]} ]]; then
cmd=(xmessage -center)
fi
if [[ -n $cmd[0] ]] &&
! ps -u$UID | grep $cmd[0] >/dev/null 2>&1; then
# turn off job control for this
($cmd "$*" &)
fi
fi

View file

@ -0,0 +1,67 @@
emulate -L zsh
setopt extendedglob
autoload -U calendar_{read,scandate,lockfiles}
local calendar line REPLY new lockfile
local -a calendar_entries
local -a times lines_sorted lines_unsorted lines_failed lockfiles
integer i
# Read the calendar file from the calendar-file style
zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
# Start block for "always" to handle lockfile
{
calendar_lockfiles $calendar || return 1
new=$calendar.new.$$
calendar_read $calendar
if [[ ${#calendar_entries} -eq 0 || \
( ${#calendar_entries} -eq 1 && -z $calendar_entries[1] ) ]]; then
return 0
fi
for line in $calendar_entries; do
if calendar_scandate -a $line; then
lines_unsorted+=("${(l.16..0.)REPLY}:$line")
else
lines_failed+=($line)
fi
done
if (( ${#lines_unsorted} )); then
lines_sorted=(${${(o)lines_unsorted}##[0-9]##:})
fi
{
for line in "${lines_failed[@]}"; do
print "$line # BAD DATE"
done
(( ${#lines_sorted} )) && print -l "${lines_sorted[@]}"
} > $new
if [[ ! -s $new ]]; then
print "Writing to $new failed."
return 1
elif (( ${#lines_failed} )); then
print "Warning: lines with date that couldn't be parsed.
Output (with unparseable dates marked) left in $new"
return 1
fi
if ! mv $calendar $calendar.old; then
print "Couldn't back-up $calendar to $calendar.old.
New calendar left in $new"
return 1
fi
if ! mv $new $calendar; then
print "Failed to rename $new to $calendar.
Old calendar left in $calendar.old"
return 1
fi
print "Old calendar left in $calendar.old"
} always {
(( ${#lockfiles} )) && rm -rf $lockfiles
}