📅 Filtering .ics Calendar Events with awk on Linux

  • State “DONE” from [2025-05-09 Fri 12:18]

Welcome back to the Linux CLI tools series. Today’s focus is on awk, a versatile text-processing tool, and how to use it to filter out events from a calendar file. This had a specific use case for me, because I wanted to filter out all events from an exported .ics file before a certain date. I needed to import the filtered events to a new calendar and want to avoid duplicating events. So, my main goal: Delete all calendar events before April 1, 2025, from a .ics file, using just the command line.

The Use Case

Let’s say, you’ve exported a calendar to .ics format. It contains events like:

BEGIN:VEVENT
UID:event1@example.com
DTSTART;TZID=Africa/Addis_Ababa:20250331
SUMMARY:Outdated Event
END:VEVENT
BEGIN:VEVENT
UID:event2@example.com
DTSTART:20250401T090000Z
SUMMARY:Relevant Event
END:VEVENT

We want to delete everything before April 1, 2025, but preserve the calendar structure. Now, we could use a calendar program to just delete the events you do not want. But this can easily become a pain in the ass if there are a lot of events in there.

The script

Here’s a complete solution using awk:

awk '
BEGIN {
    keep = 0
    in_event = 0
}
/^BEGIN:VEVENT/ {
    in_event = 1
    event = ""
    keep = 0
    next
}
/^END:VEVENT/ {
    event = event $0 ORS
    in_event = 0
    if (keep) print event
    next
}
in_event {
    event = event $0 ORS
    if ($0 ~ /^DTSTART/) {
        split($0, parts, ":")
        if (length(parts) > 1) {
            date = substr(parts[2], 1, 8)
            if (date >= "20250401") {
                keep = 1
            }
        }
    }
    next
}
{ if (!in_event) print }
' input.ics > output.ics

Explanation

So what is happening here? Let’s go through this section by section. AWK goes through a text file line by line and compares it against each of the blocks in the awk program.

First, we need to initialize to variables to keep track if we want to keep an event and a second variable to check if we are in an event or not.

  1. Initialize variables

    BEGIN {
        keep = 0
        in_event = 0
    }
    • keep: whether to keep the current VEVENT block
    • in_event: whether we are currently inside an event
  2. Begin a VEVENT block

    In this block we check if a line begins with BEGIN:VEVENT. If so, we set the in_event flag and start recording the event. We set the keep flag to zero because we do not know yet if we want to keep it.

    /^BEGIN:VEVENT/ {
        in_event = 1
        event = ""
        keep = 0
        next
    }
    • Start collecting lines into the event buffer and reset state flags.
  3. End a VEVENT block Check lines for the END:EVENT bit and then append all the lines. if it is marked to keep, print it into the file.

    /^END:VEVENT/ {
        event = event $0 ORS
        in_event = 0
        if (keep) print event
        next
    }
    • When an event ends, print it only if it’s marked to keep.
  4. Process lines inside VEVENT Check if we are in an event (is the in_event flag set?). If so, we want to find the line starting with DTSTART and extract the date from it. If the date is greater than the date given, set the keep flag.

    in_event {
        event = event $0 ORS
        if ($0 ~ /^DTSTART/) {
            split($0, parts, ":")
            if (length(parts) > 1) {
                date = substr(parts[2], 1, 8)
                if (date >= "20250401") {
                    keep = 1
                }
            }
        }
        next
    }
    • Detect and parse the DTSTART line, account for additional test like TZID or VALUE=DATE by splitting at the “:”. Extract just the YYYYMMDD portion.
  5. Pass through everything else

    { if (!in_event) print }
    • This ensures lines outside event blocks, like BEGIN:VCALENDAR or END:VCALENDAR, are preserved.

Why awk?

  • Lightweight and fast
  • No need for full-fledged calendar libraries
  • Easily adaptable to other criteria (e.g., SUMMARY, LOCATION)