tzselect.ksh revision 1.9 1 1.8 christos #! /bin/bash
2 1.2 perry #
3 1.9 christos # $NetBSD: tzselect.ksh,v 1.9 2013/09/20 19:06:54 christos Exp $
4 1.2 perry #
5 1.8 christos PKGVERSION='(tzcode) '
6 1.8 christos TZVERSION=see_Makefile
7 1.8 christos REPORT_BUGS_TO=tz@iana.org
8 1.5 kleink
9 1.1 jtc # Ask the user about the time zone, and output the resulting TZ value to stdout.
10 1.1 jtc # Interact with the user via stderr and stdin.
11 1.1 jtc
12 1.6 mlelstv # Contributed by Paul Eggert.
13 1.1 jtc
14 1.1 jtc # Porting notes:
15 1.1 jtc #
16 1.8 christos # This script requires a Posix-like shell with the extension of a
17 1.8 christos # 'select' statement. The 'select' statement was introduced in the
18 1.8 christos # Korn shell and is available in Bash and other shell implementations.
19 1.8 christos # If your host lacks both Bash and the Korn shell, you can get their
20 1.8 christos # source from one of these locations:
21 1.1 jtc #
22 1.8 christos # Bash <http://www.gnu.org/software/bash/bash.html>
23 1.8 christos # Korn Shell <http://www.kornshell.com/>
24 1.8 christos # Public Domain Korn Shell <http://www.cs.mun.ca/~michael/pdksh/>
25 1.1 jtc #
26 1.1 jtc # This script also uses several features of modern awk programs.
27 1.8 christos # If your host lacks awk, or has an old awk that does not conform to Posix,
28 1.1 jtc # you can use either of the following free programs instead:
29 1.1 jtc #
30 1.8 christos # Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
31 1.8 christos # mawk <http://invisible-island.net/mawk/>
32 1.1 jtc
33 1.1 jtc
34 1.1 jtc # Specify default values for environment variables if they are unset.
35 1.1 jtc : ${AWK=awk}
36 1.1 jtc : ${TZDIR=$(pwd)}
37 1.1 jtc
38 1.1 jtc # Check for awk Posix compliance.
39 1.1 jtc ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
40 1.1 jtc [ $? = 123 ] || {
41 1.1 jtc echo >&2 "$0: Sorry, your \`$AWK' program is not Posix compatible."
42 1.1 jtc exit 1
43 1.1 jtc }
44 1.1 jtc
45 1.9 christos coord=
46 1.9 christos location_limit=10
47 1.9 christos
48 1.9 christos usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
49 1.6 mlelstv Select a time zone interactively.
50 1.6 mlelstv
51 1.9 christos Options:
52 1.9 christos
53 1.9 christos -c COORD
54 1.9 christos Instead of asking for continent and then country and then city,
55 1.9 christos ask for selection from time zones whose largest cities
56 1.9 christos are closest to the location with geographical coordinates COORD.
57 1.9 christos COORD should use ISO 6709 notation, for example, '-c +4852+00220'
58 1.9 christos for Paris (in degrees and minutes, North and East), or
59 1.9 christos '-c -35-058' for Buenos Aires (in degrees, South and West).
60 1.9 christos
61 1.9 christos -n LIMIT
62 1.9 christos Display at most LIMIT locations when -c is used (default $location_limit).
63 1.9 christos
64 1.9 christos --version
65 1.9 christos Output version information.
66 1.9 christos
67 1.9 christos --help
68 1.9 christos Output this help.
69 1.9 christos
70 1.9 christos Report bugs to $REPORT_BUGS_TO."
71 1.9 christos
72 1.9 christos while getopts c:n:-: opt
73 1.9 christos do
74 1.9 christos case $opt$OPTARG in
75 1.9 christos c*)
76 1.9 christos coord=$OPTARG ;;
77 1.9 christos n*)
78 1.9 christos location_limit=$OPTARG ;;
79 1.9 christos -help)
80 1.9 christos exec echo "$usage" ;;
81 1.9 christos -version)
82 1.9 christos exec echo "tzselect $PKGVERSION$TZVERSION" ;;
83 1.9 christos -*)
84 1.9 christos echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
85 1.9 christos *)
86 1.9 christos echo >&2 "$0: try '$0 --help'"; exit 1 ;;
87 1.9 christos esac
88 1.9 christos done
89 1.9 christos
90 1.9 christos shift $((OPTIND-1))
91 1.9 christos case $# in
92 1.9 christos 0) ;;
93 1.9 christos *) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
94 1.9 christos esac
95 1.6 mlelstv
96 1.1 jtc # Make sure the tables are readable.
97 1.1 jtc TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
98 1.1 jtc TZ_ZONE_TABLE=$TZDIR/zone.tab
99 1.1 jtc for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
100 1.1 jtc do
101 1.1 jtc <$f || {
102 1.1 jtc echo >&2 "$0: time zone files are not set up correctly"
103 1.1 jtc exit 1
104 1.1 jtc }
105 1.1 jtc done
106 1.1 jtc
107 1.1 jtc newline='
108 1.1 jtc '
109 1.1 jtc IFS=$newline
110 1.1 jtc
111 1.1 jtc
112 1.2 perry # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
113 1.1 jtc case $(echo 1 | (select x in x; do break; done) 2>/dev/null) in
114 1.1 jtc ?*) PS3=
115 1.1 jtc esac
116 1.1 jtc
117 1.9 christos # Awk script to read a time zone table and output the same table,
118 1.9 christos # with each column preceded by its distance from 'here'.
119 1.9 christos output_distances='
120 1.9 christos BEGIN {
121 1.9 christos FS = "\t"
122 1.9 christos while (getline <TZ_COUNTRY_TABLE)
123 1.9 christos if ($0 ~ /^[^#]/)
124 1.9 christos country[$1] = $2
125 1.9 christos country["US"] = "US" # Otherwise the strings get too long.
126 1.9 christos }
127 1.9 christos function convert_coord(coord, deg, min, ilen, sign, sec) {
128 1.9 christos if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
129 1.9 christos degminsec = coord
130 1.9 christos intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
131 1.9 christos minsec = degminsec - intdeg * 10000
132 1.9 christos intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
133 1.9 christos sec = minsec - intmin * 100
134 1.9 christos deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
135 1.9 christos } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
136 1.9 christos degmin = coord
137 1.9 christos intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
138 1.9 christos min = degmin - intdeg * 100
139 1.9 christos deg = (intdeg * 60 + min) / 60
140 1.9 christos } else
141 1.9 christos deg = coord
142 1.9 christos return deg * 0.017453292519943296
143 1.9 christos }
144 1.9 christos function convert_latitude(coord) {
145 1.9 christos match(coord, /..*[-+]/)
146 1.9 christos return convert_coord(substr(coord, 1, RLENGTH - 1))
147 1.9 christos }
148 1.9 christos function convert_longitude(coord) {
149 1.9 christos match(coord, /..*[-+]/)
150 1.9 christos return convert_coord(substr(coord, RLENGTH))
151 1.9 christos }
152 1.9 christos # Great-circle distance between points with given latitude and longitude.
153 1.9 christos # Inputs and output are in radians. This uses the great-circle special
154 1.9 christos # case of the Vicenty formula for distances on ellipsoids.
155 1.9 christos function dist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
156 1.9 christos dlong = long2 - long1
157 1.9 christos x = cos (lat2) * sin (dlong)
158 1.9 christos y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
159 1.9 christos num = sqrt (x * x + y * y)
160 1.9 christos denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
161 1.9 christos return atan2(num, denom)
162 1.9 christos }
163 1.9 christos BEGIN {
164 1.9 christos coord_lat = convert_latitude(coord)
165 1.9 christos coord_long = convert_longitude(coord)
166 1.9 christos }
167 1.9 christos /^[^#]/ {
168 1.9 christos here_lat = convert_latitude($2)
169 1.9 christos here_long = convert_longitude($2)
170 1.9 christos line = $1 "\t" $2 "\t" $3 "\t" country[$1]
171 1.9 christos if (NF == 4)
172 1.9 christos line = line " - " $4
173 1.9 christos printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
174 1.9 christos }
175 1.9 christos '
176 1.1 jtc
177 1.1 jtc # Begin the main loop. We come back here if the user wants to retry.
178 1.1 jtc while
179 1.1 jtc
180 1.1 jtc echo >&2 'Please identify a location' \
181 1.1 jtc 'so that time zone rules can be set correctly.'
182 1.1 jtc
183 1.1 jtc continent=
184 1.1 jtc country=
185 1.1 jtc region=
186 1.1 jtc
187 1.9 christos case $coord in
188 1.9 christos ?*)
189 1.9 christos continent=coord;;
190 1.9 christos '')
191 1.1 jtc
192 1.1 jtc # Ask the user for continent or ocean.
193 1.1 jtc
194 1.9 christos echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
195 1.1 jtc
196 1.9 christos quoted_continents=$(
197 1.9 christos $AWK -F'\t' '
198 1.9 christos /^[^#]/ {
199 1.9 christos entry = substr($3, 1, index($3, "/") - 1)
200 1.9 christos if (entry == "America")
201 1.9 christos entry = entry "s"
202 1.9 christos if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
203 1.9 christos entry = entry " Ocean"
204 1.9 christos printf "'\''%s'\''\n", entry
205 1.9 christos }
206 1.9 christos ' $TZ_ZONE_TABLE |
207 1.9 christos sort -u |
208 1.9 christos tr '\n' ' '
209 1.9 christos echo ''
210 1.9 christos )
211 1.9 christos
212 1.9 christos eval '
213 1.9 christos select continent in '"$quoted_continents"' \
214 1.9 christos "coord - I want to use geographical coordinates." \
215 1.9 christos "TZ - I want to specify the time zone using the Posix TZ format."
216 1.9 christos do
217 1.1 jtc case $continent in
218 1.9 christos "")
219 1.9 christos echo >&2 "Please enter a number in range.";;
220 1.9 christos ?*)
221 1.9 christos case $continent in
222 1.9 christos Americas) continent=America;;
223 1.9 christos *" "*) continent=$(expr "$continent" : '\''\([^ ]*\)'\'')
224 1.9 christos esac
225 1.9 christos break
226 1.1 jtc esac
227 1.9 christos done
228 1.9 christos '
229 1.9 christos esac
230 1.9 christos
231 1.1 jtc case $continent in
232 1.1 jtc '')
233 1.1 jtc exit 1;;
234 1.9 christos TZ)
235 1.1 jtc # Ask the user for a Posix TZ string. Check that it conforms.
236 1.1 jtc while
237 1.1 jtc echo >&2 'Please enter the desired value' \
238 1.1 jtc 'of the TZ environment variable.'
239 1.1 jtc echo >&2 'For example, GST-10 is a zone named GST' \
240 1.1 jtc 'that is 10 hours ahead (east) of UTC.'
241 1.1 jtc read TZ
242 1.1 jtc $AWK -v TZ="$TZ" 'BEGIN {
243 1.1 jtc tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
244 1.1 jtc time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
245 1.1 jtc offset = "[-+]?" time
246 1.1 jtc date = "(J?[0-9]+|M[0-9]+\.[0-9]+\.[0-9]+)"
247 1.1 jtc datetime = "," date "(/" time ")?"
248 1.1 jtc tzpattern = "^(:.*|" tzname offset "(" tzname \
249 1.1 jtc "(" offset ")?(" datetime datetime ")?)?)$"
250 1.1 jtc if (TZ ~ tzpattern) exit 1
251 1.1 jtc exit 0
252 1.1 jtc }'
253 1.1 jtc do
254 1.1 jtc echo >&2 "\`$TZ' is not a conforming" \
255 1.1 jtc 'Posix time zone string.'
256 1.1 jtc done
257 1.1 jtc TZ_for_date=$TZ;;
258 1.1 jtc *)
259 1.9 christos case $continent in
260 1.9 christos coord)
261 1.9 christos case $coord in
262 1.9 christos '')
263 1.9 christos echo >&2 'Please enter coordinates' \
264 1.9 christos 'in ISO 6709 notation.'
265 1.9 christos echo >&2 'For example, +4042-07403 stands for'
266 1.9 christos echo >&2 '40 degrees 42 minutes north,' \
267 1.9 christos '74 degrees 3 minutes west.'
268 1.9 christos read coord;;
269 1.9 christos esac
270 1.9 christos distance_table=$($AWK \
271 1.9 christos -v coord="$coord" \
272 1.9 christos -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
273 1.9 christos "$output_distances" <$TZ_ZONE_TABLE |
274 1.9 christos sort -n |
275 1.9 christos sed "${location_limit}q"
276 1.9 christos )
277 1.9 christos regions=$(echo "$distance_table" | $AWK '
278 1.9 christos BEGIN { FS = "\t" }
279 1.9 christos { print $NF }
280 1.9 christos ')
281 1.9 christos echo >&2 'Please select one of the following' \
282 1.9 christos 'time zone regions,'
283 1.9 christos echo >&2 'listed roughly in increasing order' \
284 1.9 christos "of distance from $coord".
285 1.9 christos select region in $regions
286 1.9 christos do
287 1.9 christos case $region in
288 1.9 christos '') echo >&2 'Please enter a number in range.';;
289 1.9 christos ?*) break;;
290 1.9 christos esac
291 1.9 christos done
292 1.9 christos TZ=$(echo "$distance_table" | $AWK -v region="$region" '
293 1.9 christos BEGIN { FS="\t" }
294 1.9 christos $NF == region { print $4 }
295 1.9 christos ')
296 1.9 christos ;;
297 1.9 christos *)
298 1.1 jtc # Get list of names of countries in the continent or ocean.
299 1.1 jtc countries=$($AWK -F'\t' \
300 1.1 jtc -v continent="$continent" \
301 1.1 jtc -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
302 1.1 jtc '
303 1.1 jtc /^#/ { next }
304 1.1 jtc $3 ~ ("^" continent "/") {
305 1.1 jtc if (!cc_seen[$1]++) cc_list[++ccs] = $1
306 1.1 jtc }
307 1.1 jtc END {
308 1.1 jtc while (getline <TZ_COUNTRY_TABLE) {
309 1.1 jtc if ($0 !~ /^#/) cc_name[$1] = $2
310 1.1 jtc }
311 1.1 jtc for (i = 1; i <= ccs; i++) {
312 1.1 jtc country = cc_list[i]
313 1.1 jtc if (cc_name[country]) {
314 1.1 jtc country = cc_name[country]
315 1.1 jtc }
316 1.1 jtc print country
317 1.1 jtc }
318 1.1 jtc }
319 1.1 jtc ' <$TZ_ZONE_TABLE | sort -f)
320 1.1 jtc
321 1.1 jtc
322 1.1 jtc # If there's more than one country, ask the user which one.
323 1.1 jtc case $countries in
324 1.1 jtc *"$newline"*)
325 1.9 christos echo >&2 'Please select a country' \
326 1.9 christos 'whose clocks agree with yours.'
327 1.1 jtc select country in $countries
328 1.1 jtc do
329 1.1 jtc case $country in
330 1.1 jtc '') echo >&2 'Please enter a number in range.';;
331 1.1 jtc ?*) break
332 1.1 jtc esac
333 1.1 jtc done
334 1.1 jtc
335 1.1 jtc case $country in
336 1.1 jtc '') exit 1
337 1.1 jtc esac;;
338 1.1 jtc *)
339 1.1 jtc country=$countries
340 1.1 jtc esac
341 1.1 jtc
342 1.1 jtc
343 1.1 jtc # Get list of names of time zone rule regions in the country.
344 1.1 jtc regions=$($AWK -F'\t' \
345 1.1 jtc -v country="$country" \
346 1.1 jtc -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
347 1.1 jtc '
348 1.1 jtc BEGIN {
349 1.1 jtc cc = country
350 1.1 jtc while (getline <TZ_COUNTRY_TABLE) {
351 1.1 jtc if ($0 !~ /^#/ && country == $2) {
352 1.1 jtc cc = $1
353 1.1 jtc break
354 1.1 jtc }
355 1.1 jtc }
356 1.1 jtc }
357 1.1 jtc $1 == cc { print $4 }
358 1.1 jtc ' <$TZ_ZONE_TABLE)
359 1.1 jtc
360 1.1 jtc
361 1.1 jtc # If there's more than one region, ask the user which one.
362 1.1 jtc case $regions in
363 1.1 jtc *"$newline"*)
364 1.1 jtc echo >&2 'Please select one of the following' \
365 1.1 jtc 'time zone regions.'
366 1.1 jtc select region in $regions
367 1.1 jtc do
368 1.1 jtc case $region in
369 1.1 jtc '') echo >&2 'Please enter a number in range.';;
370 1.1 jtc ?*) break
371 1.1 jtc esac
372 1.1 jtc done
373 1.1 jtc case $region in
374 1.1 jtc '') exit 1
375 1.1 jtc esac;;
376 1.1 jtc *)
377 1.1 jtc region=$regions
378 1.1 jtc esac
379 1.1 jtc
380 1.1 jtc # Determine TZ from country and region.
381 1.1 jtc TZ=$($AWK -F'\t' \
382 1.1 jtc -v country="$country" \
383 1.1 jtc -v region="$region" \
384 1.1 jtc -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
385 1.1 jtc '
386 1.1 jtc BEGIN {
387 1.1 jtc cc = country
388 1.1 jtc while (getline <TZ_COUNTRY_TABLE) {
389 1.1 jtc if ($0 !~ /^#/ && country == $2) {
390 1.1 jtc cc = $1
391 1.1 jtc break
392 1.1 jtc }
393 1.1 jtc }
394 1.1 jtc }
395 1.1 jtc $1 == cc && $4 == region { print $3 }
396 1.1 jtc ' <$TZ_ZONE_TABLE)
397 1.9 christos esac
398 1.1 jtc
399 1.1 jtc # Make sure the corresponding zoneinfo file exists.
400 1.1 jtc TZ_for_date=$TZDIR/$TZ
401 1.1 jtc <$TZ_for_date || {
402 1.1 jtc echo >&2 "$0: time zone files are not set up correctly"
403 1.1 jtc exit 1
404 1.1 jtc }
405 1.1 jtc esac
406 1.1 jtc
407 1.1 jtc
408 1.1 jtc # Use the proposed TZ to output the current date relative to UTC.
409 1.1 jtc # Loop until they agree in seconds.
410 1.1 jtc # Give up after 8 unsuccessful tries.
411 1.1 jtc
412 1.1 jtc extra_info=
413 1.1 jtc for i in 1 2 3 4 5 6 7 8
414 1.1 jtc do
415 1.1 jtc TZdate=$(LANG=C TZ="$TZ_for_date" date)
416 1.1 jtc UTdate=$(LANG=C TZ=UTC0 date)
417 1.1 jtc TZsec=$(expr "$TZdate" : '.*:\([0-5][0-9]\)')
418 1.1 jtc UTsec=$(expr "$UTdate" : '.*:\([0-5][0-9]\)')
419 1.1 jtc case $TZsec in
420 1.1 jtc $UTsec)
421 1.1 jtc extra_info="
422 1.1 jtc Local time is now: $TZdate.
423 1.1 jtc Universal Time is now: $UTdate."
424 1.1 jtc break
425 1.1 jtc esac
426 1.1 jtc done
427 1.1 jtc
428 1.1 jtc
429 1.1 jtc # Output TZ info and ask the user to confirm.
430 1.1 jtc
431 1.1 jtc echo >&2 ""
432 1.1 jtc echo >&2 "The following information has been given:"
433 1.1 jtc echo >&2 ""
434 1.9 christos case $country%$region%$coord in
435 1.9 christos ?*%?*%) echo >&2 " $country$newline $region";;
436 1.9 christos ?*%%) echo >&2 " $country";;
437 1.9 christos %?*%?*) echo >&2 " coord $coord$newline $region";;
438 1.9 christos %%?*) echo >&2 " coord $coord";;
439 1.1 jtc +) echo >&2 " TZ='$TZ'"
440 1.1 jtc esac
441 1.1 jtc echo >&2 ""
442 1.1 jtc echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
443 1.1 jtc echo >&2 "Is the above information OK?"
444 1.1 jtc
445 1.1 jtc ok=
446 1.1 jtc select ok in Yes No
447 1.1 jtc do
448 1.1 jtc case $ok in
449 1.1 jtc '') echo >&2 'Please enter 1 for Yes, or 2 for No.';;
450 1.1 jtc ?*) break
451 1.1 jtc esac
452 1.1 jtc done
453 1.1 jtc case $ok in
454 1.1 jtc '') exit 1;;
455 1.1 jtc Yes) break
456 1.1 jtc esac
457 1.9 christos do coord=
458 1.1 jtc done
459 1.1 jtc
460 1.5 kleink case $SHELL in
461 1.5 kleink *csh) file=.login line="setenv TZ '$TZ'";;
462 1.5 kleink *) file=.profile line="TZ='$TZ'; export TZ"
463 1.5 kleink esac
464 1.5 kleink
465 1.5 kleink echo >&2 "
466 1.5 kleink You can make this change permanent for yourself by appending the line
467 1.5 kleink $line
468 1.5 kleink to the file '$file' in your home directory; then log out and log in again.
469 1.5 kleink
470 1.5 kleink Here is that TZ value again, this time on standard output so that you
471 1.5 kleink can use the $0 command in shell scripts:"
472 1.5 kleink
473 1.1 jtc echo "$TZ"
474