Fix build-and-build-again bug in sunrpc tests.
[glibc.git] / timezone / tzselect.ksh
blob2c3b2f44385515f5a4754c3909e5adb27f2a51a7
1 #!/bin/bash
3 PKGVERSION='(tzcode) '
4 TZVERSION=see_Makefile
5 REPORT_BUGS_TO=tz@iana.org
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.
10 # Contributed by Paul Eggert.
12 # Porting notes:
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:
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/>
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.
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:
32 # Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
33 # mawk <http://invisible-island.net/mawk/>
36 # Specify default values for environment variables if they are unset.
37 : ${AWK=awk}
38 : ${TZDIR=`pwd`}
40 # Output one argument as-is to standard output.
41 # Safer than 'echo', which can mishandle '\' or leading '-'.
42 say() {
43 printf '%s\n' "$1"
46 # Check for awk Posix compliance.
47 ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
48 [ $? = 123 ] || {
49 say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
50 exit 1
53 coord=
54 location_limit=10
55 zonetabtype=zone1970
57 usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
58 Select a time zone interactively.
60 Options:
62 -c COORD
63 Instead of asking for continent and then country and then city,
64 ask for selection from time zones whose largest cities
65 are closest to the location with geographical coordinates COORD.
66 COORD should use ISO 6709 notation, for example, '-c +4852+00220'
67 for Paris (in degrees and minutes, North and East), or
68 '-c -35-058' for Buenos Aires (in degrees, South and West).
70 -n LIMIT
71 Display at most LIMIT locations when -c is used (default $location_limit).
73 --version
74 Output version information.
76 --help
77 Output this help.
79 Report bugs to $REPORT_BUGS_TO."
81 # Ask the user to select from the function's arguments,
82 # and assign the selected argument to the variable 'select_result'.
83 # Exit on EOF or I/O error. Use the shell's 'select' builtin if available,
84 # falling back on a less-nice but portable substitute otherwise.
86 case $BASH_VERSION in
87 ?*) : ;;
88 '')
89 # '; exit' should be redundant, but Dash doesn't properly fail without it.
90 (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
91 esac
92 then
93 # Do this inside 'eval', as otherwise the shell might exit when parsing it
94 # even though it is never executed.
95 eval '
96 doselect() {
97 select select_result
99 case $select_result in
100 "") echo >&2 "Please enter a number in range." ;;
101 ?*) break
102 esac
103 done || exit
106 # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
107 case $BASH_VERSION in
108 [01].*)
109 case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
110 ?*) PS3=
111 esac
112 esac
114 else
115 doselect() {
116 # Field width of the prompt numbers.
117 select_width=`expr $# : '.*'`
119 select_i=
121 while :
123 case $select_i in
125 select_i=0
126 for select_word
128 select_i=`expr $select_i + 1`
129 printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
130 done ;;
131 *[!0-9]*)
132 echo >&2 'Please enter a number in range.' ;;
134 if test 1 -le $select_i && test $select_i -le $#; then
135 shift `expr $select_i - 1`
136 select_result=$1
137 break
139 echo >&2 'Please enter a number in range.'
140 esac
142 # Prompt and read input.
143 printf >&2 %s "${PS3-#? }"
144 read select_i || exit
145 done
149 while getopts c:n:t:-: opt
151 case $opt$OPTARG in
153 coord=$OPTARG ;;
155 location_limit=$OPTARG ;;
156 t*) # Undocumented option, used for developer testing.
157 zonetabtype=$OPTARG ;;
158 -help)
159 exec echo "$usage" ;;
160 -version)
161 exec echo "tzselect $PKGVERSION$TZVERSION" ;;
163 say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
165 say >&2 "$0: try '$0 --help'"; exit 1 ;;
166 esac
167 done
169 shift `expr $OPTIND - 1`
170 case $# in
171 0) ;;
172 *) say >&2 "$0: $1: unknown argument"; exit 1 ;;
173 esac
175 # Make sure the tables are readable.
176 TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
177 TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
178 for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
180 <"$f" || {
181 say >&2 "$0: time zone files are not set up correctly"
182 exit 1
184 done
186 # If the current locale does not support UTF-8, convert data to current
187 # locale's format if possible, as the shell aligns columns better that way.
188 # Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
189 ! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' &&
190 { tmp=`(mktemp -d) 2>/dev/null` || {
191 tmp=${TMPDIR-/tmp}/tzselect.$$ &&
192 (umask 77 && mkdir -- "$tmp")
193 };} &&
194 trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
195 (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
196 2>/dev/null &&
197 TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
198 iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
199 TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
201 newline='
203 IFS=$newline
206 # Awk script to read a time zone table and output the same table,
207 # with each column preceded by its distance from 'here'.
208 output_distances='
209 BEGIN {
210 FS = "\t"
211 while (getline <TZ_COUNTRY_TABLE)
212 if ($0 ~ /^[^#]/)
213 country[$1] = $2
214 country["US"] = "US" # Otherwise the strings get too long.
216 function abs(x) {
217 return x < 0 ? -x : x;
219 function min(x, y) {
220 return x < y ? x : y;
222 function convert_coord(coord, deg, minute, ilen, sign, sec) {
223 if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
224 degminsec = coord
225 intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
226 minsec = degminsec - intdeg * 10000
227 intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
228 sec = minsec - intmin * 100
229 deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
230 } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
231 degmin = coord
232 intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
233 minute = degmin - intdeg * 100
234 deg = (intdeg * 60 + minute) / 60
235 } else
236 deg = coord
237 return deg * 0.017453292519943296
239 function convert_latitude(coord) {
240 match(coord, /..*[-+]/)
241 return convert_coord(substr(coord, 1, RLENGTH - 1))
243 function convert_longitude(coord) {
244 match(coord, /..*[-+]/)
245 return convert_coord(substr(coord, RLENGTH))
247 # Great-circle distance between points with given latitude and longitude.
248 # Inputs and output are in radians. This uses the great-circle special
249 # case of the Vicenty formula for distances on ellipsoids.
250 function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
251 dlong = long2 - long1
252 x = cos(lat2) * sin(dlong)
253 y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
254 num = sqrt(x * x + y * y)
255 denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
256 return atan2(num, denom)
258 # Parallel distance between points with given latitude and longitude.
259 # This is the product of the longitude difference and the cosine
260 # of the latitude of the point that is further from the equator.
261 # I.e., it considers longitudes to be further apart if they are
262 # nearer the equator.
263 function pardist(lat1, long1, lat2, long2) {
264 return abs(long1 - long2) * min(cos(lat1), cos(lat2))
266 # The distance function is the sum of the great-circle distance and
267 # the parallel distance. It could be weighted.
268 function dist(lat1, long1, lat2, long2) {
269 return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
271 BEGIN {
272 coord_lat = convert_latitude(coord)
273 coord_long = convert_longitude(coord)
275 /^[^#]/ {
276 here_lat = convert_latitude($2)
277 here_long = convert_longitude($2)
278 line = $1 "\t" $2 "\t" $3
279 sep = "\t"
280 ncc = split($1, cc, /,/)
281 for (i = 1; i <= ncc; i++) {
282 line = line sep country[cc[i]]
283 sep = ", "
285 if (NF == 4)
286 line = line " - " $4
287 printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
291 # Begin the main loop. We come back here if the user wants to retry.
292 while
294 echo >&2 'Please identify a location' \
295 'so that time zone rules can be set correctly.'
297 continent=
298 country=
299 region=
301 case $coord in
303 continent=coord;;
306 # Ask the user for continent or ocean.
308 echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
310 quoted_continents=`
311 $AWK '
312 BEGIN { FS = "\t" }
313 /^[^#]/ {
314 entry = substr($3, 1, index($3, "/") - 1)
315 if (entry == "America")
316 entry = entry "s"
317 if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
318 entry = entry " Ocean"
319 printf "'\''%s'\''\n", entry
321 ' <"$TZ_ZONE_TABLE" |
322 sort -u |
323 tr '\n' ' '
324 echo ''
327 eval '
328 doselect '"$quoted_continents"' \
329 "coord - I want to use geographical coordinates." \
330 "TZ - I want to specify the time zone using the Posix TZ format."
331 continent=$select_result
332 case $continent in
333 Americas) continent=America;;
334 *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
335 esac
337 esac
339 case $continent in
341 # Ask the user for a Posix TZ string. Check that it conforms.
342 while
343 echo >&2 'Please enter the desired value' \
344 'of the TZ environment variable.'
345 echo >&2 'For example, GST-10 is a zone named GST' \
346 'that is 10 hours ahead (east) of UTC.'
347 read TZ
348 $AWK -v TZ="$TZ" 'BEGIN {
349 tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
350 time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
351 offset = "[-+]?" time
352 date = "(J?[0-9]+|M[0-9]+\\.[0-9]+\\.[0-9]+)"
353 datetime = "," date "(/" time ")?"
354 tzpattern = "^(:.*|" tzname offset "(" tzname \
355 "(" offset ")?(" datetime datetime ")?)?)$"
356 if (TZ ~ tzpattern) exit 1
357 exit 0
360 say >&2 "'$TZ' is not a conforming Posix time zone string."
361 done
362 TZ_for_date=$TZ;;
364 case $continent in
365 coord)
366 case $coord in
368 echo >&2 'Please enter coordinates' \
369 'in ISO 6709 notation.'
370 echo >&2 'For example, +4042-07403 stands for'
371 echo >&2 '40 degrees 42 minutes north,' \
372 '74 degrees 3 minutes west.'
373 read coord;;
374 esac
375 distance_table=`$AWK \
376 -v coord="$coord" \
377 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
378 "$output_distances" <"$TZ_ZONE_TABLE" |
379 sort -n |
380 sed "${location_limit}q"
382 regions=`say "$distance_table" | $AWK '
383 BEGIN { FS = "\t" }
384 { print $NF }
386 echo >&2 'Please select one of the following' \
387 'time zone regions,'
388 echo >&2 'listed roughly in increasing order' \
389 "of distance from $coord".
390 doselect $regions
391 region=$select_result
392 TZ=`say "$distance_table" | $AWK -v region="$region" '
393 BEGIN { FS="\t" }
394 $NF == region { print $4 }
398 # Get list of names of countries in the continent or ocean.
399 countries=`$AWK \
400 -v continent="$continent" \
401 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
403 BEGIN { FS = "\t" }
404 /^#/ { next }
405 $3 ~ ("^" continent "/") {
406 ncc = split($1, cc, /,/)
407 for (i = 1; i <= ncc; i++)
408 if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
410 END {
411 while (getline <TZ_COUNTRY_TABLE) {
412 if ($0 !~ /^#/) cc_name[$1] = $2
414 for (i = 1; i <= ccs; i++) {
415 country = cc_list[i]
416 if (cc_name[country]) {
417 country = cc_name[country]
419 print country
422 ' <"$TZ_ZONE_TABLE" | sort -f`
425 # If there's more than one country, ask the user which one.
426 case $countries in
427 *"$newline"*)
428 echo >&2 'Please select a country' \
429 'whose clocks agree with yours.'
430 doselect $countries
431 country=$select_result;;
433 country=$countries
434 esac
437 # Get list of names of time zone rule regions in the country.
438 regions=`$AWK \
439 -v country="$country" \
440 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
442 BEGIN {
443 FS = "\t"
444 cc = country
445 while (getline <TZ_COUNTRY_TABLE) {
446 if ($0 !~ /^#/ && country == $2) {
447 cc = $1
448 break
452 /^#/ { next }
453 $1 ~ cc { print $4 }
454 ' <"$TZ_ZONE_TABLE"`
457 # If there's more than one region, ask the user which one.
458 case $regions in
459 *"$newline"*)
460 echo >&2 'Please select one of the following' \
461 'time zone regions.'
462 doselect $regions
463 region=$select_result;;
465 region=$regions
466 esac
468 # Determine TZ from country and region.
469 TZ=`$AWK \
470 -v country="$country" \
471 -v region="$region" \
472 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
474 BEGIN {
475 FS = "\t"
476 cc = country
477 while (getline <TZ_COUNTRY_TABLE) {
478 if ($0 !~ /^#/ && country == $2) {
479 cc = $1
480 break
484 /^#/ { next }
485 $1 ~ cc && $4 == region { print $3 }
486 ' <"$TZ_ZONE_TABLE"`
487 esac
489 # Make sure the corresponding zoneinfo file exists.
490 TZ_for_date=$TZDIR/$TZ
491 <"$TZ_for_date" || {
492 say >&2 "$0: time zone files are not set up correctly"
493 exit 1
495 esac
498 # Use the proposed TZ to output the current date relative to UTC.
499 # Loop until they agree in seconds.
500 # Give up after 8 unsuccessful tries.
502 extra_info=
503 for i in 1 2 3 4 5 6 7 8
505 TZdate=`LANG=C TZ="$TZ_for_date" date`
506 UTdate=`LANG=C TZ=UTC0 date`
507 TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
508 UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
509 case $TZsec in
510 $UTsec)
511 extra_info="
512 Local time is now: $TZdate.
513 Universal Time is now: $UTdate."
514 break
515 esac
516 done
519 # Output TZ info and ask the user to confirm.
521 echo >&2 ""
522 echo >&2 "The following information has been given:"
523 echo >&2 ""
524 case $country%$region%$coord in
525 ?*%?*%) say >&2 " $country$newline $region";;
526 ?*%%) say >&2 " $country";;
527 %?*%?*) say >&2 " coord $coord$newline $region";;
528 %%?*) say >&2 " coord $coord";;
529 *) say >&2 " TZ='$TZ'"
530 esac
531 say >&2 ""
532 say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
533 say >&2 "Is the above information OK?"
535 doselect Yes No
536 ok=$select_result
537 case $ok in
538 Yes) break
539 esac
540 do coord=
541 done
543 case $SHELL in
544 *csh) file=.login line="setenv TZ '$TZ'";;
545 *) file=.profile line="TZ='$TZ'; export TZ"
546 esac
548 say >&2 "
549 You can make this change permanent for yourself by appending the line
550 $line
551 to the file '$file' in your home directory; then log out and log in again.
553 Here is that TZ value again, this time on standard output so that you
554 can use the $0 command in shell scripts:"
556 say "$TZ"