struct stat is not posix conform
[glibc.git] / timezone / tzselect.ksh
blob9d7069116aab1daa0183b77b2a582fa552dcf73a
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 # 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
47 coord=
48 location_limit=10
50 usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
51 Select a time zone interactively.
53 Options:
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).
63 -n LIMIT
64 Display at most LIMIT locations when -c is used (default $location_limit).
66 --version
67 Output version information.
69 --help
70 Output this help.
72 Report bugs to $REPORT_BUGS_TO."
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.
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
85 then
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
92 case $select_result in
93 "") echo >&2 "Please enter a number in range." ;;
94 ?*) break
95 esac
96 done || exit
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
107 else
108 doselect() {
109 # Field width of the prompt numbers.
110 select_width=`expr $# : '.*'`
112 select_i=
114 while :
116 case $select_i in
118 select_i=0
119 for select_word
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.' ;;
127 if test 1 -le $select_i && test $select_i -le $#; then
128 shift `expr $select_i - 1`
129 select_result=$1
130 break
132 echo >&2 'Please enter a number in range.'
133 esac
135 # Prompt and read input.
136 printf >&2 %s "${PS3-#? }"
137 read select_i || exit
138 done
142 while getopts c:n:-: opt
144 case $opt$OPTARG in
146 coord=$OPTARG ;;
148 location_limit=$OPTARG ;;
149 -help)
150 exec echo "$usage" ;;
151 -version)
152 exec echo "tzselect $PKGVERSION$TZVERSION" ;;
154 echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
156 echo >&2 "$0: try '$0 --help'"; exit 1 ;;
157 esac
158 done
160 shift `expr $OPTIND - 1`
161 case $# in
162 0) ;;
163 *) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
164 esac
166 # Make sure the tables are readable.
167 TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
168 TZ_ZONE_TABLE=$TZDIR/zone.tab
169 for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
171 <$f || {
172 echo >&2 "$0: time zone files are not set up correctly"
173 exit 1
175 done
177 newline='
179 IFS=$newline
182 # Awk script to read a time zone table and output the same table,
183 # with each column preceded by its distance from 'here'.
184 output_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.
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
209 function convert_latitude(coord) {
210 match(coord, /..*[-+]/)
211 return convert_coord(substr(coord, 1, RLENGTH - 1))
213 function convert_longitude(coord) {
214 match(coord, /..*[-+]/)
215 return convert_coord(substr(coord, RLENGTH))
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)
228 BEGIN {
229 coord_lat = convert_latitude(coord)
230 coord_long = convert_longitude(coord)
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
242 # Begin the main loop. We come back here if the user wants to retry.
243 while
245 echo >&2 'Please identify a location' \
246 'so that time zone rules can be set correctly.'
248 continent=
249 country=
250 region=
252 case $coord in
254 continent=coord;;
257 # Ask the user for continent or ocean.
259 echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
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
272 ' $TZ_ZONE_TABLE |
273 sort -u |
274 tr '\n' ' '
275 echo ''
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
288 esac
290 case $continent in
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
311 echo >&2 "\`$TZ' is not a conforming" \
312 'Posix time zone string.'
313 done
314 TZ_for_date=$TZ;;
316 case $continent in
317 coord)
318 case $coord in
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"
334 regions=`echo "$distance_table" | $AWK '
335 BEGIN { FS = "\t" }
336 { print $NF }
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 }
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" \
355 BEGIN { FS = "\t" }
356 /^#/ { next }
357 $3 ~ ("^" continent "/") {
358 if (!cc_seen[$1]++) cc_list[++ccs] = $1
360 END {
361 while (getline <TZ_COUNTRY_TABLE) {
362 if ($0 !~ /^#/) cc_name[$1] = $2
364 for (i = 1; i <= ccs; i++) {
365 country = cc_list[i]
366 if (cc_name[country]) {
367 country = cc_name[country]
369 print country
372 ' <$TZ_ZONE_TABLE | sort -f`
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;;
383 country=$countries
384 esac
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" \
392 BEGIN {
393 FS = "\t"
394 cc = country
395 while (getline <TZ_COUNTRY_TABLE) {
396 if ($0 !~ /^#/ && country == $2) {
397 cc = $1
398 break
402 $1 == cc { print $4 }
403 ' <$TZ_ZONE_TABLE`
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;;
414 region=$regions
415 esac
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" \
423 BEGIN {
424 FS = "\t"
425 cc = country
426 while (getline <TZ_COUNTRY_TABLE) {
427 if ($0 !~ /^#/ && country == $2) {
428 cc = $1
429 break
433 $1 == cc && $4 == region { print $3 }
434 ' <$TZ_ZONE_TABLE`
435 esac
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
443 esac
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.
450 extra_info=
451 for i in 1 2 3 4 5 6 7 8
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="
460 Local time is now: $TZdate.
461 Universal Time is now: $UTdate."
462 break
463 esac
464 done
467 # Output TZ info and ask the user to confirm.
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?"
483 doselect Yes No
484 ok=$select_result
485 case $ok in
486 Yes) break
487 esac
488 do coord=
489 done
491 case $SHELL in
492 *csh) file=.login line="setenv TZ '$TZ'";;
493 *) file=.profile line="TZ='$TZ'; export TZ"
494 esac
496 echo >&2 "
497 You can make this change permanent for yourself by appending the line
498 $line
499 to the file '$file' in your home directory; then log out and log in again.
501 Here is that TZ value again, this time on standard output so that you
502 can use the $0 command in shell scripts:"
504 echo "$TZ"