tzselect.ksh revision 1.10.4.1 1 #! /bin/bash
2 #
3 # $NetBSD: tzselect.ksh,v 1.10.4.1 2015/01/25 09:11:03 martin 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:t:-: 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 abs(x) {
198 return x < 0 ? -x : x;
199 }
200 function min(x, y) {
201 return x < y ? x : y;
202 }
203 function convert_coord(coord, deg, minute, ilen, sign, sec) {
204 if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
205 degminsec = coord
206 intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
207 minsec = degminsec - intdeg * 10000
208 intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
209 sec = minsec - intmin * 100
210 deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
211 } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
212 degmin = coord
213 intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
214 minute = degmin - intdeg * 100
215 deg = (intdeg * 60 + minute) / 60
216 } else
217 deg = coord
218 return deg * 0.017453292519943296
219 }
220 function convert_latitude(coord) {
221 match(coord, /..*[-+]/)
222 return convert_coord(substr(coord, 1, RLENGTH - 1))
223 }
224 function convert_longitude(coord) {
225 match(coord, /..*[-+]/)
226 return convert_coord(substr(coord, RLENGTH))
227 }
228 # Great-circle distance between points with given latitude and longitude.
229 # Inputs and output are in radians. This uses the great-circle special
230 # case of the Vicenty formula for distances on ellipsoids.
231 function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
232 dlong = long2 - long1
233 x = cos (lat2) * sin (dlong)
234 y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
235 num = sqrt (x * x + y * y)
236 denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
237 return atan2(num, denom)
238 }
239 # Parallel distance between points with given latitude and longitude.
240 # This is the product of the longitude difference and the cosine
241 # of the latitude of the point that is further from the equator.
242 # I.e., it considers longitudes to be further apart if they are
243 # nearer the equator.
244 function pardist(lat1, long1, lat2, long2) {
245 return abs (long1 - long2) * min (cos (lat1), cos (lat2))
246 }
247 # The distance function is the sum of the great-circle distance and
248 # the parallel distance. It could be weighted.
249 function dist(lat1, long1, lat2, long2) {
250 return gcdist (lat1, long1, lat2, long2) + pardist (lat1, long1, lat2, long2)
251 }
252 BEGIN {
253 coord_lat = convert_latitude(coord)
254 coord_long = convert_longitude(coord)
255 }
256 /^[^#]/ {
257 here_lat = convert_latitude($2)
258 here_long = convert_longitude($2)
259 line = $1 "\t" $2 "\t" $3
260 sep = "\t"
261 ncc = split($1, cc, /,/)
262 for (i = 1; i <= ncc; i++) {
263 line = line sep country[cc[i]]
264 sep = ", "
265 }
266 if (NF == 4)
267 line = line " - " $4
268 printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
269 }
270 '
271
272 # Begin the main loop. We come back here if the user wants to retry.
273 while
274
275 echo >&2 'Please identify a location' \
276 'so that time zone rules can be set correctly.'
277
278 continent=
279 country=
280 region=
281
282 case $coord in
283 ?*)
284 continent=coord;;
285 '')
286
287 # Ask the user for continent or ocean.
288
289 echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
290
291 quoted_continents=`
292 $AWK '
293 BEGIN { FS = "\t" }
294 /^[^#]/ {
295 entry = substr($3, 1, index($3, "/") - 1)
296 if (entry == "America")
297 entry = entry "s"
298 if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
299 entry = entry " Ocean"
300 printf "'\''%s'\''\n", entry
301 }
302 ' <"$TZ_ZONE_TABLE" |
303 sort -u |
304 tr '\n' ' '
305 echo ''
306 `
307
308 eval '
309 doselect '"$quoted_continents"' \
310 "coord - I want to use geographical coordinates." \
311 "TZ - I want to specify the time zone using the Posix TZ format."
312 continent=$select_result
313 case $continent in
314 Americas) continent=America;;
315 *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
316 esac
317 '
318 esac
319
320 case $continent in
321 TZ)
322 # Ask the user for a Posix TZ string. Check that it conforms.
323 while
324 echo >&2 'Please enter the desired value' \
325 'of the TZ environment variable.'
326 echo >&2 'For example, GST-10 is a zone named GST' \
327 'that is 10 hours ahead (east) of UTC.'
328 read TZ
329 $AWK -v TZ="$TZ" 'BEGIN {
330 tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
331 time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
332 offset = "[-+]?" time
333 date = "(J?[0-9]+|M[0-9]+\.[0-9]+\.[0-9]+)"
334 datetime = "," date "(/" time ")?"
335 tzpattern = "^(:.*|" tzname offset "(" tzname \
336 "(" offset ")?(" datetime datetime ")?)?)$"
337 if (TZ ~ tzpattern) exit 1
338 exit 0
339 }'
340 do
341 echo >&2 "'$TZ' is not a conforming" \
342 'Posix time zone string.'
343 done
344 TZ_for_date=$TZ;;
345 *)
346 case $continent in
347 coord)
348 case $coord in
349 '')
350 echo >&2 'Please enter coordinates' \
351 'in ISO 6709 notation.'
352 echo >&2 'For example, +4042-07403 stands for'
353 echo >&2 '40 degrees 42 minutes north,' \
354 '74 degrees 3 minutes west.'
355 read coord;;
356 esac
357 distance_table=`$AWK \
358 -v coord="$coord" \
359 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
360 "$output_distances" <"$TZ_ZONE_TABLE" |
361 sort -n |
362 sed "${location_limit}q"
363 `
364 regions=`echo "$distance_table" | $AWK '
365 BEGIN { FS = "\t" }
366 { print $NF }
367 '`
368 echo >&2 'Please select one of the following' \
369 'time zone regions,'
370 echo >&2 'listed roughly in increasing order' \
371 "of distance from $coord".
372 doselect $regions
373 region=$select_result
374 TZ=`echo "$distance_table" | $AWK -v region="$region" '
375 BEGIN { FS="\t" }
376 $NF == region { print $4 }
377 '`
378 ;;
379 *)
380 # Get list of names of countries in the continent or ocean.
381 countries=`$AWK \
382 -v continent="$continent" \
383 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
384 '
385 BEGIN { FS = "\t" }
386 /^#/ { next }
387 $3 ~ ("^" continent "/") {
388 ncc = split($1, cc, /,/)
389 for (i = 1; i <= ncc; i++)
390 if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
391 }
392 END {
393 while (getline <TZ_COUNTRY_TABLE) {
394 if ($0 !~ /^#/) cc_name[$1] = $2
395 }
396 for (i = 1; i <= ccs; i++) {
397 country = cc_list[i]
398 if (cc_name[country]) {
399 country = cc_name[country]
400 }
401 print country
402 }
403 }
404 ' <"$TZ_ZONE_TABLE" | sort -f`
405
406
407 # If there's more than one country, ask the user which one.
408 case $countries in
409 *"$newline"*)
410 echo >&2 'Please select a country' \
411 'whose clocks agree with yours.'
412 doselect $countries
413 country=$select_result;;
414 *)
415 country=$countries
416 esac
417
418
419 # Get list of names of time zone rule regions in the country.
420 regions=`$AWK \
421 -v country="$country" \
422 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
423 '
424 BEGIN {
425 FS = "\t"
426 cc = country
427 while (getline <TZ_COUNTRY_TABLE) {
428 if ($0 !~ /^#/ && country == $2) {
429 cc = $1
430 break
431 }
432 }
433 }
434 $1 ~ cc { print $4 }
435 ' <"$TZ_ZONE_TABLE"`
436
437
438 # If there's more than one region, ask the user which one.
439 case $regions in
440 *"$newline"*)
441 echo >&2 'Please select one of the following' \
442 'time zone regions.'
443 doselect $regions
444 region=$select_result;;
445 *)
446 region=$regions
447 esac
448
449 # Determine TZ from country and region.
450 TZ=`$AWK \
451 -v country="$country" \
452 -v region="$region" \
453 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
454 '
455 BEGIN {
456 FS = "\t"
457 cc = country
458 while (getline <TZ_COUNTRY_TABLE) {
459 if ($0 !~ /^#/ && country == $2) {
460 cc = $1
461 break
462 }
463 }
464 }
465 $1 ~ cc && $4 == region { print $3 }
466 ' <"$TZ_ZONE_TABLE"`
467 esac
468
469 # Make sure the corresponding zoneinfo file exists.
470 TZ_for_date=$TZDIR/$TZ
471 <"$TZ_for_date" || {
472 echo >&2 "$0: time zone files are not set up correctly"
473 exit 1
474 }
475 esac
476
477
478 # Use the proposed TZ to output the current date relative to UTC.
479 # Loop until they agree in seconds.
480 # Give up after 8 unsuccessful tries.
481
482 extra_info=
483 for i in 1 2 3 4 5 6 7 8
484 do
485 TZdate=`LANG=C TZ="$TZ_for_date" date`
486 UTdate=`LANG=C TZ=UTC0 date`
487 TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
488 UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
489 case $TZsec in
490 $UTsec)
491 extra_info="
492 Local time is now: $TZdate.
493 Universal Time is now: $UTdate."
494 break
495 esac
496 done
497
498
499 # Output TZ info and ask the user to confirm.
500
501 echo >&2 ""
502 echo >&2 "The following information has been given:"
503 echo >&2 ""
504 case $country%$region%$coord in
505 ?*%?*%) echo >&2 " $country$newline $region";;
506 ?*%%) echo >&2 " $country";;
507 %?*%?*) echo >&2 " coord $coord$newline $region";;
508 %%?*) echo >&2 " coord $coord";;
509 +) echo >&2 " TZ='$TZ'"
510 esac
511 echo >&2 ""
512 echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
513 echo >&2 "Is the above information OK?"
514
515 doselect Yes No
516 ok=$select_result
517 case $ok in
518 Yes) break
519 esac
520 do coord=
521 done
522
523 case $SHELL in
524 *csh) file=.login line="setenv TZ '$TZ'";;
525 *) file=.profile line="TZ='$TZ'; export TZ"
526 esac
527
528 echo >&2 "
529 You can make this change permanent for yourself by appending the line
530 $line
531 to the file '$file' in your home directory; then log out and log in again.
532
533 Here is that TZ value again, this time on standard output so that you
534 can use the $0 command in shell scripts:"
535
536 echo "$TZ"
537