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.
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.
40 # Check for awk Posix compliance.
41 ($AWK -v x
=y
'BEGIN { exit 123 }') </dev
/null
>/dev
/null
2>&1
43 echo >&2 "$0: Sorry, your \`$AWK' program is not Posix compatible."
50 usage
="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
51 Select a time zone interactively.
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).
64 Display at most LIMIT locations when -c is used (default $location_limit).
67 Output version information.
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.
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
86 # Do this inside 'eval', as otherwise the shell might exit when parsing it
87 # even though it is never executed.
92 case $select_result in
93 "") echo >&2 "Please enter a number in range." ;;
99 # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
100 case $BASH_VERSION in
102 case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
109 # Field width of the prompt numbers.
110 select_width
=`expr $# : '.*'`
121 select_i
=`expr $select_i + 1`
122 printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
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`
132 echo >&2 'Please enter a number in range.'
135 # Prompt and read input.
136 printf >&2 %s
"${PS3-#? }"
137 read select_i ||
exit
142 while getopts c
:n
:-: opt
148 location_limit
=$OPTARG ;;
150 exec echo "$usage" ;;
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 ;;
160 shift `expr $OPTIND - 1`
163 *) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
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
172 echo >&2 "$0: time zone files are not set up correctly"
182 # Awk script to read a time zone table and output the same table,
183 # with each column preceded by its distance from 'here'.
187 while (getline <TZ_COUNTRY_TABLE)
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]|$)/) {
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]|$)/) {
202 intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
203 min = degmin - intdeg * 100
204 deg = (intdeg * 60 + min) / 60
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)
229 coord_lat = convert_latitude(coord)
230 coord_long = convert_longitude(coord)
233 here_lat = convert_latitude($2)
234 here_long = convert_longitude($2)
235 line = $1 "\t" $2 "\t" $3 "\t" country[$1]
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.
245 echo >&2 'Please identify a location' \
246 'so that time zone rules can be set correctly.'
257 # Ask the user for continent or ocean.
259 echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
265 entry = substr($3, 1, index($3, "/") - 1)
266 if (entry == "America")
268 if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
269 entry = entry " Ocean"
270 printf "'\''%s'\''\n", entry
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
284 Americas) continent=America;;
285 *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
292 # Ask the user for a Posix TZ string. Check that it conforms.
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.'
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
311 echo >&2 "\`$TZ' is not a conforming" \
312 'Posix time zone string.'
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.'
327 distance_table
=`$AWK \
329 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
330 "$output_distances" <$TZ_ZONE_TABLE |
332 sed "${location_limit}q"
334 regions
=`echo "$distance_table" | $AWK '
338 echo >&2 'Please select one of the following' \
340 echo >&2 'listed roughly in increasing order' \
341 "of distance from $coord".
343 region
=$select_result
344 TZ
=`echo "$distance_table" | $AWK -v region="$region" '
346 $NF == region { print $4 }
350 # Get list of names of countries in the continent or ocean.
352 -v continent="$continent" \
353 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
357 $3 ~ ("^" continent "/") {
358 if (!cc_seen[$1]++) cc_list[++ccs] = $1
361 while (getline <TZ_COUNTRY_TABLE) {
362 if ($0 !~ /^#/) cc_name[$1] = $2
364 for (i = 1; i <= ccs; i++) {
366 if (cc_name[country]) {
367 country = cc_name[country]
372 ' <$TZ_ZONE_TABLE | sort -f`
375 # If there's more than one country, ask the user which one.
378 echo >&2 'Please select a country' \
379 'whose clocks agree with yours.'
381 country
=$select_result;;
387 # Get list of names of time zone rule regions in the country.
389 -v country="$country" \
390 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
395 while (getline <TZ_COUNTRY_TABLE) {
396 if ($0 !~ /^#/ && country == $2) {
402 $1 == cc { print $4 }
406 # If there's more than one region, ask the user which one.
409 echo >&2 'Please select one of the following' \
412 region
=$select_result;;
417 # Determine TZ from country and region.
419 -v country="$country" \
420 -v region="$region" \
421 -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
426 while (getline <TZ_COUNTRY_TABLE) {
427 if ($0 !~ /^#/ && country == $2) {
433 $1 == cc && $4 == region { print $3 }
437 # Make sure the corresponding zoneinfo file exists.
438 TZ_for_date
=$TZDIR/$TZ
440 echo >&2 "$0: time zone files are not set up correctly"
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.
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]\)'`
460 Local time is now: $TZdate.
461 Universal Time is now: $UTdate."
467 # Output TZ info and ask the user to confirm.
470 echo >&2 "The following information has been given:"
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'"
480 echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
481 echo >&2 "Is the above information OK?"
492 *csh
) file=.login line
="setenv TZ '$TZ'";;
493 *) file=.profile line
="TZ='$TZ'; export TZ"
497 You can make this change permanent for yourself by appending the 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:"