Ticket #3595: tzselect

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