123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- #!/usr/bin/env gawk -f
- # awk script for converting an iCal formatted file to a sequence of org-mode headings.
- # this may not work in general but seems to work for day and timed events from Google's
- # calendar, which is really all I need right now...
- #
- # usage:
- # awk -f THISFILE < icalinputfile.ics > orgmodeentries.org
- #
- # Note: change org meta information generated below for author and
- # email entries!
- #
- # Caveats:
- #
- # - date entries with no time specified are assumed to be local time zone;
- # same remark for date entries that do have a time but do not end with Z
- # e.g.: 20130101T123456 is local and will be kept as 2013-01-01 12:34
- # where 20130223T123422Z is UTC and will be corrected appropriately
- #
- # - UTC times are changed into local times, using the time zone of the
- # computer that runs the script; it would be very hard in an awk script
- # to respect the time zone of a file belonging to another time zone:
- # the offsets will be different as well as the switchover time(s);
- # (consider a remote shell to a computer with the file's time zone)
- #
- # - the UTC conversion entirely relies on the built-in strftime method;
- # the author is not responsible for any erroneous conversions nor the
- # consequence of such conversions
- #
- # - does process RRULE recurring events, but ignores COUNT specifiers
- #
- # - does not process EXDATE to exclude date(s) from recurring events
- #
- # Eric S Fraga
- # 20100629 - initial version
- # 20100708 - added end times to timed events
- # - adjust times according to time zone information
- # - fixed incorrect transfer for entries with ":" embedded within the text
- # - added support for multi-line summary entries (which become headlines)
- # 20100709 - incorporated time zone identification
- # - fixed processing of continuation lines as Google seems to
- # have changed, in the last day, the number of spaces at
- # the start of the line for each continuation...
- # - remove backslashes used to protect commas in iCal text entries
- # no further revision log after this as the file was moved into a git
- # repository...
- #
- # Updated by: Guido Van Hoecke <guivhoATgmailDOTcom>
- # Last change: 2013.05.26 14:28:33
- #----------------------------------------------------------------------------------
- BEGIN {
- ### config section
- attending_types["UNSET"] = 0;
- attending_types["ATTENDING"] = 1;
- attending_types["NEEDS_ACTION"] = 2;
- attending_types["NOT_ATTENDING"] = 3;
- attending_types[0] = "UNSET";
- attending_types[1] = "ATTENDING";
- attending_types[2] = "NEEDS_ACTION";
- attending_types[3] = "NOT_ATTENDING";
- # map of UIDS for duplicate checking -- sometimes the same id comes down
- # with multiple VEVENTS
- UIDS[0];
- # map of people attending a given event
- people_attending[0];
- # maximum age in days for entries to be output: set this to -1 to
- # get all entries or to N>0 to only get enties that start or end
- # less than N days ago
- max_age = 7;
- # set to 1 or 0 to yes or not output a header block with TITLE,
- # AUTHOR, EMAIL etc...
- header = 1;
- # set to 1 or 0 to yes or not output the original ical preamble as
- # comment
- preamble = 1;
- # set to 1 to output time and summary as one line starting with
- # the time (value 1) or to 0 to output the summary as first line
- # and the date and time info as a later line (after the property
- # drawer or org complains)
- condense = 0;
- # set to 1 or 0 to yes or not output the original ical entry as a
- # comment (mostly useful for debugging purposes)
- original = 1;
- # google truncates long subjects with ... which is misleading in
- # an org file: it gives the unfortunate impression that an
- # expanded entry is still collapsed; value 1 will trim those
- # ... and value 0 doesn't touch them
- trimdots = 1;
- # change this to your name
- author = ENVIRON["AUTHOR"] != "" ? ENVIRON["AUTHOR"] : "Marc Sherry"
- # and to your email address
- emailaddress = ENVIRON["EMAIL"] != "" ? ENVIRON["EMAIL"] : "unknown"
- # calendar/category name for display in org-mode
- calendarname = ENVIRON["CALENDAR"] != "" ? ENVIRON["CALENDAR"] : "unknown"
- # any tags for this calendar (e.g. "WORK" or "PERSONAL")
- filetags = ENVIRON["FILETAGS"] != "" ? ENVIRON["FILETAGS"] : "unknown"
- # timezone offsets
- # TODO: this is stupid
- tz_offsets["America/Los_Angeles"] = 0
- tz_offsets["America/Chicago"] = 2
- ### end config section
- # use a colon to separate the type of data line from the actual contents
- FS = ":";
- # we only need to preserve the original entry lines if either the
- # preamble or original options are true
- preserve = preamble || original
- first = 1; # true until an event has been found
- max_age_seconds = max_age*24*60*60
- if (header) {
- print "#+TITLE: Main Google calendar entries"
- print "#+AUTHOR: ", author
- print "#+EMAIL: ", emailaddress
- print "#+DESCRIPTION: converted using the ical2org awk script"
- print "#+CATEGORY: ", calendarname
- print "#+STARTUP: hidestars"
- print "#+STARTUP: overview"
- print "#+FILETAGS: ", filetags
- print ""
- }
- }
- # continuation lines (at least from Google) start with a space. If the
- # continuation is after a processed field (description, summary, attendee,
- # etc.) append the entry to the respective variable
- /^[ ]/ {
- if (indescription) {
- entry = entry gensub("\r", "", "g", gensub("^[ ]", "", 1, $0));
- # print "entry continuation: " entry
- } else if (insummary) {
- summary = summary gensub("\r", "", "g", gensub("^[ ]", "", 1, $0))
- # print "summary continuation: " summary
- } else if (inattendee) {
- attendee = attendee gensub("\r", "", "g", gensub("^[ ]", "", 1, $0))
- # print "attendee continuation: " attendee
- are_we_going(attendee)
- add_attendee(attendee)
- } else if (inlocation) {
- location = location unescape(gensub("\r", "", "g", $0), 0);
- }
- if (preserve)
- icalentry = icalentry "\n" $0
- }
- /^BEGIN:VEVENT/ {
- # start of an event: initialize global values used for each event
- date = "";
- entry = ""
- headline = ""
- icalentry = "" # the full entry for inspection
- id = ""
- indescription = 0;
- insummary = 0
- inattendee = 0
- inlocation = 0
- in_alarm = 0
- got_end_date = 0
- attending = attending_types["UNSET"];
- # http://unix.stackexchange.com/a/147958/129055
- intfreq = "" # the interval and frequency for repeating org timestamps
- lasttimestamp = -1;
- location = ""
- rrend = ""
- status = ""
- summary = ""
- attendee = ""
- delete people_attending;
- # if this is the first event, output the preamble from the iCal file
- if (first) {
- if(preamble) {
- print "* COMMENT original iCal preamble"
- print gensub("\r", "", "g", icalentry)
- }
- if (preserve)
- icalentry = ""
- first = 0;
- }
- }
- # any line that starts at the left with a non-space character is a new data field
- /^BEGIN:VALARM/ {
- # alarms have their own UID, DESCRIPTION, etc. We don't want these polluting the real fields
- in_alarm = 1
- }
- /^END:VALARM/ {
- in_alarm = 0
- }
- /^[A-Z]/ {
- # we do not copy DTSTAMP lines as they change every time you download
- # the iCal format file which leads to a change in the converted
- # org file as I output the original input. This change, which is
- # really content free, makes a revision control system update the
- # repository and confuses.
- if (preserve)
- if (! index("DTSTAMP", $1))
- icalentry = icalentry "\n" $0
- # this line terminates the collection of description and summary entries
- indescription = 0;
- insummary = 0;
- inattendee = 0;
- }
- # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD
- /^DTSTART;VALUE=DATE/ {
- date = datestring($2);
- }
- /^DTEND;VALUE=DATE/ {
- got_end_date = 1
- end_date = datestring($2, 1);
- if ( issameday )
- end_date = ""
- }
- # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS
- # we ignore the seconds
- /^DTSTART[:;][^V]/ {
- tz = "";
- match($0, /TZID=([^:]*)/, a)
- {
- tz = a[1];
- }
- offset = tz_offsets[tz]
- date = datetimestring($2, offset);
- # print date;
- if (date != "" && got_end_date) {
- fix_date_time()
- }
- }
- # and the same for the end date;
- /^DTEND[:;][^V]/ {
- # NOTE: this doesn't necessarily appear after DTSTART
- tz = "";
- match($0, /TZID=([^:]*)/, a)
- {
- tz = a[1];
- }
- offset = tz_offsets[tz]
- end_date = datetimestring($2, offset);
- got_end_date = 1
- if (date != "" && got_end_date) {
- # We got start and end date/time, let's munge as appropriate
- fix_date_time()
- }
- }
- # repetition rule
- /^RRULE:FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)/ {
- # TODO: handle BYDAY values for events that repeat weekly for multiple days
- # (e.g. a "Gym" event)
- # get the d, w, m or y value
- freq = tolower(gensub(/.*FREQ=(.).*/, "\\1", 1, $0))
- # get the interval, and use 1 if none specified
- interval = $2 ~ /INTERVAL=/ ? gensub(/.*INTERVAL=([0-9]+);.*/, "\\1", 1, $2) : 1
- # get the enddate of the rule and use "" if none specified
- rrend = $2 ~ /UNTIL=/ ? datestring(gensub(/.*UNTIL=([0-9]{8}).*/, "\\1", 1, $2)) : ""
- rrend_raw = $2 ~ /UNTIL=/ ? gensub(/.*UNTIL=([0-9]{8}).*/, "\\1", 1, $2) : ""
- repeat_count = $2 ~ /COUNT=/ ? gensub(/.*COUNT=([0-9]+).*/, "\\1", 1, $2) : ""
- # build the repetitor vale as understood by org
- intfreq = " +" interval freq
- # if the repetition is daily, and there is an end date, drop the repetitor
- # as that is the default
- if (intfreq == " +1d" && end_date == "" && rrend != "")
- intfreq = ""
- now = strftime("%Y%m%dT%H%M%SZ")
- if (rrend_raw != "" && rrend_raw < now)
- intfreq = ""
- if (repeat_count != "") # TODO: count repeats correctly
- intfreq = ""
- }
- # The description will the contents of the entry in org-mode.
- # this line may be continued.
- /^DESCRIPTION/ {
- if (!in_alarm) {
- # Setting $1 to "" clears colons from items like "1:1 with Marc", so we
- # strip "DESCRIPTION:" off of the front instead
- # $1 = "";
- entry = entry gensub("\r", "", "g", gensub(/^DESCRIPTION:/, "", 1, $0));
- indescription = 1;
- }
- }
- # the summary will be the org heading
- /^SUMMARY/ {
- # Setting $1 to "" clears colons from items like "1:1 with Marc", so we
- # strip "SUMMARY:" off of the front instead
- if (!in_alarm) {
- summary = gensub("\r", "", "g", gensub(/^SUMMARY:/, "", 1, $0));
- # trim trailing dots if requested by config option
- if(trimdots && summary ~ /\.\.\.$/)
- sub(/\.\.\.$/, "", summary)
- insummary = 1;
- # print "Summary: " summary
- }
- }
- # the unique ID will be stored as a property of the entry
- /^UID/ {
- if (!in_alarm) {
- id = gensub("\r", "", "g", $2);
- }
- }
- /^LOCATION/ {
- location = unescape(gensub("\r", "", "g", $2), 0);
- inlocation = 1;
- # print "Location: " location
- }
- /^STATUS/ {
- status = gensub("\r", "", "g", $2);
- # print "Status: " status
- }
- /^ATTENDEE/ {
- attendee = gensub("\r", "", "g", $0);
- inattendee = 1;
- # print "Attendee: " attendee
- }
- # when we reach the end of the event line, we output everything we
- # have collected so far, creating a top level org headline with the
- # date/time stamp, unique ID property and the contents, if any
- /^END:VEVENT/ {
- #output event
- # print "max_age: " max_age
- # print "lasttimestamp: " lasttimestamp
- # print "lasttimestamp+max_age_seconds: " lasttimestamp+max_age_seconds
- # print "systime(): " systime()
- is_duplicate = (id in UIDS);
- if(is_duplicate == 0 && (max_age<0 || intfreq != "" || ( lasttimestamp>0 && systime()<lasttimestamp+max_age_seconds )) )
- {
- if (attending != attending_types["NOT_ATTENDING"]) {
- # build org timestamp
- if (intfreq != "")
- date = date intfreq
- # TODO: http://orgmode.org/worg/org-faq.html#org-diary-class
- else if (end_date != "")
- date = date ">--<" end_date
- else if (rrend != "")
- date = date ">--<" rrend
- # translate \n sequences to actual newlines and unprotect commas (,)
- if (condense)
- print "* <" date "> " gensub("^[ ]+", "", "", unescape(summary, 0))
- else
- print "* " gensub("^[ ]+", "", "g", unescape(summary, 0))
- print " :PROPERTIES:"
- print " :ID: " id
- if(length(location))
- print " :LOCATION: " location
- if(length(status))
- print " :STATUS: " status
- attending_string = attending_types[attending]
- print " :ATTENDING: " attending_string
- print " :ATTENDEES: " join_keys(people_attending)
- print " :END:"
- if (date2 != "")
- {
- # Fake some logbook entries so we can generate a clock report
- print " :LOGBOOK:"
- print " CLOCK: [" date1 "]--[" date2 "] => " "0:00"
- print " :END"
- }
- if (!condense)
- print "<" date ">"
- print ""
- if(length(entry)>1)
- print gensub("^[ ]+", "", "g", unescape(entry, 1));
- # output original entry if requested by 'original' config option
- if (original)
- print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry)
- }
- UIDS[id] = 1;
- }
- }
- # Join keys in an array, return a string
- function join_keys(input)
- {
- joined = "";
- first_key = 1;
- for (key in input)
- {
- if (first_key != 1)
- joined = joined ", "
- joined = joined key
- first_key = 0;
- }
- return joined;
- }
- # unescape commas, newlines, etc. newlines are optionally converted to just
- # spaces -- it's good to preserve them in descriptions for e.g. interview
- # calendar events, but addresses look better with spaces as more info fits on a
- # line
- function unescape(input, preserve_newlines)
- {
- ret = gensub("\\\\,", ",", "g",
- gensub("\\\\;", ";", "g", input))
- if (preserve_newlines)
- ret = gensub("\\\\n", "\n", "g", ret)
- else
- ret = gensub("\\\\n", " ", "g", ret)
- return ret
- # return gensub("\\\\,", ",", "g",
- # gensub("\\\\n", " ", "g",
- # gensub("\\\\;", ";", "g", input)))
- }
- # funtion to convert an iCal time string 'yyyymmddThhmmss[Z]' into a
- # date time string as used by org, preferably including the short day
- # of week: 'yyyy-mm-dd day hh:mm' or 'yyyy-mm-dd hh:mm' if we cannot
- # define the day of the week
- function datetimestring(input, offset)
- {
- # print "________"
- # print "input : " input
- # convert the iCal Date+Time entry to a format that mktime can understand
- spec = match(input, "([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2}).*[\r]*", a);
- year = a[1]
- month = a[2]
- day = a[3]
- hour = a[4]
- min = a[5]
- sec = a[6]
- # print "spec :" spec
- if (offset > 0)
- {
- hour -= offset
- }
- # print "input: " input
- # print "datetime: " year" "month" "day" "hour" "min" "sec
- stamp = mktime(year" "month" "day" "hour" "min" "sec);
- lasttimestamp = stamp;
- if (stamp <= 0) {
- # this is a date before the start of the epoch, so we cannot
- # use strftime and will deliver a 'yyyy-mm-dd hh:mm' string
- # without day of week; this assumes local time, and does not
- # attempt UTC offset correction
- spec = gensub("([0-9]{4})([0-9]{2})([0-9]{2})T([0-9]{2})([0-9]{2})([0-9]{2}).*[\r]*", "\\1-\\2-\\3 \\4:\\5", "g", input);
- # print "==> spec:" spec;
- return spec;
- }
- if (input ~ /[0-9]{8}T[0-9]{6}Z/ ) {
- # this is an utc time;
- # we need to correct the timestamp by the utc offset for this time
- offset = strftime("%z", stamp)
- pm = substr(offset,1,1) 1 # define multiplier +1 or -1
- hh = substr(offset,2,2) * 3600 * pm
- mm = substr(offset,4,2) * 60 * pm
- # adjust the timestamp
- stamp = stamp + hh + mm
- }
- return strftime("%Y-%m-%d %a %H:%M", stamp);
- }
- # function to convert an iCal date into an org date;
- # the optional parameter indicates whether this is an end date;
- # for single or multiple whole day events, the end date given by
- # iCal is the date of the first day after the event;
- # if the optional 'isenddate' parameter is non zero, this function
- # tries to reduce the given date by one day
- function datestring(input, isenddate)
- {
- #convert the iCal string to a an mktime input string
- spec = gensub("([0-9]{4})([0-9]{2})([0-9]{2}).*[\r]*", "\\1 \\2 \\3 00 00 00", "g", input);
- # compute the nr of seconds after or before the epoch
- # dates before the epoch will have a negative timestamp
- # days after the epoch will have a positive timestamp
- stamp = mktime(spec);
- if (isenddate) {
- # subtract 1 day from the timestamp
- # note that this also works for dates before the epoch
- stamp = stamp - 86400;
- # register whether the end date is same as the start date
- issameday = lasttimestamp == stamp
- }
- # save timestamp to allow for check of max_age
- lasttimestamp = stamp
- if (stamp < 0) {
- # this date is before the epoch;
- # the returned datestring will not have the short day of week string
- # as strftime does not handle negative times;
- # we have to construct the datestring directly from the input
- if (isenddate) {
- # we really should return the date before the input date, but strftime
- # does not work with negative timestamp values; so we can not use it
- # to obtain the string representation of the corrected timestamp;
- # we have to return the date specified in the iCal input and we
- # add time 00:00 to clarify this
- return spec = gensub("([0-9]{4})([0-9]{2})([0-9]{2}).*[\r]*", "\\1-\\2-\\3 00:00", "g", input);
- } else {
- # just generate the desired representation of the input date, without time;
- return gensub("([0-9]{4})([0-9]{2})([0-9]{2}).*[\r]*", "\\1-\\2-\\3", "g", input);
- }
- }
- # return the date and day of week
- return strftime("%Y-%m-%d %a", stamp);
- }
- # Add the current attendee's response to a set, so we can list who's going
- # and who's declined
- function add_attendee(attendee)
- {
- match(attendee, /CN=([^;]+)/, m)
- {
- CN = tolower(m[1]);
- people_attending[CN] = 1;
- }
- }
- function fix_date_time()
- {
- if (substr(date,1,10) == substr(end_date,1,10)) {
- # timespan within same date, use one date with a time range, but preserve
- # original dates for org-clocktable
- date1 = date
- date2 = end_date
- date = date "-" substr(end_date, length(end_date)-4)
- end_date = ""
- }
- }
- # Parse the current ATTENDEE line and see if it belongs to us. If so, check if
- # we've accepted this calendar invite, and if so, set `attending` to True. It
- # may be the case that there are no attendees (e.g. personal calendar items),
- # and if that's the case, we'll leave `attending` unset. If there are attendees,
- # we'll parse our status out and set `attending` appropriately.
- function are_we_going(attendee)
- {
- if (attending != attending_types["UNSET"])
- {
- # print "Bailing out early, attending is " attending
- return;
- }
- match(attendee, /CN=([^;]+)/, m)
- {
- # CN's can optionally be surrounded by quotes (google calendar download
- # omits, apple calendar export includes them)
- CN = gensub("\"", "", "g", tolower(m[1]));
- # TODO: no hardcoding
- if (CN == tolower(author) || CN == tolower(emailaddress))
- {
- # This is us -- did we accept the meeting?
- if (attendee ~ /PARTSTAT=ACCEPTED/)
- {
- attending = attending_types["ATTENDING"];
- }
- else if (attendee ~ /PARTSTAT=NEEDS-ACTION/)
- {
- attending = attending_types["NEEDS_ACTION"];
- }
- else {
- attending = attending_types["NOT_ATTENDING"];
- }
- }
- }
- # print "are_we_going: " attending
- }
- # Local Variables:
- # time-stamp-line-limit: 1000
- # time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S"
- # time-stamp-active: t
- # time-stamp-start: "Last change:[ \t]+"
- # time-stamp-end: "$"
- # End:
|