--- /dev/null
+#!/bin/zsh
+
+# Copyright 2014 - 2018 Paul Hänsch
+#
+# This file is part of Confetti.
+#
+# Confetti is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Confetti is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Confetti. If not, see <http://www.gnu.org/licenses/>.
+
+# This is a parsing library for the Personal Data Interchange format (PDI)
+# PDI is the format for encoding VCard (.vcf) and iCalendar (.ics) files
+
+[ -n "$include_pdi" ] && return 0
+include_pdi="$0"
+
+BR='
+'
+
+unescape() {
+ local unescape='s;(^(\\\\)*|[^\\](\\\\)*)\\n;\1\n;g; s;\\(.);\1;g'
+ if [ $# -eq 0 ]; then
+ sed -E "$unescape"
+ else
+ printf %s "$*" \
+ | sed -E "$unescape"
+ fi
+}
+
+pdi_load() {
+ # normalise PDI file for processing with pdi_* functions
+ # functions in this library can only be applied to normalised data
+ # Usage example:
+ # data="$(pdi_load file.vcf)"
+
+ sed -srn '
+ # === Read entire file into buffer ===
+ :X $bY; N; bX; :Y s;^.*$;\n&\n;;
+
+ # === Join continuing lines, strip trailing CRs ===
+ s;\r*\n[ \t];;g;
+ s;\r*\n;\n;g;
+
+ # === turn property names to upper case, strip group names ===
+ s;\n([^;:\.\n]+\.)([^;:\n]+);\n\2;g;
+ :upcase
+ s;(\n[^;:]*)a;\1A;g; s;(\n[^;:]*)b;\1B;g; s;(\n[^;:]*)c;\1C;g; s;(\n[^;:]*)d;\1D;g; s;(\n[^;:]*)e;\1E;g;
+ s;(\n[^;:]*)f;\1F;g; s;(\n[^;:]*)g;\1G;g; s;(\n[^;:]*)h;\1H;g; s;(\n[^;:]*)i;\1I;g; s;(\n[^;:]*)j;\1J;g;
+ s;(\n[^;:]*)k;\1K;g; s;(\n[^;:]*)l;\1L;g; s;(\n[^;:]*)m;\1M;g; s;(\n[^;:]*)n;\1N;g; s;(\n[^;:]*)o;\1O;g;
+ s;(\n[^;:]*)p;\1P;g; s;(\n[^;:]*)q;\1Q;g; s;(\n[^;:]*)r;\1R;g; s;(\n[^;:]*)s;\1S;g; s;(\n[^;:]*)t;\1T;g;
+ s;(\n[^;:]*)u;\1U;g; s;(\n[^;:]*)v;\1V;g; s;(\n[^;:]*)w;\1W;g; s;(\n[^;:]*)x;\1X;g; s;(\n[^;:]*)y;\1Y;g;
+ s;(\n[^;:]*)z;\1Z;g;
+ t upcase;
+
+ # === Insert empty attribute fields where no attributes are present ===
+ s;\n([^;:]+):;\n\1\;:;g;
+
+ # === Unscramble aggregated fields ===
+ :disag
+ s;\n([^:\n]+:)(([^\n]*[^\])?(\\\\)*),;\n\1\2\n\1;;
+ t disag;
+
+ # === Insert FN when only N is present ===
+ /\nFN[;:]/!{
+ s,\nN(;[^:]*)?:([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);([^;\n]*);?\n,&FN;:\5 \3 \4 \2 \6\n,;
+ :despace
+ s,(\nFN;:[^\n]*) ([^\n]*\n),\1 \2,;
+ s,(\nFN;:) ([^\n]*\n),\1\2,;
+ s,(\nFN;:[^\n]*) (\n),\1\2,;
+ t despace;
+ }
+ /\nFN[;:]/!{ s,\n(N[;:][^\n]*)\n,&F\1\n,; } # Fallback
+
+ # === Normalise various known vendor properties ===
+ s;\nX-MS-CARDPICTURE(\;|:);\nPHOTO\1;g;
+ s;\nX-GENDER(\;|:);\nGENDER\1;g;
+ s;\nX-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+ s;\nX-EVOLUTION-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+ s;\nX-KADDRESSBOOK-X-ANNIVERSARY(\;|:);\nANNIVERSARY\1;g;
+ s;\nX-EVOLUTION-BLOG-URL(\;|:);\nURL\1;g;
+ s;\nAGENT(\;|:);\nRELATED\;VALUE=text\;TYPE=agent\1;g;
+ s;\nX-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+ s;\nX-EVOLUTION-ASSISTANT(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+ s;\nX-KADDRESSBOOK-X-ASSISTANTSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=assistant\1;g;
+ s;\nX-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+ s;\nX-EVOLUTION-MANAGER(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+ s;\nX-KADDRESSBOOK-X-MANAGERSNAME(\;|:);\nRELATED\;VALUE=text\;TYPE=manager\1;g;
+ s;\nX-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+ s;\nX-EVOLUTION-SPOUSE(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+ s;\nX-KADDRESSBOOK-X-SPOUSENAME(\;|:);\nRELATED\;VALUE=text\;TYPE=spouse\1;g;
+
+ # === Normalise obsolete vendor IM properties ===
+ s;\nX-AIM((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+ s;\nX-ICQ((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:aim:;g;
+ s;\nX-GOOGLE-TALK((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+ s;\nX-JABBER((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:xmpp:;g;
+ s;\nX-MSN((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:msn:;g;
+ s;\nX-YAHOO((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):;\nIMPP\1:ymsgr:;g;
+ s;\nX-SIP((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(sip:)?;\nIMPP\1:sip:;g;
+
+ # === Update obsolete LABEL property ===
+ s;\nLABEL((\;[A-Za-z0-9-]+|\;[A-Za-z0-9-]+=([^;,:"]+|"[^"]+")(,[^;,:"]+|,"[^"]+")*)*):(.*)\n;\nADR\1\;LABEL="\5":\n;g;
+
+ p;' "$@"
+}
+
+pdi_count(){
+ local card="$1" name="$2" rc='' cnt=0
+ while rc="${card#*${BR}${name};}"; do
+ [ "${rc}" != "${card}" ] || break
+ card="$rc"
+ cnt=$(($cnt + 1))
+ done
+ printf %i\\n $cnt
+}
+
+pdi_attrib(){
+ local card=":$1" name="$2" cnt="${3:-1}" attr="$4"
+ while [ $cnt -gt 0 ]; do
+ [ "${card#*${BR}${name};}" = "$card" ] && return 1
+ card="${card#*${BR}${name};}"
+ cnt=$((cnt - 1))
+ done
+ card="${card%%:*}"
+ if [ "$attr" ]; then
+ case $card in
+ *\;"$attr"=*) card="${card#*;${attr}=}";;
+ "$attr"=*) card="${card#${attr}=}";;
+ "$attr"|*\;"$attr"|"$attr"\;*|*\;"$attr"\;*) return 0;;
+ *) return 1;;
+ esac
+ case $card in
+ \"*\"\;*|\'*\'\;*)
+ card="${card#[\"\']}"; card="${card%%[\"\'];*}"
+ ;;
+ \"*\"|\'*\')
+ card="${card#[\"\']}"; card="${card%%[\"\']}"
+ ;;
+ *\;*) card="${card%%;*}";;
+ esac
+ fi
+ printf %s\\n "${card}"
+}
+
+pdi_value(){
+ local card="${BR}$1" name="$2" cnt="${3:-1}"
+ while [ "$cnt" -gt 0 ]; do
+ [ "${card#*${BR}${name};*:}" = "$card" ] && return 1
+ card="${card#*${BR}${name};*:}"
+ cnt=$((cnt - 1))
+ done
+ printf %s\\n "${card%%${BR}*}"
+}
+
+pdi_update_value(){
+ local card="${BR}$1" name="$2" cnt="$3" val="$4"
+ while [ "$cnt" -gt 0 ]; do
+ if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+ printf '%s\n%s;:' "${card%${BR}END;:VCARD*}" "${name}"
+ card="${BR}END;:VCARD"
+ break;
+ else
+ printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+ card="${card#*${BR}${name};}"
+ printf '%s:' "${card%%:*}"
+ card="${card#*:}"
+ fi
+ cnt=$((cnt - 1))
+ done
+ printf '%s\n%s\n' "$val" "${card#*${BR}}"
+}
+
+pdi_update_attrib(){
+ local card="${BR}$1" name="$2" cnt="$3" val="$4"
+ while [ "$cnt" -gt 0 ]; do
+ if [ "${card#*${BR}${name};*:}" = "${card}" ]; then
+ printf '%s\n%s;' "${card%${BR}END;:VCARD*}" "${name}"
+ card=":${BR}END;:VCARD"
+ break;
+ else
+ printf '%s\n%s;' "${card%%${BR}${name};*}" "${name}"
+ card="${card#*${BR}${name};}"
+ fi
+ cnt=$((cnt - 1))
+ done
+ printf '%s:%s\n' "$val" "${card#*:}"
+}