From 6b1b34d1da6b0db599c026e17df011ad6c6b3a30 Mon Sep 17 00:00:00 2001 From: Peter Stephenson Date: Fri, 1 Dec 2006 10:23:06 +0000 Subject: c.f. 23023: new calendar function system. --- ChangeLog | 16 + Completion/Unix/Type/_list_files | 8 + Doc/.distfiles | 3 +- Doc/Makefile.in | 5 +- Doc/Zsh/.distfiles | 2 +- Doc/Zsh/calsys.yo | 547 ++++++++++++++++++++++++++++++++++ Doc/Zsh/compsys.yo | 10 +- Doc/Zsh/intro.yo | 1 + Doc/Zsh/manual.yo | 1 + Doc/Zsh/modules.yo | 2 +- Doc/Zsh/tcpsys.yo | 2 +- Doc/zsh.yo | 2 + Doc/zshcalsys.yo | 3 + Functions/Calendar/.distfiles | 6 + Functions/Calendar/age | 73 +++++ Functions/Calendar/calendar | 356 ++++++++++++++++++++++ Functions/Calendar/calendar_add | 69 +++++ Functions/Calendar/calendar_lockfiles | 43 +++ Functions/Calendar/calendar_read | 35 +++ Functions/Calendar/calendar_scandate | 519 ++++++++++++++++++++++++++++++++ Functions/Calendar/calendar_show | 24 ++ Functions/Calendar/calendar_sort | 67 +++++ Src/Modules/datetime.mdd | 1 + 23 files changed, 1787 insertions(+), 8 deletions(-) create mode 100644 Doc/Zsh/calsys.yo create mode 100644 Doc/zshcalsys.yo create mode 100644 Functions/Calendar/.distfiles create mode 100644 Functions/Calendar/age create mode 100644 Functions/Calendar/calendar create mode 100644 Functions/Calendar/calendar_add create mode 100644 Functions/Calendar/calendar_lockfiles create mode 100644 Functions/Calendar/calendar_read create mode 100644 Functions/Calendar/calendar_scandate create mode 100644 Functions/Calendar/calendar_show create mode 100644 Functions/Calendar/calendar_sort diff --git a/ChangeLog b/ChangeLog index e11ead760..085bbfffc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +2006-12-01 Peter Stephenson + + * c.f. 23023: Completion/Unix/Type/_list_files, Doc/.distfiles, + Doc/Makefile.in, Doc/zsh.yo, Doc/zshcalsys.yo, Doc/Zsh/.distfiles, + Doc/Zsh/calsys.yo, Doc/Zsh/compsys.yo, Doc/Zsh/intro.yo, + Doc/Zsh/manual.yo, Doc/Zsh/modules.yo, Doc/Zsh/tcpsys.yo, + Functions/Calendar/.distfiles, Functions/Calendar/age, + Functions/Calendar/calendar, Functions/Calendar/calendar_add, + Functions/Calendar/calendar_lockfiles, + Functions/Calendar/calendar_read, + Functions/Calendar/calendar_scandate, + Functions/Calendar/calendar_show, + Functions/Calendar/calendar_sort, Src/Modules/datetime.mdd: new + calendar system with age glob qualifier function; files + _list_files to be able not to trample over external stat. + 2006-11-28 Peter Stephenson * 23022: Test/ztst.zsh: don't allow WORDCHARS to be exported diff --git a/Completion/Unix/Type/_list_files b/Completion/Unix/Type/_list_files index 5e745d9d1..3d5281669 100644 --- a/Completion/Unix/Type/_list_files +++ b/Completion/Unix/Type/_list_files @@ -48,6 +48,13 @@ done zmodload -i zsh/stat 2>/dev/null || return 1 +# Enable stat temporarily if disabled to avoid clashes. +integer disable_stat +if [[ ${builtins[stat]} != defined ]]; then + (( disable_stat = 1 )) + enable stat +fi + dir=${2:+$2/} dir=${(Q)dir} @@ -66,4 +73,5 @@ done (( ${#listfiles} )) && listopts=(-d listfiles -l -o) +(( disable_stat )) && disable stat return 0 diff --git a/Doc/.distfiles b/Doc/.distfiles index 6a0c44e3d..6175aa8d6 100644 --- a/Doc/.distfiles +++ b/Doc/.distfiles @@ -2,7 +2,8 @@ DISTFILES_SRC=' .cvsignore .distfiles Makefile.in META-FAQ.yo intro.ms version.yo zmacros.yo zman.yo ztexi.yo - zsh.yo zshbuiltins.yo zshcompctl.yo zshcompsys.yo zshcompwid.yo + zsh.yo zshbuiltins.yo zshcalsys.yo + zshcompctl.yo zshcompsys.yo zshcompwid.yo zshexpn.yo zshmisc.yo zshmodules.yo zshoptions.yo zshparam.yo zshroadmap.yo zshzftpsys.yo zshzle.yo zshcontrib.yo zshtcpsys.yo zsh.texi diff --git a/Doc/Makefile.in b/Doc/Makefile.in index fea6cb8b6..c87f79cef 100644 --- a/Doc/Makefile.in +++ b/Doc/Makefile.in @@ -45,7 +45,7 @@ TEXI2HTML = texi2html --output . --ifinfo --split=chapter .SUFFIXES: .yo .1 # man pages to install -MAN = zsh.1 zshbuiltins.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \ +MAN = zsh.1 zshbuiltins.1 zshcalsys.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \ zshcontrib.1 zshexpn.1 zshmisc.1 zshmodules.1 \ zshoptions.1 zshparam.1 zshroadmap.1 zshtcpsys.1 zshzftpsys.1 zshzle.1 \ zshall.1 @@ -70,6 +70,7 @@ Zsh/mod_zprof.yo Zsh/mod_zpty.yo Zsh/mod_zselect.yo \ Zsh/mod_zutil.yo YODLSRC = zmacros.yo zman.yo ztexi.yo Zsh/arith.yo Zsh/builtins.yo \ +Zsh/calsys.yo \ Zsh/compat.yo Zsh/compctl.yo Zsh/compsys.yo Zsh/compwid.yo Zsh/cond.yo \ Zsh/contrib.yo Zsh/exec.yo Zsh/expn.yo \ Zsh/filelist.yo Zsh/files.yo Zsh/func.yo Zsh/grammar.yo Zsh/manual.yo \ @@ -175,6 +176,8 @@ zsh.1 zshall.1: Zsh/intro.yo Zsh/metafaq.yo Zsh/invoke.yo Zsh/files.yo \ zshbuiltins.1: Zsh/builtins.yo +zshcalsys.1: Zsh/calsys.yo + zshcompctl.1: Zsh/compctl.yo zshcompwid.1: Zsh/compwid.yo diff --git a/Doc/Zsh/.distfiles b/Doc/Zsh/.distfiles index 5961f6201..2dd39b20b 100644 --- a/Doc/Zsh/.distfiles +++ b/Doc/Zsh/.distfiles @@ -1,6 +1,6 @@ DISTFILES_SRC=' .cvsignore .distfiles - arith.yo builtins.yo compat.yo compctl.yo compsys.yo compwid.yo + arith.yo builtins.yo calsys.yo compat.yo compctl.yo compsys.yo compwid.yo cond.yo exec.yo expn.yo filelist.yo files.yo func.yo grammar.yo index.yo intro.yo invoke.yo jobs.yo manual.yo metafaq.yo mod_cap.yo mod_clone.yo mod_compctl.yo mod_complete.yo mod_complist.yo diff --git a/Doc/Zsh/calsys.yo b/Doc/Zsh/calsys.yo new file mode 100644 index 000000000..61226d39d --- /dev/null +++ b/Doc/Zsh/calsys.yo @@ -0,0 +1,547 @@ +texinode(Calendar Function System)(TCP Function System)(Zsh Modules)(Top) +chapter(Calendar Function System) +cindex(calendar function system) +cindex(zsh/datetime, function system based on) +sect(Description) + +The shell is supplied with a series of functions to replace and enhance the +traditional Unix tt(calendar) programme, which warns the user of imminent +or future events, details of which are stored in a text file (typically +tt(calendar) in the user's home directory). The version provided here +includes a mechanism for alerting the user when an event is due. + +In addition a function tt(age) is provided that can be used in a glob +qualifier; it allows files to be selected based on their modification +times. + +The format of the tt(calendar) file and the dates used there in and in +the tt(age) function are described first, then the functions that can +be called to examine and modify the tt(calendar) file. + +The functions here depend on the availability of the tt(zsh/datetime) +module which is usually installed with the shell. The library function +tt(strptime+LPAR()RPAR()) must be available; it is present on most recent +operating systems. + +startmenu() +menu(Calendar File and Date Formats) +menu(Calendar System User Functions) +menu(Calendar Styles) +menu(Calendar Utility Functions) +menu(Calendar Bugs) +endmenu() + + +texinode(Calendar File and Date Formats)(Calendar System User Functions)()(Calendar Function System) +sect(File and Date Formats) + +subsect(Calendar File Format) + +The calendar file is by default tt(~/calendar). This can be configured +by the tt(calendar-file) style, see +ifzman(the section STYLES below)\ +ifnzman(noderef(Calendar Styles)). The basic format consists +of a series of separate lines, with no indentation, each including +a date and time specification followed by a description of the event. + +Various enhancements to this format are supported, based on the syntax +of Emacs calendar mode. An indented line indicates a continuation line +that continues the description of the event from the preceeding line +(note the date may not be continued in this way). An initial ampersand +(tt(&)) is ignored for compatibility. + +The Emacs extension that a date with no description may refer to a number +of succeeding events at different times is not supported. + +Unless the tt(done-file) style has been altered, any events which +have been processed are appended to the file with the same name as the +calendar file with the suffix tt(.done), hence tt(~/calendar.done) by +default. + +An example is shown below. + +subsect(Date Format) + +The format of the date and time is designed to allow flexibility without +admitting ambiguity. Note that there is no localization support; month and +day names must be in English (though only the first three letters are +significant) and separator characters are fixed. Furthermore, time zones +are not handled; all times are assumed to be local. + +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. + +startitemize() +itemiz(In particular, note the confusion between +var(month)tt(/)var(day)tt(/)var(year) and +var(day)tt(/)var(month)tt(/)var(year) when the month is numeric; these +formats should be avoided if at all possible. Many alternatives are +available.) +itemiz(The year must be given in full to avoid confusion, and only years +from 1900 to 2099 inclusive are matched.) +enditemize() + +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). + +example(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) + +More detailed rules follow. + +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 the following: + +startitemize() +itemiz(var(HH)tt(:)var(MM)[tt(:)var(SS)[tt(.)var(FFFFF)]] [tt(am)|tt(pm)|tt(a.m.)|tt(p.m.)]) +itemiz(var(HH)tt(:)var(MM)tt(.)var(SS)[tt(.)var(FFFFF)] [tt(am)|tt(pm)|tt(a.m.)|tt(p.m.)]) +enditemize() + +Here, square brackets indicate optional elements, possibly with +alternatives. Fractions of a second are recognised but ignored. For +absolute times (the normal format require by the tt(calendar) file and the +tt(age) function) a date is mandatory but a time of day is not; the time +returned is at the start of the date. One variation is allowed: if +tt(a.m.) or tt(p.m.) or one of their variants is present, an hour without a +minute is allowed, e.g. tt(3 p.m.). + +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 +unusual. The following are examples of forms that are understood: + +example(+0100 +GMT +GMT-7 +CET+1CDT) + +Any part of the timezone that is not numeric must have exactly three +capital letters in the name. + +Dates suffer from the ambiguity between var(DD)tt(/)var(MM)tt(/)var(YYYY) +and var(MM)tt(/)var(DD)tt(/)var(YYYY). It is recommended this form is +avoided with purely numeric dates, but use of ordinals, +eg. tt(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 tt(19) or tt(20)); tt(03/04/08) is not recognised. Other +numbers may have leading zeroes, but they are not required. The following +are handled: + +startitemize() +itemiz(var(YYYY)tt(/)var(MM)tt(/)var(DD)) +itemiz(var(YYYY)tt(-)var(MM)tt(-)var(DD)) +itemiz(var(YYYY)tt(/)var(MNM)tt(/)var(DD)) +itemiz(var(YYYY)tt(-)var(MNM)tt(-)var(DD)) +itemiz(var(DD)[tt(th)|tt(st)|tt(rd)] var(MNM)[tt(,)] [ var(YYYY) ]) +itemiz(var(MNM) var(DD)[tt(th)|tt(st)|tt(rd)][tt(,)] [ var(YYYY) ]) +itemiz(var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(MM)[tt(,)] var(YYYY)) +itemiz(var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(MM)tt(/)var(YYYY)) +itemiz(var(MM)tt(/)var(DD)[tt(th)|tt(st)|tt(rd)][tt(,)] var(YYYY)) +itemiz(var(MM)tt(/)var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(YYYY)) +enditemize() + +Here, var(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, +maybe, junta, etc. are happily handled. + +Where the year is shown as optional, the current year is assumed. There +are only two such cases, the form tt(Jun 20) or tt(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. tt(1965/07/12:09:45); this is in +order to provide a format with no whitespace. A comma and whitespace are +allowed, e.g. tt(1965/07/12, 09:45). Currently the order of these +separators is not checked, so illogical formats such as tt(1965/07/12, : +,09:45) will also be matched. For simplicity such variations are not shown +in the list above. 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: + +example(Fri Aug 18 17:00:48 BST 2006) + +is handled by matching var(HH)tt(:)var(MM)tt(:)var(SS) and removing it +together with the matched (but unused) time zone. This leaves the following: + +example(Fri Aug 18 2006) + +tt(Fri) is ignored and the rest is matched according to the standard rules. + +subsect(Relative Time Format) + +In certain places relative times are handled. Here, a date is not allowed; +instead a combination of various supported periods are allowed, together +with an optional time. The periods must be in order from most to +least significant. + +The periods handled, with possible abbreviations are: + +startitem() +item(Years)( +tt(years), tt(yrs), tt(ys), tt(year), tt(yr), tt(y). +Currently a year is 365.25 days, not a calendar year. +) +item(Months)( +tt(months), tt(mons), tt(mnths), tt(mths), tt(month), tt(mon), +tt(mnth), tt(mth). Note that tt(m), tt(ms), tt(mn), tt(mns) +are ambiguous and are em(not) handled. Currently a month is a period +of 30 days rather than a calendar month. +) +item(Weeks)( +tt(weeks), tt(wks), tt(ws), tt(week), tt(wk), tt(w) +) +item(Days)( +tt(days), tt(dys), tt(ds), tt(day), tt(dy), tt(d) +) +item(Minutes)( +tt(minutes), tt(mins), tt(minute), tt(min), but em(not) tt(m), +tt(ms), tt(mn) or tt(mns) +) +item(Seconds)( +tt(seconds), tt(secs), tt(ss), tt(second), tt(sec), tt(s) +) +enditem() + +Spaces between the numbers are optional, but are required between items, +although a comma may be used (with or without spaces). + +Here are some examples: + +example(30 years 3 months 4 days 3:42:41 +14 days 5 hours +4d,10hr) + +subsect(Example) + +Here is an example calendar file. It uses a consistent date format, +as recommended above. The second entry has a continuation line. + +example(Feb 1, 2006 14:30 Pointless bureaucratic meeting +Mar 27, 2006 11:00 Mutual recrimination and finger pointing + Bring water pistol and waterproofs +Apr 10, 2006 13:30 Even more pointless blame assignment exercise) + + +texinode(Calendar System User Functions)(Calendar Styles)(Calendar File and Date Formats)(Calendar Function System) +sect(User Functions) + +This section describes functions that are designed to be called +directly by the user. The first part describes those functions +associated with the user's calendar; the second part describes +the use in glob qualifiers. + +subsect(Calendar system functions) + +startitem() +findex(calendar) +xitem(tt(calendar) [ tt(-dDsv) ] [ tt(-C) var(calfile) ] [ -n var(num) ] [ tt(-S) var(showprog) ] [ [ var(start) ] var(end) ])( +item(tt(calendar -r) [ tt(-dDrsv) ] [ tt(-C) var(calfile) ] [ -n var(num) ] [ tt(-S) var(showprog) ] [ var(start) ])( +Show events in the calendar. + +With no arguments, show events from the start of today until the end of +the next working day after today. In other words, if today is Friday, +Saturday, or Sunday, show up to the end of the following Monday, otherwise +show today and tomorrow. + +If var(end) is given, show events from the start of today up to the time +and date given, which is in the format described in the previous section. +Note that if this is a date the time is assumed to be midnight at the +start of the date, so that effectively this shows all events before +the given date. + +var(end) may start with a tt(+), in which case the remainder of the +specification is a relative time format as described in the previous +section indicating the range of time from the start time that is to +be included. + +If var(start) is also given, show events starting from that time and date. +The word tt(now) can be used to indicate the current time. + +To implement an alert when events are due, include tt(calendar -s) in your +tt(~/.zshrc) file. + +Options: + +startitem() +item(tt(-C) var(calfile))( +Explicitly specify a calendar file instead of the value of +the tt(calendar-file) style or the the default tt(~/calendar). +) +item(tt(-d))( +Move any events that have passed from the calendar file to the +"done" file, as given by the tt(done-file) style or the default +which is the calendar file with tt(.done) appended. This option +is implied by the tt(-s) option. +) +item(tt(-D))( +Turns off the option tt(-d), even if the tt(-s) option is also present. +) +item(tt(-n) var(num), tt(-)var(num))( +Show at least var(num) events, if present in the calendar file, regardless +of the tt(start) and tt(end). +) +item(tt(-r))( +Show all the remaining options in the calendar, ignoring the given tt(end) +time. The tt(start) time is respected; any argument given is treated +as a tt(start) time. +) +item(tt(-s))( +Use the shell's tt(sched) command to schedule a timed event that +will warn the user when an event is due. Note that the tt(sched) command +only runs if the shell is at an interactive prompt; a foreground taks +blocks the scheduled task from running until it is finished. + +The timed event usually runs the programme tt(calendar_show) to show +the event, as described in +ifzman(the section UTILITY FUNCTIONS below)\ +ifnzman(noderef(Calendar Utility Functions)). + +By default, a warning of the event is shown five minutes before it is due. +The warning period can be configured by the style tt(warn-time) or +for a single calendar entry by including tt(WARN) var(reltime) in the first +line of the entry, where var(reltime) is one of the usual relative time +formats. + +It is safe to run tt(calendar -s) to reschedule an existing event +(if the calendar file has changed, for example), and also to have it +running in multiples instances of the shell since the calendar file +is locked when in use. + +By default, expired events are moved to the "done" file; see the tt(-d) +option. Use tt(-D) to prevent this. +) +item(tt(-S) var(showprog))( +Explicitly specify a programme to be used for showing events instead +of the value of the tt(show-prog) style or the default tt(calendar_show). +) +item(tt(-v))( +Verbose: show more information about stages of processing. +) +enditem() +) +findex(calendar_add) +item(tt(calendar_add) var(event ...))( +Adds a single event to the calendar in the appropriate location. +Using this function ensures that the calendar file is sorted in date +and time order. It also makes special arrangments for locking +the file will it is altered. The old calendar is left in a file +with the suffix tt(.old). +) +findex(calendar_sort) +item(tt(calendar_sort))( +Sorts the calendar file into date and time order. The old calendar is +left in a file with the suffix tt(.old). +) +enditem() + +subsect(Glob qualifiers) +findex(age) + +The function tt(age) can be autoloaded and use separately from +the calendar system, although it uses the function tt(calendar_scandate) +for date formatting. It requires the tt(zsh/stat) builtin, which +makes available the builtin tt(stat). This may conflict with an +external programme of the same name; if it does, the builtin may be +disabled for normal operation by including the following code in +an initialization file: + +example(zmodload -i zsh/stat +disable stat) + +tt(age) selects files having a given modification time for use +as a glob qualifer. The format of the date is the same as that +understood by the calendar system, described in +ifzman(the section FILE AND DATE FORMATS above)\ +ifnzman(noderef(Calendar File and Date Formats)). + +The function can take one or two arguments, which can be supplied either +directly as command or arguments, or separately as shell parameters. + +example(print *+LPAR()e:age 2006/10/04 2006/10/09:+RPAR()) + +The example above matches all files modified between the start of those +dates. + +example(print *+LPAR()e:age 2006/10/04+RPAR()) + +The example above matches 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). + +example(print *+LPAR()e-age 2006/10/04:10:15 2006/10/04:10:45-RPAR()) + +The example above supplies times. Note that whitespace within the time and +date specification must be quoted to ensure tt(age) receives the correct +arguments, hence the use of the additional colon to separate the date and +time. + +example(AGEREF1=2006/10/04:10:15 +AGEREF2=2006/10/04:10:45 +print *+LPAR()PLUS()age+RPAR()) + +This shows the same example before using another form of argument +passing. The dates and times in the parameters tt(AGEREF1) and tt(AGEREF2) +stay in effect until unset, but will be overridden if any argument is +passed as an explicit argument to age. Any explicit argument +causes both parameters to be ignored. + + +texinode(Calendar Styles)(Calendar Utility Functions)(Calendar System User Functions)(Calendar Function System) +sect(Styles) + +The zsh style mechanism using the tt(zstyle) command is describe in +ifzman(zmanref(zshmodules))\ +ifnzman(noderef(The zsh/zutil Module)). This is the same mechanism +used in the completion system. + +The styles below are all examined in the context +tt(:datetime:)var(function)tt(:), for example tt(:datetime:calendar:). + +startitem() +kindex(calendar-file) +item(tt(calendar-file))( +The location of the main calendar. The default is tt(~/calendar). +) +kindex(done-file) +item(tt(done-file))( +The location of the file to which events which have passed are appended. +The default is the calendar file location with the suffix tt(.done). +The style may be set to an empty string in which case a "done" file +will not be maintained. +) +kindex(show-prog) +item(tt(show-prog))( +The programme run by tt(calendar) for showing events. It will +be passed the start time and stop time of the events requested in seconds +since the epoch followed by the event text. Note that tt(calendar -s) uses +a start time and stop time equal to one another to indicate alerts +for specific events. + +The default is the function tt(calendar_show). +) +kindex(warn-time) +item(tt(warn-time))( +The time before an event at which a warning will be displayed, if the +first line of the event does not include the text tt(EVENT) var(reltime). +The default is 5 minutes. +) +enditem() + + +texinode(Calendar Utility Functions)(Calendar Bugs)(Calendar Styles)(Calendar Function System) +sect(Utility functions) + +startitem() +findex(calendar_lockfiles) +item(tt(calendar_lockfiles))( +Attempt to lock the files given in the argument. To prevent +problems with network file locking this is done in an ad hoc fashion +by attempting to create a symbolic link to the file with the name +var(file)tt(.lockfile). Otherwise, however, the function is not +specific to the calendar system. Three attempts are made to lock +the file before giving up. + +The files locked are appended to the array tt(lockfiles), which should +be local to the caller. + +If all files were successully, status zero is returned, else status one. +) +findex(calendar_read) +item(tt(calendar_read))( +This is a backend used by various other functions to parse the +calendar file, which is passed as the only argument. The array +tt(calendar_entries) is set to the list of events in the file; no +pruning is done except that ampersands are removed from the start of +the line. Each entry may contain multiple lines. +) +findex(calendar_scandate) +item(tt(calendar_scandate))( +This is a generic function to parse dates and times that may be +used separately from the calendar system. The argument is a date +or time specification as described in +ifzman(the section FILE AND DATE FORMATS above)\ +ifnzman(noderef(Calendar File and Date Formats). The parameter tt(REPLY) +is set to the number of seconds since the epoch corresponding to that date +or time. By default, the date and time may occur anywhere within the given +argument. + +Returns status zero if the date and time were successfully parsed, +else one. + +Options: +startitem() +item(tt(-a))( +The date and time are anchored to the start of the argument; they +will not be matched if there is preceeding text. +) +item(tt(-A))( +The date and time are anchored to both the start and end of the argument; +they will not be matched if the is any other text in the argument. +) +item(tt(-d))( +Enable additional debugging output. +) +item(tt(-r))( +The arguments passed is to be parsed as a relative time. +) +item(tt(-s))( +In addition to setting tt(REPLY), set tt(REPLY2) to the remainder of +the argument after the date and time have been stripped. This is +empty if the option tt(-A) was given. +) +enditem() +) +findex(calendar_show) +item(tt(calendar_show))( +The function used by default to display events. It accepts a start time +and end time for events, both in epoch seconds, and an event description. + +The event is always printed to standard output. If the command line editor +is active (which will usually be the case) the command line will be +redisplayed after the output. + +If the parameter tt(DISPLAY) is set and the start and end times are +the same (indicating a scheduled event), the function uses the +command tt(xmessage) to display a window with the event details. +) +enditem() + +texinode(Calendar Bugs)(Calendar Utility Functions)()(Calendar Function System) +sect(Bugs) + +There is no tt(calendar_delete) function. + +There is no localization support for dates and times, nor any support +for the use of time zones. + +Relative periods of months and years do not take into account the variable +number of days. + +Recurrent events are not yet supported. + +The tt(calendar_show) function is currently hardwired to use tt(xmessage) +for displaying alerts on X Window System displays. This should be +configurable and ideally integrate better with the desktop. + +tt(calendar_lockfiles) hangs the shell while waiting for a lock on a file. +If called from a scheduled task, it should instead reschedule the event +that caused it. diff --git a/Doc/Zsh/compsys.yo b/Doc/Zsh/compsys.yo index de8f18094..d8e43ea77 100644 --- a/Doc/Zsh/compsys.yo +++ b/Doc/Zsh/compsys.yo @@ -1361,10 +1361,14 @@ specified will always be completed. kindex(file-list, completion style) item(tt(file-list))( This style controls whether files completed using the standard builtin -mechanism are to be listed with a long list similar to tt(ls -l) -(although note that this feature actually uses the shell module +mechanism are to be listed with a long list similar to tt(ls -l). +Note that this feature uses the shell module tt(zsh/stat) for file information; this loads the builtin tt(stat) -which will replace any external tt(stat) executable). +which will replace any external tt(stat) executable. To avoid +this the following code can be included in an initialization file: + +example(zmodload -i zsh/stat +disable stat) The style may either be set to a true value (or `tt(all)'), or one of the values `tt(insert)' or `tt(list)', indicating that files diff --git a/Doc/Zsh/intro.yo b/Doc/Zsh/intro.yo index c56dcaedd..510a7bda5 100644 --- a/Doc/Zsh/intro.yo +++ b/Doc/Zsh/intro.yo @@ -26,6 +26,7 @@ list(em(zshcompwid) Zsh completion widgets) list(em(zshcompsys) Zsh completion system) list(em(zshcompctl) Zsh completion control) list(em(zshmodules) Zsh loadable modules) +list(em(zshcalsys) Zsh built-in calendar functions) list(em(zshtcpsys) Zsh built-in TCP functions) list(em(zshzftpsys) Zsh built-in FTP client) list(em(zshcontrib) Additional zsh functions and utilities) diff --git a/Doc/Zsh/manual.yo b/Doc/Zsh/manual.yo index 4bf9f6bcd..ff6ecae41 100644 --- a/Doc/Zsh/manual.yo +++ b/Doc/Zsh/manual.yo @@ -34,6 +34,7 @@ menu(Completion Widgets) menu(Completion System) menu(Completion Using compctl) menu(Zsh Modules) +menu(Calendar Function System) menu(TCP Function System) menu(Zftp Function System) menu(User Contributions) diff --git a/Doc/Zsh/modules.yo b/Doc/Zsh/modules.yo index cbec478e3..3f9986096 100644 --- a/Doc/Zsh/modules.yo +++ b/Doc/Zsh/modules.yo @@ -1,4 +1,4 @@ -texinode(Zsh Modules)(TCP Function System)(Completion Using compctl)(Top) +texinode(Zsh Modules)(Calendar Function System)(Completion Using compctl)(Top) chapter(Zsh Modules) cindex(modules) sect(Description) diff --git a/Doc/Zsh/tcpsys.yo b/Doc/Zsh/tcpsys.yo index f1586b602..c93736a37 100644 --- a/Doc/Zsh/tcpsys.yo +++ b/Doc/Zsh/tcpsys.yo @@ -1,4 +1,4 @@ -texinode(TCP Function System)(Zftp Function System)(Zsh Modules)(Top) +texinode(TCP Function System)(Zftp Function System)(Calendar Function System)(Top) chapter(TCP Function System) cindex(TCP function system) cindex(ztcp, function system based on) diff --git a/Doc/zsh.yo b/Doc/zsh.yo index 0517c8748..0d815c2e0 100644 --- a/Doc/zsh.yo +++ b/Doc/zsh.yo @@ -64,6 +64,7 @@ ifnzman(includefile(Zsh/compwid.yo)) ifnzman(includefile(Zsh/compsys.yo)) ifnzman(includefile(Zsh/compctl.yo)) ifnzman(includefile(Zsh/modules.yo)) +ifnzman(includefile(Zsh/calsys.yo)) ifnzman(includefile(Zsh/tcpsys.yo)) ifnzman(includefile(Zsh/zftpsys.yo)) ifnzman(includefile(Zsh/contrib.yo)) @@ -81,6 +82,7 @@ source(zshcompwid) source(zshcompsys) source(zshcompctl) source(zshmodules) +source(zshcalsys) source(zshtcpsys) source(zshzftpsys) source(zshcontrib) diff --git a/Doc/zshcalsys.yo b/Doc/zshcalsys.yo new file mode 100644 index 000000000..70c800db0 --- /dev/null +++ b/Doc/zshcalsys.yo @@ -0,0 +1,3 @@ +manpage(ZSHCALSYS)(1)(date())(zsh version()) +manpagename(zshcalsys)(zsh calendar system) +includefile(Zsh/calsys.yo) diff --git a/Functions/Calendar/.distfiles b/Functions/Calendar/.distfiles new file mode 100644 index 000000000..5d96b5706 --- /dev/null +++ b/Functions/Calendar/.distfiles @@ -0,0 +1,6 @@ +DISTFILES_SRC=' + .distfiles + age + calendar calendar_add calendar_lockfiles calendar_read + calendar_scandate calendar_show calendar_sort +' diff --git a/Functions/Calendar/age b/Functions/Calendar/age new file mode 100644 index 000000000..4ed3bd8c2 --- /dev/null +++ b/Functions/Calendar/age @@ -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 diff --git a/Functions/Calendar/calendar b/Functions/Calendar/calendar new file mode 100644 index 000000000..124fd9786 --- /dev/null +++ b/Functions/Calendar/calendar @@ -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 diff --git a/Functions/Calendar/calendar_add b/Functions/Calendar/calendar_add new file mode 100644 index 000000000..2a00811fd --- /dev/null +++ b/Functions/Calendar/calendar_add @@ -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 diff --git a/Functions/Calendar/calendar_lockfiles b/Functions/Calendar/calendar_lockfiles new file mode 100644 index 000000000..58ee42114 --- /dev/null +++ b/Functions/Calendar/calendar_lockfiles @@ -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 diff --git a/Functions/Calendar/calendar_read b/Functions/Calendar/calendar_read new file mode 100644 index 000000000..ed163887f --- /dev/null +++ b/Functions/Calendar/calendar_read @@ -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 diff --git a/Functions/Calendar/calendar_scandate b/Functions/Calendar/calendar_scandate new file mode 100644 index 000000000..f0024b89a --- /dev/null +++ b/Functions/Calendar/calendar_scandate @@ -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 diff --git a/Functions/Calendar/calendar_show b/Functions/Calendar/calendar_show new file mode 100644 index 000000000..f731d07a5 --- /dev/null +++ b/Functions/Calendar/calendar_show @@ -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 diff --git a/Functions/Calendar/calendar_sort b/Functions/Calendar/calendar_sort new file mode 100644 index 000000000..7d346efc1 --- /dev/null +++ b/Functions/Calendar/calendar_sort @@ -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 +} diff --git a/Src/Modules/datetime.mdd b/Src/Modules/datetime.mdd index fbe7e0356..853b3bc79 100644 --- a/Src/Modules/datetime.mdd +++ b/Src/Modules/datetime.mdd @@ -3,6 +3,7 @@ name=zsh/datetime link=either load=no +functions='Functions/Calendar/*' autobins="strftime" objects="datetime.o" -- cgit 1.4.1