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