#!/bin/zsh

Myname="${0##*/}"
Mypath="${0:A}"
Version=2.09
setopt rematchpcre

:<<'DOC'
= contmgr - list contacts for your terminal iPad, Fritz!Box, hand bag

= Synopsis
contmgr [options] [searchkey]	

== General options:

-h,--help	Print this help and exit
-H,--Help	Show full documentation and exit
-V,--version	Print version and exit
-i,--input=X	Take input from file X with default extension vcf;
		if X has no path info, the file is supposed to reside
		in the same directory as the default input file.
-k,--key=X	select records containing key X in the CATEGORIES field
-w,--word	search key must be a complete word

== Format options:

-b,--booklet	print booklet
-l,--labels	print tex source for address labels
-L,--letter	print tex source for letters
-s,--sorter	print only first line of |--term| format
-T,--tagged	print tag / value list
-t,--term	print to terminal (this is the default format)
-v,--vcard	print vcards
-x,--xml	print xml for FritzBox

== Format specific options (b, l, t and T specify relevant formats):

-d,--dark	(t) do not use color in terminal output
-f,--font=X	(bl) set one or more of regular font, bold font, and pointsize.
		Default: |X='DejaVuSerif,DejaVuSerif-Bold 10'|
		Argument examples:
		|'a,b 12' 'a,b' 'a' 'a 12' 'a, 12' ',b' '12'|
		The last one (|'12'|) changes the pointsize only.
-N,--nocolor	(t) set colors for dark background
-n,--noarea=X	(bl) in phone numbers, remove area codes with the
		value X, possibly prefixed with 0 (zero)
-r,--return=X	(l) use X as return addres in labels, commas are replaced with
		bullets
-c,--counter	(b) prefix display name in booklet with a counter for reference
-B,--blind	(ltTv) Modifies the output for visually handicapped users:
                - Terminal output will be one-liners, with white space
                between the lines. Dots are inserted where appropriate, in
                order to have text-to-voice programs too pause there.
                - Label output and tagged output is modified by suffixing
                  label lines with a dot for the same reason. Labels will
                  be boxed to force reading horizontally.
                - Vcard output will get a NICKNAME entry containing most
                info. This nickname can easily be invoked to be read
                aloud on an iphone: he Siri, what is the nickname of
                Barack Obama?

= Description
contmgr lists your contacts to standard output. If you provide a search
key, only contacts containing that key are listed. With the |--word| option, 
the search key must occur as a whole word.

The input is a vcard file maintained in, for example, Thunderbird or the
iCloud. The name of the vcard file is is read from the configuration file,
see below, but it can be modified with the |--input| option.

There are some limitations on the data that are read from that vcard file.
The following table shows the vcard fields that are stored and the formats
(v=vcard, T=tagged, x=xml, t=terminal, b=booklet, l=label L=letter) in
which they are reproduced:

Last	vTxtblL	N 1
First	vTxtblL	N 2
Middle	vTxtb-	N 3
Prefix	vT-tblL	N 4
Suffix	vT-tblL	N 5
Nick	vTxtb-	NICKNAME
Disp	vT----	FN (unused, reconstructed from above entries)
Org	vTxtb-	ORG
Url	vT-t--	URL
Bday	vT-t--	BDAY
Hmail	vTxt--	EMAIL;TYPE=HOME
Wmail	vTxt--	EMAIL;TYPE=WORK
Hpobox	vT-tblL	ADR;TYPE=HOME 1
Hext	vT-tblL	ADR;TYPE=HOME 2
Hstreet	vT-tblL	ADR;TYPE=HOME 3
Hcity	vT-tblL	ADR;TYPE=HOME 4
Hstate	vT-tblL	ADR;TYPE=HOME 5
Hzip	vT-tblL	ADR;TYPE=HOME 6
Hccode	vT-tblL	ADR;TYPE=HOME 7
Wpobox	vT-tblL	ADR;TYPE=WORK 1
Wext	vT-tblL	ADR;TYPE=WORK 2
Wstreet	vT-tblL	ADR;TYPE=WORK 3
Wcity	vT-tblL	ADR;TYPE=WORK 4
Wstate	vT-tblL	ADR;TYPE=WORK 5
Wzip	vT-tblL	ADR;TYPE=WORK 6
Wccode	vT-tblL	ADR;TYPE=WORK 7
Keys	vT-t--	CATEGORIES
Hphone	vTxt--	TEL;TYPE=HOME
Wphone	vTxt--	TEL;TYPE=WORK
Mphone	vTxt--	TEL;TYPE=CELL
Note	vT-tb-	NOTE

Note that the last (7th) field of the ADR entry is supposed to be a country
code, like NL for The Netherlands. Vcard exports of most applications tend
to put the name of the country here, in the language set in that
application. contmgr tries to replace such names with the corresponding
country code, using the information in the |CCodes| associative array. When
a country name has to be written, in an address label for example, this
country code is used to find, in |CCodes|, the country name in the language
of the user, |Mylanguage|, as defined in the user configuration file.

Currently, country names are defined in |CCodes| in 6 languages: EN, FR,
ES, IT, and NL.

= Formats

contmgr can print in various formats, specified in the |--format| option.
Currently, the following formats are available:

== booklet
This produces a latex source on standard output, to be printed twosided on
A4 paper, which can be folded to create an A5 booklet.

== labels
Produces a latex source on standard output, to be printed on A4 sheets with
sticky labels.

== letter
Produces a latex source on standard output, containing one letter for each
address. The content of the letter has to be insertes afterwards.

== sorter
Lists the records, one line for each, with name, organisation, birth day,
age and any tags.

== tagged
Lists each record completely, one field per line, prefixed with the
fields tags.

== term
This is the default. Lists each record completely, with Home data on the
left, work data on the right, mobile numer in the middle.

== vcard
Prints the reformatted vcard file, sorting the fields and the records,
cleaning up unneeded fields.

= Configuration
The defaults for some variables, like font, fontsize, vcard file, and
country code, may be inconvenient. You can set your own defaults in a
configuration file, either in system file: |$PREFIX/contmgr.conf| or in a
user file: |$HOME/.contmgr.conf|.
These files are read in this order and they can contain input such as this:

   Fontsize=10
   Font='DejaVuSerif'
   Bold='DejaVuSerif-Bold'
   Input="$HOME/Contacts.vcf"
   Myccode=NL
   Cols=3
   Rows=8
   Paperwidth=210
   Paperheight=297
   LetterStyle=iso

The content shown here would have no effect, as its values are the
defaults already.

= Todo
== Country code
The vCard format lacks an entry for the country code, needed for formatting
the zip code and for zip code positioning in address labels. There is of
course an entry for the country, but that can be expressed in any language,
preferably the local language.

My solution for now is to expect two vcard entries in the vcard file:
|X-CCODE-HOME| and |X-CCode-WORK|, which set the country codes for home
and work addresses. By default, the users own country code |Myccode|, as set
in the configuration file, is used. So normally, these entries are needed
only for foreign addresses.

== Phone number formatting
I suppose phonenumbers to have the format |+can|, where |c| is 1-3
country code digits, a is 1-3 area code digits and n is 1 or more phone
number digits.
If the |c|'s match the value of |Myccode| (say xx), this is converted to
|0xx an|; if not, to |00xx an|. After this, I match the |a|'s with an array
|AreaCodes| and I use the match (say yyy) to convert to |00xx yyy n|.
Finally, the |n|'s are spaced, depending on the number of |n|'s.
Problem: the |AreaCodes| array is valid for the Netherlands only.

= Author
[Wybo Dekker](wybo@dekkerdocumenten.nl)

= Copyright
Released under the [GNU General Public License](www.gnu.org/copyleft/gpl.html)
DOC

    die() { local i; for i; do echo -e "$Myname: $Red$dn: $i$Nor"; done 1>&2; exit 1; }
   Warn() { local i; for i; do displayname>/dev/null; echo -e "$Myname: $Mag$dn$i$Nor"; done 1>&2; }
helpsrt() { sed -n '/^= Synopsis/,/^= /p' "$Mypath"|sed '1d;$d'; exit; }
helpall() { sed -n "/^:<<'DOC'$/,/^DOC/p" "$Mypath"|sed '1d;$d'|
            less -P"$Myname-${Version/./·} (press h for help, q to quit)";exit; }
sorter() { cat; }


for i in awk csplit getopt mktemp pnc realpath xargs; do
  command -v $i  >/dev/null || die "Please install $i"
done

:<<'DOC' #----------------------------------------------------------------------
= handle_options
synopsis:	 handle_options "$@"
description:	handle the options.
globals used:	 Myname Version AreaCodes
globals  set:	 Args Nodial Grepopt Format Return Fontsize Font Bold Input
		 Mycountry Key
