Fix year 2039 bug for localtime with 64-bit time_t (bug 22639).
[glibc.git] / timezone / tzselect.ksh
blobd2c3a6d1dd9d4e7b5ef16f5fd921bb45caeb13ff
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. This file is in the public domain.
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 = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
350 time = "(2[0-4]|[0-1]?[0-9])" \
351 "(:[0-5][0-9](:[0-5][0-9])?)?"
352 offset = "[-+]?" time
353 mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
354 jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
355 "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
356 datetime = ",(" mdate "|" jdate ")(/" time ")?"
357 tzpattern = "^(:.*|" tzname offset "(" tzname \
358 "(" offset ")?(" datetime datetime ")?)?)$"
359 if (TZ ~ tzpattern) exit 1
360 exit 0
363 say >&2 "'$TZ' is not a conforming Posix time zone string."
364 done
365 TZ_for_date=$TZ;;
367 case $continent in
368 coord)
369 case $coord in
371 echo >&2 'Please enter coordinates' \
372 'in ISO 6709 notation.'
373 echo >&2 'For example, +4042-07403 stands for'
374 echo >&2 '40 degrees 42 minutes north,' \
375 '74 degrees 3 minutes west.'
376 read coord;;
377 esac
378 distance_table=`$AWK \
379 -v coord="$coord" \
380 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
381 "$output_distances" <"$TZ_ZONE_TABLE" |
382 sort -n |
383 sed "${location_limit}q"
385 regions=`say "$distance_table" | $AWK '
386 BEGIN { FS = "\t" }
387 { print $NF }
389 echo >&2 'Please select one of the following' \
390 'time zone regions,'
391 echo >&2 'listed roughly in increasing order' \
392 "of distance from $coord".
393 doselect $regions
394 region=$select_result
395 TZ=`say "$distance_table" | $AWK -v region="$region" '
396 BEGIN { FS="\t" }
397 $NF == region { print $4 }
401 # Get list of names of countries in the continent or ocean.
402 countries=`$AWK \
403 -v continent="$continent" \
404 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
406 BEGIN { FS = "\t" }
407 /^#/ { next }
408 $3 ~ ("^" continent "/") {
409 ncc = split($1, cc, /,/)
410 for (i = 1; i <= ncc; i++)
411 if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
413 END {
414 while (getline <TZ_COUNTRY_TABLE) {
415 if ($0 !~ /^#/) cc_name[$1] = $2
417 for (i = 1; i <= ccs; i++) {
418 country = cc_list[i]
419 if (cc_name[country]) {
420 country = cc_name[country]
422 print country
425 ' <"$TZ_ZONE_TABLE" | sort -f`
428 # If there's more than one country, ask the user which one.
429 case $countries in
430 *"$newline"*)
431 echo >&2 'Please select a country' \
432 'whose clocks agree with yours.'
433 doselect $countries
434 country=$select_result;;
436 country=$countries
437 esac
440 # Get list of names of time zone rule regions in the country.
441 regions=`$AWK \
442 -v country="$country" \
443 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
445 BEGIN {
446 FS = "\t"
447 cc = country
448 while (getline <TZ_COUNTRY_TABLE) {
449 if ($0 !~ /^#/ && country == $2) {
450 cc = $1
451 break
455 /^#/ { next }
456 $1 ~ cc { print $4 }
457 ' <"$TZ_ZONE_TABLE"`
460 # If there's more than one region, ask the user which one.
461 case $regions in
462 *"$newline"*)
463 echo >&2 'Please select one of the following' \
464 'time zone regions.'
465 doselect $regions
466 region=$select_result;;
468 region=$regions
469 esac
471 # Determine TZ from country and region.
472 TZ=`$AWK \
473 -v country="$country" \
474 -v region="$region" \
475 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
477 BEGIN {
478 FS = "\t"
479 cc = country
480 while (getline <TZ_COUNTRY_TABLE) {
481 if ($0 !~ /^#/ && country == $2) {
482 cc = $1
483 break
487 /^#/ { next }
488 $1 ~ cc && $4 == region { print $3 }
489 ' <"$TZ_ZONE_TABLE"`
490 esac
492 # Make sure the corresponding zoneinfo file exists.
493 TZ_for_date=$TZDIR/$TZ
494 <"$TZ_for_date" || {
495 say >&2 "$0: time zone files are not set up correctly"
496 exit 1
498 esac
501 # Use the proposed TZ to output the current date relative to UTC.
502 # Loop until they agree in seconds.
503 # Give up after 8 unsuccessful tries.
505 extra_info=
506 for i in 1 2 3 4 5 6 7 8
508 TZdate=`LANG=C TZ="$TZ_for_date" date`
509 UTdate=`LANG=C TZ=UTC0 date`
510 TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
511 UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
512 case $TZsec in
513 $UTsec)
514 extra_info="
515 Selected time is now: $TZdate.
516 Universal Time is now: $UTdate."
517 break
518 esac
519 done
522 # Output TZ info and ask the user to confirm.
524 echo >&2 ""
525 echo >&2 "The following information has been given:"
526 echo >&2 ""
527 case $country%$region%$coord in
528 ?*%?*%) say >&2 " $country$newline $region";;
529 ?*%%) say >&2 " $country";;
530 %?*%?*) say >&2 " coord $coord$newline $region";;
531 %%?*) say >&2 " coord $coord";;
532 *) say >&2 " TZ='$TZ'"
533 esac
534 say >&2 ""
535 say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
536 say >&2 "Is the above information OK?"
538 doselect Yes No
539 ok=$select_result
540 case $ok in
541 Yes) break
542 esac
543 do coord=
544 done
546 case $SHELL in
547 *csh) file=.login line="setenv TZ '$TZ'";;
548 *) file=.profile line="TZ='$TZ'; export TZ"
549 esac
551 test -t 1 && say >&2 "
552 You can make this change permanent for yourself by appending the line
553 $line
554 to the file '$file' in your home directory; then log out and log in again.
556 Here is that TZ value again, this time on standard output so that you
557 can use the $0 command in shell scripts:"
559 say "$TZ"