Last active
January 19, 2026 06:18
-
-
Save mtesch-um/125f77da7b53a542480f2cce971b5f82 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -euo pipefail | |
| usage() { | |
| cat <<EOF | |
| Usage: $(basename "$0") <calendar.ics> <year> <month> [--rewrite-domain OLD NEW] | |
| Removes all events before the specified year/month and saves the result | |
| to a new file with .trimmed.ics suffix. | |
| Arguments: | |
| calendar.ics Path to the iCalendar file (absolute or relative) | |
| year 4-digit year (e.g., 2024) | |
| month 2-digit month (01-12) | |
| Options: | |
| --rewrite-domain OLD NEW | |
| Replace email domain OLD with NEW in all events. | |
| Handles iCalendar line folding automatically. | |
| Examples: | |
| $(basename "$0") ~/calendars/work.ics 2024 06 | |
| # Creates ~/calendars/work.trimmed.ics with events from June 2024 onwards | |
| $(basename "$0") ~/calendars/work.ics 2024 06 --rewrite-domain utilimarc.com smith-system.com | |
| # Also rewrites all @utilimarc.com emails to @smith-system.com | |
| Requirements (macOS): | |
| brew install coreutils # provides gcsplit | |
| EOF | |
| exit 1 | |
| } | |
| ics_unfold() { | |
| awk '{ | |
| if (substr($0, 1, 1) == " " || substr($0, 1, 1) == "\t") { | |
| printf "%s", substr($0, 2) | |
| } else { | |
| if (NR > 1) print "" | |
| printf "%s", $0 | |
| } | |
| } | |
| END { print "" }' | |
| } | |
| ics_fold() { | |
| awk '{ | |
| line = $0 | |
| if (length(line) <= 75) { | |
| print line | |
| } else { | |
| print substr(line, 1, 75) | |
| line = substr(line, 76) | |
| while (length(line) > 74) { | |
| print " " substr(line, 1, 74) | |
| line = substr(line, 75) | |
| } | |
| if (length(line) > 0) { | |
| print " " line | |
| } | |
| } | |
| }' | |
| } | |
| cleanup() { | |
| if [[ -n "${temp_dir:-}" && -d "$temp_dir" ]]; then | |
| rm -rf "$temp_dir" | |
| fi | |
| } | |
| trap cleanup EXIT | |
| if [[ $# -lt 3 ]]; then | |
| usage | |
| fi | |
| calendar_input="$1" | |
| year="$2" | |
| month="$3" | |
| shift 3 | |
| old_domain="" | |
| new_domain="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --rewrite-domain) | |
| if [[ $# -lt 3 ]]; then | |
| echo "Error: --rewrite-domain requires OLD and NEW arguments" >&2 | |
| exit 1 | |
| fi | |
| old_domain="$2" | |
| new_domain="$3" | |
| shift 3 | |
| ;; | |
| *) | |
| echo "Error: Unknown option: $1" >&2 | |
| usage | |
| ;; | |
| esac | |
| done | |
| if [[ ! "$year" =~ ^[0-9]{4}$ ]]; then | |
| echo "Error: Year must be 4 digits (e.g., 2024)" >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$month" =~ ^(0[1-9]|1[0-2])$ ]]; then | |
| echo "Error: Month must be 01-12" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$calendar_input" = /* ]]; then | |
| calendar_file="$calendar_input" | |
| else | |
| calendar_file="$(pwd)/$calendar_input" | |
| fi | |
| if [[ ! -f "$calendar_file" ]]; then | |
| echo "Error: Calendar file not found: $calendar_file" >&2 | |
| exit 1 | |
| fi | |
| calendar_dir="$(dirname "$calendar_file")" | |
| calendar_basename="$(basename "$calendar_file" .ics)" | |
| output_file="${calendar_dir}/${calendar_basename}.trimmed.ics" | |
| if command -v gcsplit &>/dev/null; then | |
| CSPLIT=gcsplit | |
| elif command -v csplit &>/dev/null; then | |
| if csplit --version 2>&1 | grep -q GNU; then | |
| CSPLIT="csplit" | |
| else | |
| echo "Error: GNU csplit required. On macOS, install with: brew install coreutils" >&2 | |
| exit 1 | |
| fi | |
| else | |
| echo "Error: csplit not found. On macOS, install with: brew install coreutils" >&2 | |
| exit 1 | |
| fi | |
| temp_dir=$(mktemp -d) | |
| temp_files_digits=5 | |
| cd "$temp_dir" | |
| total_files=$($CSPLIT -n "$temp_files_digits" "$calendar_file" '/BEGIN:VEVENT/' '{*}' 2>/dev/null | wc -l) | |
| total_files=$((total_files)) | |
| if [[ $total_files -lt 2 ]]; then | |
| echo "Error: No events found in calendar file" >&2 | |
| exit 1 | |
| fi | |
| echo "Found $((total_files - 1)) events in calendar" | |
| grep -l DTSTART xx* 2>/dev/null | while read -r file; do | |
| dtstart=$(grep -m1 'DTSTART' "$file" | head -1) | |
| if [[ "$dtstart" =~ ([0-9]{4})([0-9]{2}) ]]; then | |
| event_year="${BASH_REMATCH[1]}" | |
| event_month="${BASH_REMATCH[2]}" | |
| if (( 10#$event_year > 10#$year || (10#$event_year == 10#$year && 10#$event_month >= 10#$month) )); then | |
| cat "$file" | |
| fi | |
| fi | |
| done | grep -v '^END:VCALENDAR$' > events | |
| header_file="xx$(printf '%0*d' "$temp_files_digits" 0)" | |
| { | |
| cat "$header_file" | |
| cat events | |
| echo "END:VCALENDAR" | |
| } > "$output_file" | |
| if [[ -n "$old_domain" ]]; then | |
| echo "Rewriting @${old_domain} to @${new_domain}..." | |
| escaped_old=$(printf '%s' "$old_domain" | sed 's/[.[\*^$/]/\\&/g') | |
| escaped_new=$(printf '%s' "$new_domain" | sed 's/[&/\]/\\&/g') | |
| tr -d '\r' < "$output_file" \ | |
| | ics_unfold \ | |
| | sed "s/@${escaped_old}/@${escaped_new}/g" \ | |
| | ics_fold > "${output_file}.tmp" | |
| mv "${output_file}.tmp" "$output_file" | |
| fi | |
| kept_events=$(grep -c 'BEGIN:VEVENT' "$output_file" 2>/dev/null || echo 0) | |
| echo "Kept $kept_events events from ${year}-${month} onwards" | |
| echo "Output saved to: $output_file" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment