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