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