returns:	the number of remaining arguments
DOC
#-------------------------------------------------------------------------------
handle_options() {
   local options
   options=$(getopt \
      -n "$Myname" \
      -o bcdBf:HhIi:k:lLNn:r:sTtvVwx \
      -l booklet,counter,dark,blind,font:,Help,help,input:,key:,labels,letter,noarea: \
      -l nocolor,return:,sorter,tagged,term,vcard,version,word,xml \
      -- "$@"
   ) || exit 1
   eval set -- "$options"
   
   while true; do
      case $1 in
# General options:

      (-h|--help)	# Print this help and exit
			helpsrt
			;;
      (-H|--Help)	# Show full documentation and exit
			helpall
			;;
      (-V|--version)	# Print version and exit
			echo "$Version"
			exit
			;;
      (-i|--input)	# Take input from file X with default extension vcf;
			# if X has no path info, the file is supposed to reside
			# in the same directory as the default input file.
			if [[ $2 =~ / ]]; then # path info given?
			   Input="$2"
			else # no path info? then look in default input directory
			   Input="${Input%/*}/$2"
			fi
			Input="${Input%.vcf}.vcf"
			[[ -e $Input ]] || die "Input file $Input does not exist"
			shift 2
			;;
      (-k|--key)	# select records containing key X in the CATEGORIES field
			Key="$2"
			shift 2
			;;
      (-w|--word)	# search key must be a complete word
			Grepopt=-wirl
			shift
			;;
# Format options:

      (-b|--booklet)	# print booklet
			Format=booklet
			shift
			;;
      (-l|--labels)	# print tex source for address labels
			Format=labels
			shift
			;;
      (-L|--letter)	# print tex source for letters
			Format=letter
			shift
			;;
      (-s|--sorter)	# print only first line of |--term| format
			Format=sorter
			shift
			;;
      (-T|--tagged)	# print tag / value list
			Format=tagged
			shift
			;;
      (-t|--term)	# print to terminal (this is the default format)
			Format=term
			shift
			;;
      (-v|--vcard)	# print vcards
			Format=vcard
			# sort by FN:
			sorter() {
			   msort \
			   --suppress-log \
			   --block \
			   --tag="FN:" \
			   --fold-case  \
			   --quiet 2>/dev/null
			}
			shift
			;;
      (-x|--xml)	# print xml for FritzBox
			Format=xml
			shift
			;;
# Format specific options (b, l, t and T specify relevant formats):

      (-d|--dark)	# (t)   set colors for dark background
			dark
			shift
			;;
      (-f|--font)       # (bl)  set font and/or bold font and/or pointsize.
			#       Default: X='DejaVuSerif,DejaVuSerif-Bold 10'
			#       Argument examples:
			#        'a,b 12' 'a,b' 'a' 'a 12' 'a, 12' ',b' '12'
			#       The last one (|'12'|) changes the pointsize only.
			if [[ $2 =~ ([[:alnum:]_-]*)(,[[:alnum:]_-]*)?( [0-9]+)?$ ]]; then
			   f=$match[1]
			   b=${match[2]#,}
			   s=${match[3]# }
			  if [[ $f =~ ^[[:digit:]]+ ]]; then
			     Fontsize="$f"
			  else
			     [[ -n $f ]] && Font="$f"
			     [[ -n $b ]] && Bold="$b"
			     [[ -n $s ]] && Fontsize="$s"
			  fi
			fi
			shift 2
			;;
      (-N|--nocolor)    # (t)   do not use color in terminal output
			nocolor
			shift
			;;        
      (-n|--noarea)	# (bl)  in phone numbers, remove area codes with the
			#       value X, possibly prefixed with 0 (zero)
			Nodial=${2#0}
			[[ "${AreaCodes[*]}" =~ $Nodial ]] ||
			   die "Unrecognized value for --noarea option"
			shift 2
			;;
      (-r|--return)	# (l)   use X as return addres in labels, commas are 
			#       replaced with bullets
			Return="${2//,/\\bul }"
			shift 2
			;;
      (-c|--counter)	# (b)   prefix display name with a counter
			counter=true
			shift
			;;
      (-B|--blind)	# (ltT) suffix label lines with a dot for visually
			#       handicapped, text-to-voice progs will pause there
			#       Labels will be boxed to force reading horizontally.
			#       Creates a nickname in vcards containing most
			#       info. The nickname can easily be invoked to be
			#       read aloud on an iphone.
			Dot=.
			Blind=true
			shift
			;;
      (-I)		instscript "$Mypath" ||
			   die 'the -I option is for developers only'
			exit
			;;
      (--)		shift
			break
			;;
      (*)		break
			;;
      esac
   done
   Args=( "$@" )
}

:<<'DOC' #---------------------------------------------------------------------
= validateemail
description:	validate a comma+space-separated email list
DOC
#-------------------------------------------------------------------------------
validateemail() {
   [[ -n $2 ]] && die "validateemail: expect only 1 argument"
   while IFS=', ' read -r e; do
      [[ "$e" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$ ]] ||
        Warn "Invalid email address: $e"
   done <<<"${1//, /$'\n'}"
}

:<<'DOC' #---------------------------------------------------------------------
= light
description:	set colors for light background
DOC
#-------------------------------------------------------------------------------
light() {
   RED='\e[1;38;5;9m'
   Red='\e[38;5;1m'
   Mag='\e[38;5;5m'
   Nor='\e[0m'
   Yel='\e[38;5;3m'
   Blu='\e[38;5;4m'
}

:<<'DOC' #---------------------------------------------------------------------
= dark
description:	set colors for dark background
DOC
#-------------------------------------------------------------------------------
dark() {
   Red='\e[1;38;5;9m'
   RED='\e[38;5;1m'
   Mag='\e[1;38;5;13m'
   Nor='\e[0m'
   Yel='\e[1;38;5;11m'
   Blu='\e[1;38;5;6m'
}

:<<'DOC' #---------------------------------------------------------------------
= nocolor
description:	set no colors
DOC
#-------------------------------------------------------------------------------
nocolor() {
   Red=''
   Mag=''
   Nor=''
   Yel=''
   Blu=''
}

:<<'DOC' #---------------------------------------------------------------------
= join
synopsis:	 join separator string [string...]
description:	use first argument as separator to print other non-empty
		arguments separated with it
		 join '=' a b '' d → a=b=d
DOC
#-------------------------------------------------------------------------------
join() {
   local x='' sep="$1"
   shift
   while [ $# -gt 0 ]; do
      [[ -n $1 ]] && x+="${sep}$1"
      shift
   done
   echo "${x##"${sep//\\/\\\\}"}"
}

:<<'DOC' #---------------------------------------------------------------------
= pr_age
description:	print age; On March 13, 2019:
		after Bday=2011-03-22
		pr_age prints (7) or, for blind people: age: 7
DOC
#-------------------------------------------------------------------------------
pr_age() {
   local byear bmonth bday cyear cmonth cday corr age
   if [[ -n $Bday  ]]; then
      IFS=- read -r byear bmonth bday <<<"$Bday"
      IFS=- read -r cyear cmonth cday <<<"$(date +%Y-%m-%d)"
      corr=0
      if [[ 1"$cmonth" -lt 1"$bmonth" ]] ||
         [[ 1"$cmonth" -eq 1"$bmonth" && 1"$cday" -lt 1"$bday" ]]
      then
         corr=-1
      fi
      age=$((cyear-byear+corr))
      if $Blind; then
         echo "Age: $age"
      else
         echo "($age)"
      fi
   fi
 
}

:<<'DOC' #---------------------------------------------------------------------
= pr_city
synopsis:	 pr_city zip city state countrycode [Blind]
description:	Try to format city, state and zip
		if rules not known, print "city, state, zip"
		 pr_city 4158CH Deil NL → 4158 CH Deil
		 pr_city 94595 'Walnut Creek' CA US → Walnut Creek CA 94595
		If there is a 5th argument and it is true, zip codes are
		printed without spaces: this is useful for blind people,
		because spaces cause pronunciation problems for speaking
		programs.
DOC
#-------------------------------------------------------------------------------
pr_city() {
   local ccode
   [[ -z $2 ]] && return # no city = no address
   if [[ -z $4 ]]; then 
      Warn "Missing country code for $1 $2; using $Myccode"
      ccode=$Myccode
   else
      ccode=$4
   fi
   space=' '
   [[ -n $5 ]] && $5 && space=''
   case "$ccode" in
    (LU) # Luxemburg
	 echo -n "L-${1//[L-]/} $2" # L-1234
	 ;;
    (GB) # UK
	 echo -ne "$2\n${1%???}$space${1: -3}" # all but last 3 <space> last 3
	 ;;
    (FR|ES|CH|AT|NO|DR|FI|HR|MC|PT) # France, Spain, Switserland, Austria, ...
	 echo -n "$1 $2" 
	 ;;
    (BE) # Belgium
	 echo -n "B-${1//[B-]/} $2" # B-1234
	 ;;
    (NL) # Netherlands
	 echo -n "${1:0:4}$space${1:4:2} $2" # first 4 <space> last 2
	 ;;
    (DE) # Germany
	 echo -n "$1 $2"
	 ;;
    (IT) # Italy:
	 echo -n "$1 $2 $3"
	 ;;
    (US) # USA
	 echo -n "$2 ${1:0:2}$space${1:2:5}"
	 ;;
     (*) Warn "Please add $ccode to my pr_city function"
	 ;;
   esac
}

:<<'DOC' #---------------------------------------------------------------------
= fix
synopsis:	 fix phonenumber
description:	Use pnc to insert spaces in the phonenumber:
		+31630333955 => +31 6 30333955
		If the country code is that of the user's country, replace it,
		plus the space after it, with 0. 
		+31 6 30333955 => 06 30333955
		If there are 6 or more digits at the end, insert a space in
		their middle:
		For other countries, the + is still there; replace it with 00.
		Example, with Myccode=NL:
		  x=+31630333955; fix x; echo $x => 06 3033 3955
		  x=+32630333955; fix x; echo $x => 0032 6 3033 3955
returns:	1 if the phone number is empty, else 0
DOC
#-------------------------------------------------------------------------------
fix() {
   local p
   eval "p=\$$1" # example: p=HPhone
   test -n "$p" || return 1
   pnc valid "$p" || Warn "Invalid phone number: «$p»"
   out="$(pnc format -f int "$p")"
   out="${out/\+${Mydial} /0}"
   # 6 or more digits at the end? Then put a space in the middle of them:
   end=$(expr "$out" : '.* \([0-9]*\)')
   if [[ ${#end} -ge 6 ]]; then
      middle=$(( ${#end} / 2 ))
      out="${out%"$end"}${end[1,$middle]} ${end[$((middle + 1)),-1]}"
   fi
   eval "$1=\${out/#+/00}\$ln"
}


:<<'DOC' #---------------------------------------------------------------------
= texescape
synopsis:	 texescape varname
description:	escape #, _, %, & in the variable named in the argument
		 x='50%'; texescape x; echo $x → 50\%
DOC
#-------------------------------------------------------------------------------
texescape() {
  local escaped string
  eval "string=\$$1"
  for i in ${(s::)string}; do
    [[ $i =~ [%#_\&] ]] && escaped+='\\'
    escaped+="$i"
  done
  eval "$1=\$escaped"
}

:<<'DOC' #---------------------------------------------------------------------
= displayname
description:	print Last, First (Organization), but clean up if some are empty.
		 First=Wybo Last=Dekker Org=DekDoc displayname → Dekker, Wybo (DekDoc)
		 First=Wybo Last=Dekker Org=''     displayname → Dekker, Wybo
		 First=''   Last=Dekker Org=DekDoc displayname → Dekker (DekDoc)
		 First=''   Last=''     Org=DekDoc displayname → DekDoc
DOC
#-------------------------------------------------------------------------------
displayname() {
   local dn
   dn="$Last $Suffix, $Prefix $First $Middle $Nick ${Org:+(}$Org${Org:+)}"
   # Remove leading spaces and commas
   dn="${dn#"${dn%%[! ,]*}"}"
   # Remove trailing spaces and commas
   dn="${dn%"${dn##*[! ,]}"}"
   # reduce multiple spaces to 1
   dn=${(S)dn//  */ }
   # remove any space before a comma
   dn=${dn// ,/,}
   [[ $dn =~ '^\((.*)\)$' ]] && dn=$match[1] # remove () if they are at begin and end
   echo "${dn:-—no name—}"
}

:<<'DOC' #---------------------------------------------------------------------
= setvar
synopsis:	 setvar varname value vardescription 
description:	set named variable to value, but only if it's empty:
		Example: setvar Hphone +31345652152 'home phone'
DOC
#-------------------------------------------------------------------------------
setvar() {
   local current
   eval "current=\$$1"
   if [[ -n $current ]]; then 
      Warn "Skipping $2: only one value for $3 allowed"
   else
      eval "$1='$2'"
   fi
}

:<<'DOC' #---------------------------------------------------------------------
= pr_sorter
description:	print only first line of terminal format
DOC
#-------------------------------------------------------------------------------
pr_sorter() {
   printf "%b %b %b %b\n" "$Disp" "$Bday" "$(pr_age)" "${Keys[*]}"
}

:<<'DOC' #---------------------------------------------------------------------
= pr_tagged
description:	print in tag:value format
DOC
#-------------------------------------------------------------------------------
pr_tagged() {
   hc=$(pr_city "$Hzip" "$Hcity" "$Hstate" "$Hccode")
   wc=$(pr_city "$Wzip" "$Wcity" "$Wstate" "$Wccode")
   dn="$Prefix $First $Middle $Last $Suffix"
   home=''
   work=''
   [[ -n "$Hstreet$hc$Hcountry$Hphone$Hmail" ]] && home=':x:'  
   [[ -n "$Wstreet$wc$Wcountry$Wphone$Wmail" ]] && work=':x:'  
   if [[ $work$home =~ ^:x:$ ]]; then
      work=''
      home=''
   fi
   fix Hphone
   fix Mphone
   fix Wphone
   a=("Naam:		$Disp"
      "Organisatie:	$Org"
      "Bijnaam:		$Nick"
      "Geboortedatum:	$Bday"
      "Marker;		${Keys[*]}"
      "Mobiel:		$Mphone"
      "Gegevens thuis:	$home"
      "Adres:		$Hpobox $Hstreet $hc $Hcountry"
      "Telefoon:	$Hphone"
      "E-mail:		$Hmail"
      "Gegevens werk:	$work"
      "Adres:		$Wpobox $Wstreet $wc $Wcountry"
      "Telefoon:	$Wphone"
      "E-mail:		$Wmail"
      "Notitie:		$Note"
     )
   for i in "${a[@]}"; do
      IFS=: read -r tag val <<<"$i"
      [[ $val =~ [[:alnum:]] ]] && echo "$tag: $val"
   done |
   sed "s/\t\+/\t/
	s/:\s\+:x://
	s/^://
	s/\\\\n/$Dot\\n\t/
	s/  */ /g   # squeeze spaces
	s/\. /.~/g
	s/\t /\t/   # remove space from missing first name
	s/.$/&$Dot/ # add $Dot to non-empty lines
   " |
   expand -16
   echo
}

:<<'DOC' #---------------------------------------------------------------------
= pr_oneliner
description:	print in oneliner terminal format
DOC
#-------------------------------------------------------------------------------
pr_oneliner() {
   fix Hphone
   fix Wphone
   fix Mphone
   hc=$(pr_city "$Hzip" "$Hcity" "$Hstate" "$Hccode" $Blind)
   wc=$(pr_city "$Wzip" "$Wcity" "$Wstate" "$Wccode" $Blind)
   h="$Hpobox, $Hext, $Hstreet, $hc, $Hcountry"
   w="$Wpobox, $Wext, $Wstreet, $wc, $Wcountry"
   [[ $h = ', , , , ' ]] && h=''
   [[ $w = ', , , , ' ]] && w=''
   for i in \
	"$Disp" \
	"${Bday:+Born: }$Bday" \
	"$(pr_age)" \
	"${h:+Home: }$h" \
	"${w:+Work: }$w" \
   	"$Hphone" \
   	"$Mphone" \
   	"$Wphone"
   do
      [[ -n $i ]] && printf "%s. " "$i"
   done |
   sed 's/ ,//g;s/ \.//g' |
   fmt -w 95
   echo
   echo
}

:<<'DOC' #---------------------------------------------------------------------
= pr_term
description:	print in terminal format
DOC
#-------------------------------------------------------------------------------
pr_term() {
   hc=$(pr_city "$Hzip" "$Hcity" "$Hstate" "$Hccode")
   wc=$(pr_city "$Wzip" "$Wcity" "$Wstate" "$Wccode")
   echo -e "$Nor———————————————————————————————————————————————————————————"
   printf "$Yel%-44s%-10s %-5s$Nor\n" "$Disp" "$Bday" "$(pr_age)"
   [[ -n "$Hpobox$Wpobox" ]] && printf "%-30s%-30s\n" "$Hpobox" "$Wpobox"
   [[ -n "$Hext$Wext" ]] && printf "%-30s%-30s\n" "$Hext" "$Wext"
   [[ -n "$Hstreet$Wstreet" ]] && printf "%-30s%-30s\n" "$Hstreet" "$Wstreet"
   [[ -n "$hc$wc" ]] && printf "%-30s%-30s\n" "$hc" "$wc"
   [[ -n "$Hcountry$Wcountry" ]] && printf "%-30s%-30s\n" "$Hcountry" "$Wcountry"
   fix Hphone
   fix Mphone
   fix Wphone
   [[ -n "$Hphone$Mphone$Wphone" ]] &&
      printf "%-20s$Mag%-20s$Nor%-20s\n" "$Hphone" "$Mphone" "$Wphone"
   [[ -n "$Hmail$Wmail" ]] && printf "$Blu%-27s%27s$Nor\n" "$Hmail" "$Wmail"
   [[ -n $Note ]] && echo -e "${Note%'\n'}" |fmt -60
   (( ${#Keys[@]} > 0 )) && printf "${RED}tags:$Nor %s\n" "${Keys[*]}"
}

:<<'DOC' #---------------------------------------------------------------------
= pr_startlabels
description:	print preamble for address labels
DOC
#-------------------------------------------------------------------------------
pr_startlabels() { local awa=''
   [[ -n $Return ]] && awa='\awa'
   cat <<-EOD
	%%!lualatex
	%%=== labels ===
	\documentclass{memoir}
	\usepackage[papersize={${Paperwidth}mm,${Paperheight}mm}]{geometry}
	\usepackage{fontspec}
	\protrudechars=2
	\adjustspacing=2
	\newfontfeature{Microtype}{protrusion=default;expansion=default}
	\directlua{fonts.protrusions.setups.default.factor=.5}
	\setmainfont{$Font}[BoldFont=$Bold]
	\usepackage{polyglossia}\setmainlanguage{dutch}
	\usepackage{labels}
	\LabelRows=$Rows
	\LabelCols=$Cols
	\TopBorder=0mm
	\BottomBorder=0mm
	\LeftBorder=7mm
	\RightBorder=7mm
	\LabelGrid$Blind
	\newcommand{\bul}{{\hfill$\bullet$}\hfill}
	\newcommand{\awa}{\begin{minipage}{56mm}
	  \textbf{\tiny $Return}\\\\%[-.5ex]
	  \protect\rule[2ex]{56mm}{.2mm}
	  \end{minipage}\\\\[-3ex]
	}
	\def\C{$awa}
	\begin{document}
	\fontsize{$Fontsize}{$((Fontsize+2))}\selectfont
	\begin{labels}
	EOD
}

:<<'DOC' #---------------------------------------------------------------------
= pr_startletter
description:	print preamble for letter
DOC
#-------------------------------------------------------------------------------
pr_startletter() { local awa=''
   [[ -n $Return ]] && awa='\awa'
   cat <<-EOD
	%!lualatex
	\documentclass{isodoc}
	\usepackage{$LetterStyle}                                                              
	\setupdocument{
	    ourref = {${LetterReference:-===reference===}},
	      date = {${LetterDate:-today}},
	   subject = {${LetterSubject:-===subject===}},
	}
	\def\Letter{\letter{%
	${LetterContent:-===Letter content===}
	}}
	\def\cleardoublepage{}
	\begin{document}
	\fontsize{$Fontsize}{$((Fontsize+2))}\selectfont
	EOD
}

:<<'DOC' #---------------------------------------------------------------------
= trim
description:	trim and squeeze named variable
DOC
#-------------------------------------------------------------------------------
trim() {
  local x
  eval "x=\${$1%%+([[:space:]])}"	# get the value of the named argument
  x=${(MS)x##[[:graph:]]*[[:graph:]]}	# trim spaces
  while [[ $x == *"  "* ]]; do x="${x//  / }"; done # squeeze multiple spaces
  eval "$1=\${x//+([[:space:]])/ }"	# put trimmed and squeezed value back
}

:<<'DOC' #---------------------------------------------------------------------
= pr_label
description:	print in home and work addresses label format
DOC
#-------------------------------------------------------------------------------
pr_label() {
  local i j ext street zip city state country ccode name pobox
   for i in H W; do
      for j in pobox ext street zip city state country ccode; do
         eval $j="\$$i$j"
      done
      [[ -n $ext ]] && street="$ext\n$street"
      [[ -n $street$pobox ]] || continue
      name="$Prefix $First $Last $Middle $Suffix"
      trim name
      for i in Org name street; do texescape $i; done
      echo "\\C"
      [[ -n $Org ]] && echo "$Org"
      [[ -n $name ]] && echo "$name"
      [[ -n $pobox ]] && echo -e "$pobox"
      [[ -n $street ]] && echo -e "${street//\\n/\\n}"
      pr_city "$zip" "$city" "$state" "$ccode" ""
      [[ -z $country ]] || printf '\n%s' "$country"
      echo
   done |
   sed "s/\. /.~/g
	s/...$/&$Dot/ # add $Dot to lines longer than 2 chars (not \C)
   "
}

:<<'DOC' #---------------------------------------------------------------------
= pr_letter
description:	print in home and work addresses letter format
DOC
#-------------------------------------------------------------------------------
pr_letter() {
  local i j ext street zip city state ccode country name pobox
   for i in H W; do
      for j in pobox ext street zip city state country ccode; do
         eval $j="\$$i$j"
      done
      [[ -n $ext ]] && street="$ext\n$street"
      [[ -n $street$pobox ]] || continue
      name="$Prefix $First $Last $Middle $Suffix"
      trim name
      for i in Org name street; do texescape $i; done
      printf '\\setupdocument{to={%%\n'
      [[ -n $Org ]] && printf '\t%s\\\\\n' "$Org"
      [[ -n $name ]] && printf '\t%s\\\\\n' "$name"
      [[ -n $pobox ]] && printf '\t%s\\\\\n' "$pobox"
      [[ -n $street ]] && printf '\t%s\\\\\n\t' "$street"
      pr_city "$zip" "$city" "$state" "$ccode" 
      [[ -z $country ]] || printf '\\\\\n\t%s' "$country"
      printf '\n\t}, opening={Geachte %s %s}\n' "$Prefix" "$Last"
      printf '}\\Letter\n'
   done 
}

:<<'DOC' #---------------------------------------------------------------------
= pr_startbooklet
description:	print preamble for booklet format
DOC
#-------------------------------------------------------------------------------
pr_startbooklet() {
   local name="${Key:-$(basename "${Input%.vcf}")}"
   cat <<-EOD
	%%!lualatex
	%%=== phone book ===
	\documentclass{memoir}
	\usepackage{fontspec}
	\protrudechars=2
	\adjustspacing=2
	\newfontfeature{Microtype}{protrusion=default;expansion=default}
	\directlua{fonts.protrusions.setups.default.factor=.5}
	\setmainfont{$Font}[BoldFont=$Bold]
	\usepackage{polyglossia}\setmainlanguage{dutch}
	\usepackage{ctable}
	\usepackage{colortbl}
	\usepackage{longtable}
	\usepackage[noheadfoot,a4paper,margin=10mm,left=18mm,right=18mm]{geometry}
	\pagestyle{empty}
	\newcommand{\kop}{
	  \hline\strut Naam
	  &Postadres
	  &Telefoon\tabularnewline\hline
	}
	\def\EM#1{\makebox[0pt][r]{#1}}
	\def\Red#1{\textbf{\textcolor[rgb]{1,0,0}{#1} }}
	\def\Blu#1{\textcolor[rgb]{0,0,1}{#1}}
	\def\C#1{}
	\newcolumntype{L}[1]{>{\raggedright}p{#1mm}}
	\newcolumntype{R}[1]{>{\raggedleft}p{#1mm}}
	\setlength{\extrarowheight}{1ex}
	\def\G{\rowcolor[gray]{.8}}
	\begin{document}%\renewcommand{\arraystretch}{.8}
	\fontsize{$Fontsize}{$((Fontsize+2))}\selectfont
	\begin{longtable}{L{52}L{68}R{36}}
	  \multicolumn{3}{@{}l@{}}{\Large\bfseries Adreslijst $name
	  \hfill\today}\tabularnewline
	  \kop\endfirsthead
	  \kop\endhead
	EOD
}

:<<'DOC' #---------------------------------------------------------------------
= pr_booklet
description:	print in booklet format
DOC
#-------------------------------------------------------------------------------
pr_booklet() {
   $counter && ((count+=1))
   ((gray=!gray))
   local i j pobox ext street zip city state ccode country
   local G=('\G' '  ')
   local g=${G[$gray]}
   local adr='' name="$Last $Suffix" org="$Org"
   [[ -z $name ]] && name="$org"
   texescape name
   texescape org

   # addresses for column 2:
   for i in H W; do
      for j in pobox ext street zip city state ccode; do
         eval $j="\$$i$j"
      done
      country=''
      [[ $ccode != $Myccode ]] && country=${Cname[$ccode]}
      # create an address only if there is a street or a PO box:
      if [[ -n $street$pobox ]]; then
         # initially, use | for newlines
         adr+="$(join "|" \
             "$pobox" \
             "$ext" \
             "$street" \
             "$(pr_city "$zip" "$city" "$state" "$ccode")" \
             "$country" \
	     "[2ex]"
         )"
      fi
   done
   if [[ -n $adr ]]; then
      adr=${adr:0:-6}	# remove final |[2ex]
      texescape adr
   fi
   fix Hphone
   fix Mphone
   fix Wphone
   texescape Hmail
   texescape Wmail
   echo -n "$g\\Red{$count $name} $Prefix $First"
   [[ -n $Middle ]] && echo -n " $Middle"
   [[ -n $Nick ]] && echo -n " $Nick"
   [[ -n $org ]] && [[ $name != "$org" ]] && echo -n " ($org)"
   if [[ -n $Note ]]; then
      local x=()
      texescape Note
      IFS=$'|' read -A x <<<"${Note//'\n'/|}"
      printf '\\\\\\Blu{%s}' "${x[@]}"
   fi
   phoneenmail="$(join '}|\\EM{' "$Hphone" "$Mphone" "$Wphone" "$Hmail" "$Wmail")}"
   local out="&$adr&${phoneenmail:2}\\NN"
   echo ${out//|/\\\\\\\\}
}

:<<'DOC' #---------------------------------------------------------------------
= pr_startxml
description:	print preamble for FritzBox xml format
DOC
#-------------------------------------------------------------------------------
pr_startxml() {
   cat <<-'EOD'
	<?xml version="1.0" encoding="utf-8"?>
	<phonebooks><phonebook name="Contacts">
	EOD
}

:<<'DOC' #---------------------------------------------------------------------
= pr_xml
description:	print in FritzBox xml format
DOC
#-------------------------------------------------------------------------------
pr_xml() {
   # need at least one telephone number:
   local p=() t=() n=0
   [[ -n $Hphone ]] && { p+=("$Hphone"); t+=('private'); ((n++)); }
   [[ -n $Wphone ]] && { p+=("$Wphone"); t+=('business'); ((n++)); }
   [[ -n $Mphone ]] && { p+=("$Mphone"); t+=('mobile'); ((n++)); }
   if (( n == 0 )); then
      return
   else
      cat <<-EOD
	<contact>
	  <category/>
	  <person>
	    <realName>$Disp</realName>
	  </person>
	EOD
      echo "  <telephony nid=\"$n\">"
      for ((i=1;i<=n;i++)); do
         echo "    <number type=\"${t[$i]}\" id=\"$i\">${p[$i]}</number>"
      done
   fi
   echo "  </telephony>"
   p=() t=() n=0
   [[ -n $Hmail ]] && { p+=("$Hmail"); t+=('private'); ((n++)); }
   [[ -n $Wmail ]] && { p+=("$Wmail"); t+=('business'); ((n++)); }
   if [[ $n -gt 0 ]]; then
      echo "  <services nid=\"$n\">"
      for ((i=1;i<=n;i++)); do
         echo "    <email classifier=\"${t[$i]}\" id=\"$i\">${p[$i]}</email>"
      done
      echo "  </services>"
   fi
   echo '</contact>'
}

:<<'DOC' #---------------------------------------------------------------------
= pr_vcard
description:	print vcard
DOC
#-------------------------------------------------------------------------------
pr_vcard() {
   local h w i hmw typarr
   declare -A typarr=([H]=HOME [M]=CELL [W]=WORK)
   echo -e "BEGIN:VCARD\nVERSION:3.0"
   h="ADR;TYPE=HOME:$Hpobox;$Hext;$Hstreet;$Hcity;$Hstate;${Hzip// /};$Hccode"
   w="ADR;TYPE=WORK:$Wpobox;$Wext;$Wstreet;$Wcity;$Wstate;${Wzip// /};$Wccode"
   for i in "$h" "$w"; do
      if [[ ! $i =~ \;\;\;\;\;\;$ ]]; then
         [[ $i =~ \;$ ]] && i+="$Myccode"
         echo "$i"
      fi
   done
   [[ -n $Bday  ]] && echo "BDAY:$Bday"
   [[ -n $Hmail ]] && printf "EMAIL;TYPE=HOME:%s\n" "$Hmail"
   [[ -n $Wmail ]] && printf "EMAIL;TYPE=WORK:%s\n" "$Wmail"
   echo "FN:$Disp"
   echo "N:$Last;$First;$Middle;$Prefix;$Suffix"
   [[ -n $Nick  ]] && ! $Blind && echo "NICKNAME:${Nick:1:-1}"
   [[ -n $Note  ]] && echo "NOTE:$Note"
   [[ -n $Org   ]] && echo "ORG:$Org"
   set -- HOME CELL WORK
   for hmw in H M W; do
      eval "i=\$${hmw}phone"
      [[ -n $i ]] || continue # i becomes Hphone, Mphone, Wphone
      tel="TEL;TYPE=${typarr[$hmw]}:$i"
      [[ $tel =~ \:$ ]] || echo "$tel"
      shift
   done
   [[ -n $Url ]] && echo "URL:$Url"
   if [[ -n $Keys ]]; then
      k="${Keys[*]}"
      k="${k// /,}"
      echo -e "CATEGORIES:$k\nTITLE:$k"
   fi
   # (TITLE makes categories searchable in iCloud)
   # do this at the end, in case pr_oneliner changes the phone nrs:
   $Blind && echo "NICKNAME:$(pr_oneliner)"
   echo -e 'END:VCARD\n'
}

:<<'DOC' #---------------------------------------------------------------------
= filltype
synopsis	 filltype xhome xwork [xcell]
description:	Set Type to the first argument for which the corresponding
		variable is empty; the name of the argument tells the type
		assigned.
		note:
		Made this, because thunderbird exports home address without
		type=HOME if it's the first address, and the work address
		without type=WORK if it's the second address. iCloud exports
		with both type indications.
		thus, in the absence of type indicators
		the first two addresses are home and work
		the first two emails are home and work
		the first three phonenrs are home, work. and celll
DOC
#-------------------------------------------------------------------------------
filltype() {
   until [[ $# -eq 0 ]] || [[ -n $Type  ]]; do
      eval "test -z \$$1 && Type=${1:1}; shift"
   done
}

Hphone='' Mphone='' Wphone='' Hmail='' Wmail='' # satisfy shellcheck
# this works for dutch users only...
AreaCodes=(
   6    10   13   15   20   23   24   26   30   33   35   36   38   40  
   43   45   46   50   53   55   58   70   71   72   73   74   75   76  
   77   78   79   85   87   88   101  111  113  114  115  117  118  161 
   162  164  165  166  167  168  172  174  180  181  182  183  184  186 
   187  222  223  224  226  227  228  229  251  252  255  294  297  299 
   313  314  315  316  317  318  320  321  341  342  343  344  345  346 
   347  348  411  412  413  416  418  475  478  481  485  486  487  488 
   492  493  495  497  499  511  512  513  514  515  516  517  518  519 
   521  522  523  524  525  527  528  529  541  543  544  545  546  547 
   548  561  562  566  570  571  572  573  575  577  578  591  592  593 
   594  595  596  597  598  599  800  900  032  033  034  039  044  049 
   061  062  064  0352 0385 0599 01                                     
)

declare -A Cdial
declare -A Cname
declare -A CCodes=(
#ccode en     fr      es      it      nl      dial
[AD]="Andorra;Andorre;Andorra;Andorra;Andorra;376"
[AE]="United Arab Emirates;Emirats Arabes Unis;Emiratos Árabes Unidos;Emirati Arabi Uniti;Verenigde Arabische Emiraten;971"
[AF]="Afghanistan;Afghanistan;Afganistán;Afganistan;Afghanistan;93"
[AG]="Antigua and Barbuda;Antigua et Barbuda;Antigua y Barbuda;Antigua e Barbuda;Antigua en Barbuda;1268"
[AI]="Anguilla;Anguilla;Anguila;Anguilla;Anguilla;1264"
[AL]="Albania;Albanie;Albania;Albania;Albanië;355"
[AM]="Armenia;Arménie;Armenia;Armenia;Armenië;374"
[AN]="Netherlands Antilles;Antilles Néerlandaises;Antillas Neerlandesas;Antille olandesi;Nederlandse Antillen;599"
[AO]="Angola;Angola;Angola;Angola;Angola;244"
[AQ]="Antarctica;Antarctique;Antártida;Antartide;Antarctica;6721"
[AR]="Argentina;Argentine;Argentino;Argentina;Argentinië;54"
[AS]="American Samoa;Samoa américaines;Samoa Americana;Samoa americane;Amerikaans Samoa;1684"
[AT]="Austria;Autriche;Austria;Austria;Oostenrijk;43"
[AU]="Australia;Australie;Australia;Australia;Australië;61"
[AW]="Aruba;Aruba;Aruba;Aruba;Aruba;297"
[AX]="Åland;Åland;Åland;Åland;Åland;35818"
[AZ]="Azerbaijan;Azerbaïdjan;Azerbaiyán;Azerbaigian;Azerbeidzjan;994"
[BA]="Bosnia and Herzegovina;Bosnie-Herzégovine;Bosnia y Herzegovina;Bosnia ed Erzegovina;Bosnië en Herzegovina;387"
[BB]="Barbados;Barbade;Barbados;Barbados;Barbados;1246"
[BD]="Bangladesh;Bangladesh;Bangladés;Bangladesh;Bangladesh;880"
[BE]="Belgium;Belgique;Bélgica;Belgio;België;32"
[BF]="Burkina Faso;Burkina Faso;Burkina Faso;Burkina Faso;Burkina Faso;226"
[BG]="Bulgaria;Bulgarie;Bulgaria;Bulgaria;Bulgarije;359"
[BH]="Bahrain;Bahreïn;Baréin;Bahrein;Bahrein;973"
[BI]="Burundi;Burundi;Burundi;Burundi;Burundi;257"
[BJ]="Benin;Bénin;Benín;Benin;Benin;229"
[BL]="Saint Barthélemy;Saint Barthélemy;San Bartolomé;Saint Barthélemy;Saint Barthélemy;590"
[BM]="Bermuda;Bermudes;Bermudas;Bermuda;Bermuda;1441"
[BN]="Brunei;Brunei;Brunéi;Brunei;Brunei;673"
[BO]="Bolivia;Bolivie;Bolivia;Bolivia;Bolivia;591"
[BQ]="Bonaire, Sint Eustatius, and Saba;Bonaire, Sint Eustatius et Saba;Bonaire, San Eustaquio y Saba;Bonaire, Sint Eustatius e Saba;Bonaire, Sint Eustatius en Saba;5997"
[BR]="Brazil;Brésil;Brasil;Brasile;Brazilië;55"
[BS]="Bahamas;Bahamas;Bahamas;Bahamas;Bahama's;1242"
[BT]="Bhutan;Bhoutan;Bután;Bhutan;Bhutan;975"
[BV]="Bouvet Island;Ile Bouvet;Isla Bouvet;Isola Bouvet;Bouveteiland;47"
[BW]="Botswana;Botswana;Botsuana;Botswana;Botswana;267"
[BY]="Belarus;Biélorussie;Bielorrusia;Bielorussia;Wit-Rusland;375"
[BZ]="Belize;Bélize;Belice;Belize;Belize;501"
[CA]="Canada;Canada;Canadá;Canada;Canada;1"
[CC]="Cocos (Keeling) Islands;Îles Cocos (Keeling);Islas Cocos (Keeling);Isole Cocos (Keeling).;Cocos (Keeling) Eilanden;61"
[CD]="DR Congo;RD Congo;RD Congo;RD Congo;DR Congo;243"
[CF]="Central African Republic;République centrafricaine;República Centroafricana;Repubblica Centrafricana;Centraal-Afrikaanse Republiek;236"
[CG]="Congo Republic;République du Congo;República del Congo;Repubblica del Congo;Congo Republiek;242"
[CH]="Switzerland;Suisse;Suiza;Svizzera;Zwitserland;41"
[CI]="Ivory Coast;Côte d'Ivoire;Costa de Marfil;Costa d'Avorio;Ivoorkust;225"
[CK]="Cook Islands;Îles Cook;Islas Cook;Isole Cook;Cook Eilanden;682"
[CL]="Chile;Chili;Chile;Cile;Chili;56"
[CM]="Cameroon;Cameroun;Camerún;Camerun;Kameroen;237"
[CN]="China;Chine;China;Cina;China;86"
[CO]="Colombia;Colombie;Colombia;Colombia;Colombia;57"
[CR]="Costa Rica;Costa Rica;Costa Rica;Costarica;Costa Rica;506"
[CU]="Cuba;Cuba;Cuba;Cuba;Cuba;53"
[CV]="Cabo Verde;Cap-Vert;Cabo Verde;Capo Verde;Kaapverdië;238"
[CW]="Curaçao;Curaçao;Curazao;Curaçao;Curaçao;599"
[CX]="Christmas Island;Île Christmas;Isla de Navidad;Isola di Natale;Christmaseiland;61"
[CY]="Cyprus;Chypre;Chipre;Cipro;Cyprus;357"
[CZ]="Czechia;Tchéquie;Chequia;Cechia;Tsjechië;420"
[DE]="Germany;Allemagne;Alemania;Germania;Duitsland;49"
[DJ]="Djibouti;Djibouti;Yibuti;Gibuti;Djibouti;253"
[DK]="Denmark;Danemark;Dinamarca;Danimarca;Denemarken;45"
[DM]="Dominica;Dominique;Dominica;Domenico;Dominica;1767"
[DO]="Dominican Republic;République Dominicaine;República Dominicana;Repubblica Dominicana;Dominicaanse Republiek;1809;1829;1849"
[DZ]="Algeria;Algérie;Argelia;Algeria;Algerije;213"
[EC]="Ecuador;Equateur;Ecuador;Ecuador;Ecuador;593"
[EE]="Estonia;Estonie;Estonia;Estonia;Estland;372"
[EG]="Egypt;Egypte;Egipto;Egitto;Egypte;20"
[EH]="Western Sahara;Sahara Occidental;Sáhara Occidental;Sahara occidentale;Westelijke Sahara;212"
[ER]="Eritrea;Erythrée;Eritrea;Eritrea;Eritrea;291"
[ES]="Spain;Espagne;España;Spagna;Spanje;34"
[ET]="Ethiopia;Ethiopie;Etiopía;Etiopia;Ethiopië;251"
[FI]="Finland;Finlande;Finlandia;Finlandia;Finland;358"
[FJ]="Fiji;Fidji;Fiyi;Figi;Fiji;679"
[FK]="Falkland Islands;Îles Malouines;Islas Malvinas;Isole Falkland;Falkland Eilanden;500"
[FM]="Micronesia;Micronésie;Micronesia;Micronesia;Micronesië;691"
[FO]="Faroe Islands;Îles Féroé;Islas Feroe;Isole Faroe;Faeröer;298"
[FR]="France;France;Francia;Francia;Frankrijk;33"
[GA]="Gabon;Gabon;Gabón;Gabon;Gabon;241"
[GB]="United Kingdom;Royaume-Uni;Reino Unido;Regno Unito;Verenigd Koninkrijk;44"
[GD]="Grenada;Grenade;Granada;Granada;Grenada;1473"
[GE]="Georgia;Géorgie;Georgia;Georgia;Georgië;995"
[GF]="French Guiana;Guyane Française;Guayana Francesa;Guyana francese;Frans-Guyana;594"
[GG]="Guernsey;Guernesey;Guernesey;Guernsey;Guernsey;44"
[GH]="Ghana;Ghana;Ghana;Ghana;Ghana;233"
[GI]="Gibraltar;Gibraltar;Gibraltareño;Gibilterra;Gibraltar;350"
[GL]="Greenland;Groenland;Groenlandia;Groenlandia;Groenland;299"
[GM]="The Gambia;Gambie;Gambia;Gambia;Gambia;220"
[GN]="Guinea;Guinée;Guinea;Guinea;Guinee;224"
[GP]="Guadeloupe;Guadeloupe;Guadalupe;Guadalupa;Guadeloupe;590"
[GQ]="Equatorial Guinea;Guinée équatoriale;Guinea Ecuatorial;Guinea Equatoriale;Equatoriaal Guinea;240"
[GR]="Greece;Grèce;Grecia;Grecia;Griekenland;30"
[GS]="South Georgia and South Sandwich Islands;Géorgie du Sud et Îles Sandwich du Sud;Islas Georgias del Sur y Sandwich del Sur;Georgia del Sud e Isole Sandwich Australi;South Georgia en South Sandwich Islands;500"
[GT]="Guatemala;Guatémala;Guatemala;Guatemala;Guatemala;502"
[GU]="Guam;Guam;Guam;Guam;Guam;1671"
[GW]="Guinea-Bissau;Guinée-Bissau;Guinea-Bisáu;Guinea-Bissau;Guinee-Bissau;245"
[GY]="Guyana;Guyane;Guayana;Guyana;Guyana;592"
[HK]="Hong Kong;Hong-Kong;Hong Kong;Hong Kong;Hongkong;852"
[HN]="Honduras;Honduras;;Honduras;Honduras;504"
[HR]="Croatia;Croatie;Croacia;Croazia;Kroatië;385"
[HT]="Haiti;Haïti;Haití;Haiti;Haïti;509"
[HU]="Hungary;Hongrie;Hungría;Ungheria;Hongarije;36"
[ID]="Indonesia;Indonésie;Indonesia;Indonesia;Indonesië;62"
[IE]="Ireland;Irlande;Irlanda;Irlanda;Ierland;353"
[IL]="Israel;Israël;Israel;Israele;Israël;972"
[IM]="Isle of Man;Ile de Man;Isla de Man;Isola di Man;Isle of Man;44162"
[IN]="India;Inde;India;India;Indië;91"
[IO]="British Indian Ocean Territory;Territoire britannique de l'océan Indien;Territorio Británico del Océano Índico;Territorio britannico dell'Oceano Indiano;Brits Indische Oceaanterritorium;246"
[IQ]="Iraq;Irak;Irak;Iraq;Irak;964"
[IR]="Iran;Iran;Irán;Iran;Iran;98"
[IS]="Iceland;Islande;Islandia;Islanda;IJsland;354"
[IT]="Italy;Italie;Italia;Italia;Italië;39"
[JE]="Jersey;Jersey;Jersey;Jersey;Jersey;44153"
[JM]="Jamaica;Jamaïque;Jamaica;Giamaica;Jamaica;1876"
[JO]="Jordan;Jordanie;Jordania;Giordania;Jordanië;962"
[JP]="Japan;Japon;Japón;Giappone;Japan;81"
[KE]="Kenya;Kenya;Kenia;Kenia;Kenia;254"
[KG]="Kyrgyzstan;Kirghizistan;Kirguistán;Kirghizistan;Kirgizië;996"
[KH]="Cambodia;Cambodge;Camboya;Cambogia;Cambodja;855"
[KI]="Kiribati;Kiribati;Kiribati;Kiribati;Kiribati;686"
[KM]="Comoros;Comores;Comoras;Comore;Comoren;269"
[KN]="St Kitts and Nevis;St Kitts et Nevis;San Cristóbal y Nieves;St Kitts e Nevis;Saint Kitts en Nevis;1869"
[KP]="North Korea;Corée du Nord;Corea del Norte;Corea del Nord;Noord-Korea;850"
[KR]="South Korea;Corée du Sud;Corea del Sur;Corea del Sud;Zuid-Korea;82"
[KW]="Kuwait;Koweït;Kuwait;Kuwait;Koeweit;965"
[KY]="Cayman Islands;Îles Caïmans;Islas Caimán;Isole Cayman;Kaaimaneilanden;1345"
[KZ]="Kazakhstan;Kazakhstan;Kazajistán;Kazakistan;Kazachstan;7"
[LA]="Laos;Laos;Laos;Laos;Laos;856"
[LB]="Lebanon;Liban;Líbano;Libano;Libanon;961"
[LC]="Saint Lucia;Sainte Lucie;Santa Lucía;Santa Lucia;Saint Lucia;1758"
[LI]="Liechtenstein;Liechtenstein;Liechtenstein;Liechtenstein;Liechtenstein;423"
[LK]="Sri Lanka;Sri Lanka;Sri Lanka;Sri Lanka;Sri Lanka;94"
[LR]="Liberia;Libéria;Liberia;Liberia;Liberia;231"
[LS]="Lesotho;Lesotho;Lesoto;Lesotho;Lesotho;266"
[LT]="Lithuania;Lituanie;Lituania;Lituania;Litouwen;370"
[LU]="Luxembourg;Luxembourg;Luxemburgo;Lussemburgo;Luxemburg;352"
[LV]="Latvia;Lettonie;Letonia;Lettonia;Letland;371"
[LY]="Libya;Libye;Libia;Libia;Libië;218"
[MA]="Morocco;Maroc;Marruecos;Marocco;Marokko;212"
[MC]="Monaco;Monaco;Mónaco;Monaco;Monaco;377"
[MD]="Moldova;Moldavie;Moldavia;Moldavia;Moldavië;373"
[ME]="Montenegro;Monténégro;Montenegro;Montenegro;Montenegro;382"
[MF]="Saint Martin;Saint-Martin;San Martín;San Martino;Sint Maarten;590"
[MG]="Madagascar;Madagascar;Madagascar;Madagascar;Madagaskar;261"
[MH]="Marshall Islands;Îles Marshall;Islas Marshall;Isole Marshall;Marshalleilanden;692"
[MK]="North Macedonia;Macédoine du Nord;Macedonia del Norte;Macedonia del Nord;Noord-Macedonië;389"
[ML]="Mali;Mali;Malí;Mali;Mali;223"
[MM]="Myanmar;Birmanie;Birmania;Birmania;Myanmar;95"
[MN]="Mongolia;Mongolie;Mongolia;Mongolia;Mongolië;976"
[MO]="Macao;Macao;Macao;Macao;Macau;853"
[MP]="Northern Mariana Islands;Îles Mariannes du Nord;Islas Marianas del Norte;Isole Marianne Settentrionali;Noordelijke Marianen;1670"
[MQ]="Martinique;Martinique;Martinica;Martinica;Martinique;596"
[MR]="Mauritania;Mauritanie;Mauritania;Mauritania;Mauritanië;222"
[MS]="Montserrat;Montserrat;Montserrat;Montserrat;Montserrat;1664"
[MT]="Malta;Malte;Malta;Malta;Malta;356"
[MU]="Mauritius;Maurice;Mauricio;Maurizio;Mauritius;230"
[MV]="Maldives;Maldives;Maldivas;Maldive;Malediven;960"
[MW]="Malawi;Malawi;Malaui;Malawi;Malawi;265"
[MX]="Mexico;Mexique;México;Messico;Mexico;52"
[MY]="Malaysia;Malaisie;Malasia;Malesia;Maleisië;60"
[MZ]="Mozambique;Mozambique;Mozambique;Mozambico;Mozambique;258"
[NA]="Namibia;Namibie;;Namibia;Namibië;264"
[NC]="New Caledonia;Nouvelle-Calédonie;Nueva Caledonia;Nuova Caledonia;Nieuw-Caledonië;687"
[NE]="Niger;Niger;Níger;Nigeria;Nigeria;227"
[NF]="Norfolk Island;Île Norfolk;Isla Norfolk;Isola Norfolk;Norfolkeiland;6723"
[NG]="Nigeria;Nigéria;Nigeria;Nigeria;Niger;234"
[NI]="Nicaragua;Nicaragua;Nicaragua;Nicaragua;Nicaragua;505"
[NL]="Netherlands;Pays-Bas;Países Bajos;Paesi Bassi;Nederland;31"
[NO]="Norway;Norvège;Noruega;Norvegia;Noorwegen;47"
[NP]="Nepal;Népal;Nepal;Il Nepal;Nepal;977"
[NR]="Nauru;Nauru;Nauru;Nauru;Nauru;674"
[NU]="Niue;Niué;Niue;Niue;Niue;683"
[NZ]="New Zealand;Nouvelle-Zélande;Nueva Zelanda;Nuova Zelanda;Nieuw-Zeeland;64"
[OM]="Oman;Oman;Omán;Oman;Oman;968"
[PA]="Panama;Panama;Panamá;Panama;Panama;507"
[PE]="Peru;Pérou;Perú;Perù;Peru;51"
[PF]="French Polynesia;Polynésie française;Polinesia Francesa;Polinesia francese;Frans-Polynesië;689"
[PG]="Papua New Guinea;Papouasie-Nouvelle-Guinée;Papúa Nueva Guinea;Papua Nuova Guinea;Papoea-Nieuw-Guinea;675"
[PH]="Philippines;Philippines;Filipinas;Filippine;Filipijnen;63"
[PK]="Pakistan;Pakistan;Pakistán;Pakistan;Pakistan;92"
[PL]="Poland;Pologne;Polonia;Polonia;Polen;48"
[PM]="Saint Pierre and Miquelon;Saint Pierre et Miquelon;San Pedro y Miquelón;Saint Pierre e Miquelon;Saint Pierre en Miquelon;508"
[PN]="Pitcairn Islands;Îles Pitcairn;Islas Pitcairn;Isole Pitcairn;Pitcairneilanden;64"
[PR]="Puerto Rico;Porto Rico;Puerto Rico;Porto Rico;Puerto Rico;1787;1939"
[PS]="Palestine;Palestine;Palestina;Palestina;Palestina;970"
[PT]="Portugal;Portugal;Portugal;Portogallo;Portugal;351"
[PW]="Palau;Palaos;Palaos;Palau;Palau;680"
[PY]="Paraguay;Paraguay;Paraguayo;Paraguay;Paraguay;595"
[QA]="Qatar;Qatar;Catar;Qatar;Qatar;974"
[RE]="Réunion;Réunion;Reunión;Riunione;Réunion;262"
[RO]="Romania;Roumanie;Rumania;Romania;Roemenië;40"
[RS]="Serbia;Serbie;Serbia;Serbia;Servië;381"
[RU]="Russia;Russie;Rusia;Russia;Rusland;7"
[RW]="Rwanda;Rwanda;Ruanda;Ruanda;Rwanda;250"
[SA]="Saudi Arabia;Arabie Saoudite;Arabia Saudita;Arabia Saudita;Saoedi-Arabië;966"
[SB]="Solomon Islands;Îles Salomon;Islas Salomón;Isole Salomone;Salomonseilanden;677"
[SC]="Seychelles;Seychelles;Seychelles;Seychelles;Seychellen;248"
[SD]="Sudan;Soudan;Sudán;Sudan;Soedan;249"
[SE]="Sweden;Suède;Suecia;Svezia;Zweden;46"
[SG]="Singapore;Singapour;Singapur;Singapore;Singapore;65"
[SH]="Saint Helena;Sainte-Hélène;Santa Elena;Sant'Elena;Sint-Helena;290"
[SI]="Slovenia;Slovénie;Eslovenia;Slovenia;Slovenië;386"
[SJ]="Svalbard and Jan Mayen;Svalbard et Jan Mayen;Svalbard y Jan Mayen;Svalbard e Jan Mayen;Spitsbergen en Jan Mayen;47"
[SK]="Slovakia;Slovaquie;Eslovaquia;Slovacchia;Slowakije;421"
[SL]="Sierra Leone;Sierra Léone;Sierra Leona;Sierra Leone;Sierra Leone;232"
[SM]="San Marino;Saint-Marin;San Marino;San Marino;San Marino;378"
[SN]="Senegal;Sénégal;Senegal;Senegal;Senegal;221"
[SO]="Somalia;Somalie;Somalia;Somalia;Somalië;252"
[SR]="Suriname;Surinam;Surinam;Suriname;Suriname;597"
[SS]="South Sudan;Soudan du Sud;Sudán del Sur;Sud Sudan;Zuid-Soedan;211"
[ST]="São Tomé and Príncipe;São Tomé et Príncipe;Santo Tomé y Príncipe;São Tomé e Príncipe;Sao Tomé en Principe;239"
[SV]="El Salvador;Salvador;El Salvador;El Salvador;El Salvador;503"
[SX]="Sint Maarten;Saint-Martin;Sint Maarten;Sint Maarten;Sint Maarten;1721"
[SY]="Syria;Syrie;Siria;Siria;Syrië;963"
[SZ]="Eswatini;Eswatini;Esuatini;Swaziland;Eswatini;268"
[TC]="Turks and Caicos Islands;Îles Turques et Caïques;Islas Turcas y Caicos;Isole Turks e Caicos;Turks- en Caicoseilanden;1649"
[TD]="Chad;Tchad;Chad;Ciad;Tsjaad;235"
[TF]="French Southern Territories;Terres Australes Françaises;Territorios Australes Franceses;Territori australi francesi;Franse Zuidelijke Gebieden;262"
[TG]="Togo;Togo;Togo;Togo;Togo;228"
[TH]="Thailand;Thaïlande;Tailandia;Tailandia;Thailand;66"
[TJ]="Tajikistan;Tadjikistan;Tayikistán;Tagikistan;Tadzjikistan;992"
[TK]="Tokelau;Tokélaou;Tokelau;Tokelau;Tokelau;690"
[TL]="Timor-Leste;Timor oriental;Timor Oriental;Timor Est;Oost-Timor;670"
[TM]="Turkmenistan;Turkménistan;Turkmenistán;Turkmenistan;Turkmenistan;993"
[TN]="Tunisia;Tunisie;Túnez;Tunisia;Tunesië;216"
[TO]="Tonga;Tonga;Tonga;Tonga;Tonga;676"
[TR]="Turkey;Turquie;Turquía;Turchia;Turkije;90"
[TT]="Trinidad and Tobago;Trinité-et-Tobago;Trinidad y Tobago;Trinidad e Tobago;Trinidad en Tobago;1868"
[TV]="Tuvalu;Tuvalu;Tuvalu;Tuvalu;Tuvalu;688"
[TW]="Taiwan;Taïwan;Taiwán;Taiwan;Taiwan;886"
[TZ]="Tanzania;Tanzanie;Tanzania;Tanzania;Tanzania;255"
[UA]="Ukraine;Ukraine;Ucrania;Ucraina;Oekraïne;380"
[UG]="Uganda;Ouganda;Uganda;Uganda;Oeganda;256"
[UM]="U.S. Outlying Islands;Îles éloignées des États-Unis;Islas periféricas de EE. UU.;Isole esterne degli Stati Uniti;Amerikaanse afgelegen eilanden;1"
[US]="United States;États-Unis;Estados Unidos;Stati Uniti;Verenigde Staten;1"
[UY]="Uruguay;Uruguay;Uruguay;Uruguay;Uruguay;598"
[UZ]="Uzbekistan;Ouzbékistan;Uzbekistán;Uzbekistan;Oezbekistan;998"
[VA]="Vatican City;Cité du Vatican;Ciudad del Vaticano;Città del Vaticano;Vaticaanstad;379"
[VC]="St Vincent and Grenadines;St Vincent et les Grenadines;San Vicente y las Granadinas;St Vincent e Grenadine;Saint Vincent en de Grenadines;1784"
[VE]="Venezuela;Vénézuela;Venezuela;Venezuela;Venezuela;58"
[VG]="British Virgin Islands;Iles Vierges Britanniques;Islas Vírgenes Británicas;Isole Vergini britanniche;Britse Maagdeneilanden;1284"
[VI]="U.S. Virgin Islands;Îles Vierges des États-Unis;Islas Vírgenes de los Estados Unidos;Isole Vergini americane;Amerikaanse Maagdeneilanden;1340"
[VN]="Vietnam;Vietnam;Vietnam;Vietnam;Vietnam;84"
[VU]="Vanuatu;Vanuatu;Vanuatu;Vanuatu;Vanuatu;678"
[WF]="Wallis and Futuna;Wallis et Futuna;Wallis y Futuna;Wallis e Futuna;Wallis en Futuna;681"
[WS]="Samoa;Samoa;Samoa;Samoa;Samoa;685"
[XK]="Kosovo;Kosovo;Kosovo;Kosovo;Kosovo;383"
[YE]="Yemen;Yémen;Yemen;Yemen;Jemen;967"
[YT]="Mayotte;Mayotte;Mayotte;Mayotte;Mayotte;262"
[ZA]="South Africa;Afrique du Sud;Sudáfrica;Sud Africa;Zuid-Afrika;27"
[ZM]="Zambia;Zambie;Zambia;Zambia;Zambia;260"
[ZW]="Zimbabwe;Zimbabwé;Zimbabue;Zimbabwe;Zimbabwe;263"
)

:<<'DOC' #---------------------------------------------------------------------
= find_country
synopsis:	find_country countrycode
description:	From the countrycode (FR, NL, GB, BE, et cetera) find the
		country name.
                The Vcard ADD entry has 7 fields; the last field is the
                country name. This name can occur in any language, while
                what we need in address labels is the name of the country
                in the language of the country where we live (Mylanguage),
                so that the postman recognizes it.
                Therefore, contmgr expects the country code in this
                field, and if it finds a country name instead, in any of
                the languages it understands, it replaces it with the
                country code.
                Finally, |find_country| prints country code, call code and
                country name, separated with |-| characters. If the country
                code happens to be the country of the user (|Myccode|),
                then the country name is cleared, since it would not have
                to be written on address labels in that case.
		Example: 
DOC
#-------------------------------------------------------------------------------
find_country() { 
   local country
   if [[ -z $1 ]]; then
      Warn "Missing country code, assuming $Myccode"
      echo "$Myccode|${Cdial[$Myccode]}|${Cname[$Myccode]}"
   else
      country=${Cname[$1]}
      if [[ -n $country ]]; then
         # if it is indeed a country code
         echo "$1|${Cdial[$1]}|${country%"$Mycountry"}"
      else
         # it may be a country name in one of the languages:
         country='Unknown'
         for i in "${(@k)CCodes}"; do
           if [[ ${CCodes[$i]} =~ $1\; ]]; then
              Warn "Replacing country name $1 with country code"
              echo "$i|${Cdial[$i]}|${Cname[$i]%$Mycountry}"
              return
           fi
         done
         echo "??|??|Unknown"
      fi
   fi
}

:<<'DOC' #---------------------------------------------------------------------
= mobtest
synopsis:	mobtest Xphone type boolean
description:	Test if the content of the named variable in arg 1 is probably a 
		mobile number. If so, and arg 3 is false, of if not and arg 3 is
		true, issue a warning.
		Finally, edit the content of arg 1 by replacing an initial 
		|+$Myccode| with 0 for nicer display (unless the output format is 
		|vcard|.
DOC
#-------------------------------------------------------------------------------
mobtest() {
   local n
   eval "n=\$$1"
   [[ -z $n ]] && return
   cell=false
   pnc info "$n" |grep -q mobile$ && cell=true
   if [[ $cell != "$3" ]]; then
      if $cell; then 
         Warn "$Disp: $2 phone $n looks like a mobile number"
      else
         Warn "$Disp: $2 phone $n does not look like a mobile number"
      fi
   fi
}

#========== start ==========
   
Key='' Nodial='' Grepopt=-ril Format='term' Return=''
Fontsize=10 Font='DejaVuSerif' Bold='DejaVuSerif-Bold' Input="$HOME/Contacts.vcf"
Paperwidth=210 Paperheight=297 Cols=3 Rows=8 LetterStyle='iso'
counter=false count='' Myccode='' Dot='' Blind=false Languages=(US FR ES IT NL)

# shellcheck disable=SC2153
if [[ -n $PREFIX ]]; then
   if [[ -e $PREFIX/contmgr.conf ]]; then
      # shellcheck disable=SC1091
      source "$PREFIX/contmgr.conf"
   fi
else
   Warn "Env var PREFIX is undefined; looking for ~/.contmgr.conf only"
fi
# shellcheck disable=SC1091
[[ -e $HOME/.contmgr.conf ]] && source "$HOME/.contmgr.conf"
[[ -n $Myccode ]] || Warn "You did not set Myccode in any configuration file"
[[ -n ${CCodes[$Myccode]} ]] || die "The country code $Myccode is not recognized"

Mylanguage=${Languages[(i)$Myccode]}
if [[ $Mylanguage > $#Languages ]]; then
   Mylanguage=1
   Warn "The language for country code $Myccode is not available" \
	"Country names will be output in US English"
fi

for i in "${(@k)CCodes}"; do
  IFS=';' read -A k <<<"${CCodes[$i]}"
  Cname[$i]="${k[$Mylanguage]}"
  Cdial[$i]="${k[-1]}"
done

# now  country="${Cname[DO]}" is like  country="Dominican Republic"
# and callcode="${Cdial[DO}}" is like callcode="1809;1829;1849"

Mycountry=${Cname[$Myccode]}
Mydial=${CCodes[$Myccode]##*;}

light # start with colors for a light background

handle_options "$@"
set -- "${Args[@]}"

searchkey="$1"
# searches are case-insensitive by default in zsh #shopt -s nocasematch

Input="$(eval "realpath '$Input'")" # eval allows use of ~
[[ -e $Input ]] || die "input file $Input not found"
tmp="$(mktemp -dt "$Myname".XXXXXXXXXX)"

cd "$tmp" || die "Could not cd to $tmp"

case $Format in
(labels)  pr_startlabels;;
(letter)  pr_startletter;;
(booklet) pr_startbooklet;;
(xml)     pr_startxml;;
(vcard)   ;;
(term)    ;;
esac

# remove \r for unix line endings,
# remove waid=[0-9]* (can occur in phone numbers)
# join lines starting with space (=continuation lines) with previous line
sed 'N;s/;waid=[0-9]\+//;s/\r//g;s/\n //g;P;D' "$Input" |
sed '
s/,UNKNOWN//gI
s/UNKNOWN,//gI
/^ITEM[0-9]\..*/Is/ITEM[0-9]\.//I	# remove ITEMs
s/\\,/,/g		# unescape commas
/^$/d			# remove empty lines
s/^END:VCARD/&\n/I	# insert empty line after END:VCARD as a record separator for msort.
s/\r//g			# remove \r for unix line endings
s/;TYPE=\(VOICE\|INTERNET\)//gI		# Remove unused types
s/;VALUE=TEXT//gI			# ditto
s/;TYPE=IPHONE/;TYPE=CELL/gI		# IPHONE is a CELL phone
s/;TYPE=MOBILE/;TYPE=CELL/gI		# MOBILE is a CELL phone
s/;TYPE=PREF//gI			# remove TYPE=PREF
s/;PREF=TRUE//gI
s/;PREF=[0-9]\+//gI			# remove PREF=n
/^\(REV\|PRODID\|UID\|VERSION\|IMPP\):/Id# remove unused entries
/^N:;;;;/d				# remove if empty
/^ORG:$/d
/^TEL[:;]/Is/[() ~ -]//g		# + and digits only
/^TEL[;:]/Is/:00/:+/			# 00 → +
/^TEL[;:]/Is/:0/:+'"$Myccode"'/		# 0 → +31
/TYPE=\(FAX\|PAGER\)/Id			# remove FAX etc. entries
' |
# one file xnnnnn for each vcard:
csplit --suppress-matched -ksz -fx -n5 - /BEGIN:VCARD/ '{*}'

# read each file containing the search key:
find . -type f -name 'x*' -print0 |
xargs --null grep $Grepopt "$searchkey" |
sort |
while read -r file; do
   unset Bday Disp First \
	 Hcity Hcountry Hext Hmail Hphone Hpobox Hstate Hstreet Hzip \
	 Last Middle Mphone Nick Note Org Prefix Suffix Url \
	 Wcity Wcountry Wext Wmail Wphone Wpobox Wstate Wstreet Wzip \
	 ahome awork ehome ework pcell phome pwork
   Hccode='' Wccode='' Hcountry='' Wcountry='' Keys=()
   while IFS= read -r line; do
      Type=''
      if [[ $line =~ TYPE=([[:alpha:]]*) ]]; then
         Type="$match[1]"
         line=${line/;TYPE=$Type/}
      fi
      if [[ $line =~ ^N:(.*)\;(.*)\;(.*)\;(.*)\;(.*) ]]; then
         Last=$match[1]
         First=$match[2]
         Middle=$match[3]
         Prefix=$match[4]
         Suffix=$match[5]
      elif [[ $line =~ ^NICKNAME:(.*) ]]; then
         # shellcheck disable=SC1111
         Nick="“$match[1]”"
      elif [[ $line =~ ^END:VCARD ]]; then
         Disp=$(displayname)
         break
      elif [[ $line =~ ^ORG:(.*) ]]; then
         Org=${match[1]%;}
      elif [[ $line =~ ^URL*:(.*) ]]; then
         Url=$match[1]
      elif [[ $line =~ ^BDAY.*:(.*) ]]; then
         Bday=$match[1]
      elif [[ $line =~ ^EMAIL:(.*) ]]; then
         m="$match[1]"
         if validateemail "$m"; then
            [[ -z $Type ]] && filltype ehome ework
            # shellcheck disable=SC2034
            case $Type in
            (HOME) ehome=1
                   setvar Hmail "$m" 'home email'
                   ;;
            (WORK) ework=1
                   setvar Wmail "$m" 'work email'
                   ;;
               (*) Warn "Skipping email address $m: unrecognized type ($Type)"
            esac
         fi
      elif [[ $line =~ ^ADR:(.*)\;(.*)\;(.*)\;(.*)\;(.*)\;(.*)\;(.*)$ ]]; then
         filltype ahome awork
         # shellcheck disable=SC2034
         case $Type in
         (HOME)
             ahome=1
	     Hpobox="$match[1]"
             Hext="$match[2]"
             Hstreet="$match[3]"
             Hcity="$match[4]"
             Hstate="$match[5]"
             Hzip="${(U)match[6]}"
             Hzip="${Hzip// /}"
             IFS='|' read -r Hccode Hcall Hcountry <<<"$(find_country "${match[7]}")"
	     [[ $Hccode == "$Myccode" ]] && Hcountry=''
             ;;
         (WORK)
             awork=1
             Wpobox="$match[1]"
             Wext="$match[2]"
             Wstreet="$match[3]"
             Wcity="$match[4]"
             Wstate="$match[5]"
             Wzip="${(U)match[6]}"
             Wzip="${Wzip// /}"
             IFS='|' read -r Wccode Wcall Wcountry <<<"$(find_country "${match[7]}")"
	     [[ $Wccode == "$Myccode" ]] && Wcountry=''
             ;;
        ('') Warn "Already have home and work addresses, skipping $line"
             ;;
         (*) Warn "Skipping address $line of unrecognized type ($Type)"
         esac
      elif [[ $line =~ ^CATEGORIES:(.*)$ ]]; then
         IFS=, read -A Keys <<<"$match[1]"
      elif [[ $line =~ ^TITLE: ]]; then
         # skip: TITLE lines are copied from CATEGORIES to make the latter searchable
         continue
      elif [[ $line =~ ^TEL:(.*) ]]; then
         v="$match[1]"
         if [[ ! $v =~ ^(11|14|[+])[0-9]+$ ]]; then
            Warn "phone number $v has wrong format"
         fi
         # v is now 11n or 114 or +nnnnn...
         filltype phome pwork pcell
         # shellcheck disable=SC2034
         case $Type in
         (HOME) phome=1 # remember that home phone was set
                setvar Hphone "$v" 'home phone'
                ;;
         (WORK) pwork=1 # remember that work phone was set
                setvar Wphone "$v" 'work phone'
                ;;
         (CELL) pcell=1 # remember that cell phone was set
                setvar Mphone "$v" 'cell phone'
                ;;
           ('') Warn "Already have home, mobile and work phone numbers, skipping $v"
                ;;
            (*) Warn "Skipping phone number ($v) of unrecognized type ($Type)"
         esac
      elif [[ $line =~ ^NOTE:(.*) ]]; then
         Note=$match[1]
      elif [[ $line =~ ^FN: ]]; then
         true
      elif [[ $Format != vcard ]]; then
         Warn "Unexpected field: $line" \
	      "Your input file ($file) needs to be cleaned"
      fi
   done < "$file"

   # If a key was specified, print only if the record has that key.
   # No key specified, just print.
   [[ -n $Key ]] && [[ ! ${Keys[*]} =~ $Key ]] && continue

   # Check if phone numbers are cell/not cell as expected
   mobtest "Hphone" home false
   mobtest "Wphone" work false
   mobtest "Mphone" mobile true

   case $Format in
   (labels)  pr_label;;
   (letter)  pr_letter;;
   (booklet) pr_booklet;;
   (xml)     pr_xml;;
   (term)    if $Blind; then pr_oneliner; else pr_term; fi;;
   (sorter)  pr_sorter;;
   (tagged)  pr_tagged;;
   (vcard)   pr_vcard;;
   esac
done | sorter
case $Format in
(labels)  printf '\\end{labels}\\end{document}\n';;
(letter)  printf '\\end{document}\n';;
(term)    ;;
(sorter)  ;;
(vcard)   ;;
(xml)     printf '</phonebook></phonebooks>\n';;
(booklet) printf '\\end{longtable}\\end{document}\n';;
esac
rm -r "$tmp"
