I’ve been typing timezone names from memory for years. TZ=[tab] was always there.
I needed to tell someone in Italy my availability in their timezone. I’ve done this a hundred times — open a browser, search “Italy timezone IANA,” copy the string, paste it back into the terminal.
This time I typed TZ= and hit tab.
$ TZ=<tab>
-- time zone --
Africa/ Asia/ CET Cuba Egypt Factory GMT+0 HST Iran Kwajalein MST7MDT Navajo Poland Singapore UTC Zulu
America/ Atlantic/ CST6CDT EET Eire GB GMT-0 Hongkong Israel Libya Mexico/ PRC Portugal Turkey Universal
Antarctica/ Australia/ Canada/ EST Etc/ GB-Eire GMT0 Iceland Jamaica MET NZ PST8PDT Pacific/ ROC UCT W-SU
Arctic/ Brazil/ Chile/ EST5EDT Europe/ GMT Greenwich Indian/ Japan MST NZ-CHAT PST8PDT ROK US/ WET
Continents. I typed E and tabbed to winnow:
$ TZ=E<tab>
-- time zone --
EET EST EST5EDT Egypt Eire Etc/ Europe/
Then u[tab] — enough to complete Europe/ — and tabbed again:
$ TZ=Europe/<tab>
Amsterdam Belfast Busingen Dublin Helsinki Kaliningrad London Minsk Oslo Riga Simferopol Tiraspol Vienna Zagreb
Andorra Belgrade Chisinau Gibraltar Istanbul Kiev Luxembourg Monaco Paris Rome Skopje Ulyanovsk Vilnius Zaporozhye
Astrakhan Berlin Copenhagen Guernsey Jersey Kirov Madrid Moscow Podgorica Samara Sofia Uzhgorod Volgograd Zurich
Athens Bratislava Dublin Helsinki Kaliningrad Lisbon Malta Nicosia Prague San_Marino Stockholm Vaduz Warsaw
R[tab]:
$ TZ=Europe/R<tab>
-- time zone --
Riga Rome
o[tab] → TZ=Europe/Rome. Seven keystrokes and four tabs.
You don’t have to know every city and region in the database. You don’t have to memorize anything. It’s a self-revealing affordance — each tab unfolds the next layer. TZ=Rome[tab] gives you nothing — don’t start with a city. TZ=[tab] gives you the whole menu. You don’t even have to know that Italy is under Europe/ — presented with a list of continents, you’ll pick the right one. Then you unwrap it from there.
How I found the machinery
After it worked, I had to know where the names were coming from. Zsh completions are functions that live in $fpath, so I grepped for TZ-specific logic:
$ grep -R "TZ" /usr/share/zsh/5.9/functions/
/usr/share/zsh/5.9/functions/_time_zone:#compdef -value-,TZ,-default-
One hit. A function called _time_zone. That #compdef -value-,TZ,-default- line at the top is how zsh registers a completion function — it tells the completion system “when someone tabs after TZ=, call me.” I read the rest:
$ cat /usr/share/zsh/5.9/functions/_time_zone
#compdef -value-,TZ,-default-
local _zoneinfo_dirs=(/usr/{share,lib,share/lib}/zoneinfo(/N))
(( $#_zoneinfo_dirs )) &&
_wanted time-zones expl 'time zone' _files -g '[A-Z]*' \
-M 'm:{a-z}={A-Z}' -W _zoneinfo_dirs
Four lines, and every one is doing something clever. The #compdef -value-,TZ,-default- directive hooks on the value position after TZ= — not on a command name, which is how most completers work. It fires whenever any command has TZ= in its arguments. The -g '[A-Z]*' glob filters out the POSIX compatibility junk in zoneinfo (lowercase files like posixrules) so you only see real regions and cities. The -M 'm:{a-z}={A-Z}' flag means case-insensitive matching — TZ=e[tab] works just as well as TZ=E[tab]. And the real stroke: it uses _files, the filesystem completer. There is no lookup table. The IANA tz database is already a directory tree, and the completer just walks it.
I was about to write a timezone lookup function because I refused to google it one more time. Seven keystrokes and four tabs.