#!/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 # Last change: 2013.05.26 14:28:33 #---------------------------------------------------------------------------------- BEGIN { ### config section # 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 second line 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 = "Eric S Fraga" # and to your email address emailaddress = "e.fraga@ucl.ac.uk" ### 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: google" print "#+STARTUP: hidestars" print "#+STARTUP: overview" print "" } } # continuation lines (at least from Google) start with a space # if the continuation is after a description or a summary, append the entry # to the respective variable /^[ ]/ { if (indescription) { entry = entry gensub("\r", "", "g", gensub("^[ ]", "", "", $0)); } else if (insummary) { summary = summary gensub("\r", "", "g", gensub("^[ ]", "", "", $0)) } if (preserve) icalentry = icalentry "\n" $0 } /^BEGIN:VEVENT/ { # start of an event: initialize global velues used for each event date = ""; entry = "" headline = "" icalentry = "" # the full entry for inspection id = "" indescription = 0; insummary = 0 intfreq = "" # the interval and frequency for repeating org timestamps lasttimestamp = -1; location = "" rrend = "" status = "" summary = "" # 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 = false; } } # any line that starts at the left with a non-space character is a new data field /^[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; } # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD /^DTSTART;VALUE=DATE/ { date = datestring($2); } /^DTEND;VALUE=DATE/ { time2 = datestring($2, 1); if ( issameday ) time2 = "" } # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS # we ignore the seconds /^DTSTART[:;][^V]/ { date = datetimestring($2); # print date; } # and the same for the end date; /^DTEND[:;][^V]/ { time2 = datetimestring($2); if (substr(date,1,10) == substr(time2,1,10)) { # timespan within same date, use one date with a time range date = date "-" substr(time2, length(time2)-4) time2 = "" } } # repetition rule /^RRULE:FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)/ { # get the d, w, m or y value freq = tolower(gensub(/.*FREQ=(.).*/, "\\1", $0)) # get the interval, and use 1 if none specified interval = $2 ~ /INTERVAL=/ ? gensub(/.*INTERVAL=([0-9]+);.*/, "\\1", $2) : 1 # get the enddate of the rule and use "" if none specified rrend = $2 ~ /UNTIL=/ ? datestring(gensub(/.*UNTIL=([0-9]{8}).*/, "\\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" && time2 =="" && rrend != "") intfreq = "" } # The description will the contents of the entry in org-mode. # this line may be continued. /^DESCRIPTION/ { $1 = ""; entry = entry gensub("\r", "", "g", $0); indescription = 1; } # the summary will be the org heading /^SUMMARY/ { $1 = ""; summary = gensub("\r", "", "g", $0); # trim trailing dots if requested by config option if(trimdots && summary ~ /\.\.\.$/) sub(/\.\.\.$/, "", summary) insummary = 1; } # the unique ID will be stored as a property of the entry /^UID/ { id = gensub("\r", "", "g", $2); } /^LOCATION/ { location = gensub("\r", "", "g", $2); } /^STATUS/ { status = gensub("\r", "", "g", $2); } # 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 if(max_age<0 || ( lasttimestamp>0 && systime()--<" time2 else if (rrend != "") date = date ">--<" rrend # translate \n sequences to actual newlines and unprotect commas (,) if (condense) print "* <" date "> " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) else print "* " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) print ":PROPERTIES:" print ":ID: " id if(length(location)) print ":LOCATION: " location if(length(status)) print ":STATUS: " status print ":END:" if (! condense) print "<" date ">" print "" # translate \n sequences to actual newlines and unprotect commas (,) if(length(entry)>1) print gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry))); # output original entry if requested by 'original' config option if (original) print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry) } } # 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) { # print "________" # print "input : " input # convert the iCal Date+Time entry to a format that mktime can understand spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1 \\2 \\3 \\4 \\5 \\6", "g", input); # print "spec :" spec stamp = mktime(spec); 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][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\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][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\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][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3 00:00", "g", input); } else { # just generate the desired representation of the input date, without time; return gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3", "g", input); } } # return the date and day of week return strftime("%Y-%m-%d %a", stamp); } # 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: