Home | History | Annotate | Line # | Download | only in time
tzselect.ksh revision 1.10.4.1
      1 #! /bin/bash
      2 #
      3 #	$NetBSD: tzselect.ksh,v 1.10.4.1 2015/01/25 09:11:03 martin 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:t:-: 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 abs(x) {
    198     return x < 0 ? -x : x;
    199   }
    200   function min(x, y) {
    201     return x < y ? x : y;
    202   }
    203   function convert_coord(coord, deg, minute, ilen, sign, sec) {
    204     if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
    205       degminsec = coord
    206       intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
    207       minsec = degminsec - intdeg * 10000
    208       intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
    209       sec = minsec - intmin * 100
    210       deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
    211     } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
    212       degmin = coord
    213       intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
    214       minute = degmin - intdeg * 100
    215       deg = (intdeg * 60 + minute) / 60
    216     } else
    217       deg = coord
    218     return deg * 0.017453292519943296
    219   }
    220   function convert_latitude(coord) {
    221     match(coord, /..*[-+]/)
    222     return convert_coord(substr(coord, 1, RLENGTH - 1))
    223   }
    224   function convert_longitude(coord) {
    225     match(coord, /..*[-+]/)
    226     return convert_coord(substr(coord, RLENGTH))
    227   }
    228   # Great-circle distance between points with given latitude and longitude.
    229   # Inputs and output are in radians.  This uses the great-circle special
    230   # case of the Vicenty formula for distances on ellipsoids.
    231   function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
    232     dlong = long2 - long1
    233     x = cos (lat2) * sin (dlong)
    234     y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
    235     num = sqrt (x * x + y * y)
    236     denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
    237     return atan2(num, denom)
    238   }
    239   # Parallel distance between points with given latitude and longitude.
    240   # This is the product of the longitude difference and the cosine
    241   # of the latitude of the point that is further from the equator.
    242   # I.e., it considers longitudes to be further apart if they are
    243   # nearer the equator.
    244   function pardist(lat1, long1, lat2, long2) {
    245     return abs (long1 - long2) * min (cos (lat1), cos (lat2))
    246   }
    247   # The distance function is the sum of the great-circle distance and
    248   # the parallel distance.  It could be weighted.
    249   function dist(lat1, long1, lat2, long2) {
    250     return gcdist (lat1, long1, lat2, long2) + pardist (lat1, long1, lat2, long2)
    251   }
    252   BEGIN {
    253     coord_lat = convert_latitude(coord)
    254     coord_long = convert_longitude(coord)
    255   }
    256   /^[^#]/ {
    257     here_lat = convert_latitude($2)
    258     here_long = convert_longitude($2)
    259     line = $1 "\t" $2 "\t" $3
    260     sep = "\t"
    261     ncc = split($1, cc, /,/)
    262     for (i = 1; i <= ncc; i++) {
    263       line = line sep country[cc[i]]
    264       sep = ", "
    265     }
    266     if (NF == 4)
    267       line = line " - " $4
    268     printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
    269   }
    270 '
    271 
    272 # Begin the main loop.  We come back here if the user wants to retry.
    273 while
    274 
    275 	echo >&2 'Please identify a location' \
    276 		'so that time zone rules can be set correctly.'
    277 
    278 	continent=
    279 	country=
    280 	region=
    281 
    282 	case $coord in
    283 	?*)
    284 		continent=coord;;
    285 	'')
    286 
    287 	# Ask the user for continent or ocean.
    288 
    289 	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
    290 
    291         quoted_continents=`
    292 	  $AWK '
    293 	    BEGIN { FS = "\t" }
    294 	    /^[^#]/ {
    295               entry = substr($3, 1, index($3, "/") - 1)
    296               if (entry == "America")
    297 		entry = entry "s"
    298               if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
    299 		entry = entry " Ocean"
    300               printf "'\''%s'\''\n", entry
    301             }
    302           ' <"$TZ_ZONE_TABLE" |
    303 	  sort -u |
    304 	  tr '\n' ' '
    305 	  echo ''
    306 	`
    307 
    308 	eval '
    309 	    doselect '"$quoted_continents"' \
    310 		"coord - I want to use geographical coordinates." \
    311 		"TZ - I want to specify the time zone using the Posix TZ format."
    312 	    continent=$select_result
    313 	    case $continent in
    314 	    Americas) continent=America;;
    315 	    *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
    316 	    esac
    317 	'
    318 	esac
    319 
    320 	case $continent in
    321 	TZ)
    322 		# Ask the user for a Posix TZ string.  Check that it conforms.
    323 		while
    324 			echo >&2 'Please enter the desired value' \
    325 				'of the TZ environment variable.'
    326 			echo >&2 'For example, GST-10 is a zone named GST' \
    327 				'that is 10 hours ahead (east) of UTC.'
    328 			read TZ
    329 			$AWK -v TZ="$TZ" 'BEGIN {
    330 				tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
    331 				time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
    332 				offset = "[-+]?" time
    333 				date = "(J?[0-9]+|M[0-9]+\.[0-9]+\.[0-9]+)"
    334 				datetime = "," date "(/" time ")?"
    335 				tzpattern = "^(:.*|" tzname offset "(" tzname \
    336 				  "(" offset ")?(" datetime datetime ")?)?)$"
    337 				if (TZ ~ tzpattern) exit 1
    338 				exit 0
    339 			}'
    340 		do
    341 			echo >&2 "'$TZ' is not a conforming" \
    342 				'Posix time zone string.'
    343 		done
    344 		TZ_for_date=$TZ;;
    345 	*)
    346 		case $continent in
    347 		coord)
    348 		    case $coord in
    349 		    '')
    350 			echo >&2 'Please enter coordinates' \
    351 				'in ISO 6709 notation.'
    352 			echo >&2 'For example, +4042-07403 stands for'
    353 			echo >&2 '40 degrees 42 minutes north,' \
    354 				'74 degrees 3 minutes west.'
    355 			read coord;;
    356 		    esac
    357 		    distance_table=`$AWK \
    358 			    -v coord="$coord" \
    359 			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    360 			    "$output_distances" <"$TZ_ZONE_TABLE" |
    361 		      sort -n |
    362 		      sed "${location_limit}q"
    363 		    `
    364 		    regions=`echo "$distance_table" | $AWK '
    365 		      BEGIN { FS = "\t" }
    366 		      { print $NF }
    367 		    '`
    368 		    echo >&2 'Please select one of the following' \
    369 			    'time zone regions,'
    370 		    echo >&2 'listed roughly in increasing order' \
    371 			    "of distance from $coord".
    372 		    doselect $regions
    373 		    region=$select_result
    374 		    TZ=`echo "$distance_table" | $AWK -v region="$region" '
    375 		      BEGIN { FS="\t" }
    376 		      $NF == region { print $4 }
    377 		    '`
    378 		    ;;
    379 		*)
    380 		# Get list of names of countries in the continent or ocean.
    381 		countries=`$AWK \
    382 			-v continent="$continent" \
    383 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    384 		'
    385 			BEGIN { FS = "\t" }
    386 			/^#/ { next }
    387 			$3 ~ ("^" continent "/") {
    388 			    ncc = split($1, cc, /,/)
    389 			    for (i = 1; i <= ncc; i++)
    390 				if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
    391 			}
    392 			END {
    393 				while (getline <TZ_COUNTRY_TABLE) {
    394 					if ($0 !~ /^#/) cc_name[$1] = $2
    395 				}
    396 				for (i = 1; i <= ccs; i++) {
    397 					country = cc_list[i]
    398 					if (cc_name[country]) {
    399 					  country = cc_name[country]
    400 					}
    401 					print country
    402 				}
    403 			}
    404 		' <"$TZ_ZONE_TABLE" | sort -f`
    405 
    406 
    407 		# If there's more than one country, ask the user which one.
    408 		case $countries in
    409 		*"$newline"*)
    410 			echo >&2 'Please select a country' \
    411 				'whose clocks agree with yours.'
    412 			doselect $countries
    413 			country=$select_result;;
    414 		*)
    415 			country=$countries
    416 		esac
    417 
    418 
    419 		# Get list of names of time zone rule regions in the country.
    420 		regions=`$AWK \
    421 			-v country="$country" \
    422 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    423 		'
    424 			BEGIN {
    425 				FS = "\t"
    426 				cc = country
    427 				while (getline <TZ_COUNTRY_TABLE) {
    428 					if ($0 !~ /^#/  &&  country == $2) {
    429 						cc = $1
    430 						break
    431 					}
    432 				}
    433 			}
    434 			$1 ~ cc { print $4 }
    435 		' <"$TZ_ZONE_TABLE"`
    436 
    437 
    438 		# If there's more than one region, ask the user which one.
    439 		case $regions in
    440 		*"$newline"*)
    441 			echo >&2 'Please select one of the following' \
    442 				'time zone regions.'
    443 			doselect $regions
    444 			region=$select_result;;
    445 		*)
    446 			region=$regions
    447 		esac
    448 
    449 		# Determine TZ from country and region.
    450 		TZ=`$AWK \
    451 			-v country="$country" \
    452 			-v region="$region" \
    453 			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
    454 		'
    455 			BEGIN {
    456 				FS = "\t"
    457 				cc = country
    458 				while (getline <TZ_COUNTRY_TABLE) {
    459 					if ($0 !~ /^#/  &&  country == $2) {
    460 						cc = $1
    461 						break
    462 					}
    463 				}
    464 			}
    465 			$1 ~ cc && $4 == region { print $3 }
    466 		' <"$TZ_ZONE_TABLE"`
    467 		esac
    468 
    469 		# Make sure the corresponding zoneinfo file exists.
    470 		TZ_for_date=$TZDIR/$TZ
    471 		<"$TZ_for_date" || {
    472 			echo >&2 "$0: time zone files are not set up correctly"
    473 			exit 1
    474 		}
    475 	esac
    476 
    477 
    478 	# Use the proposed TZ to output the current date relative to UTC.
    479 	# Loop until they agree in seconds.
    480 	# Give up after 8 unsuccessful tries.
    481 
    482 	extra_info=
    483 	for i in 1 2 3 4 5 6 7 8
    484 	do
    485 		TZdate=`LANG=C TZ="$TZ_for_date" date`
    486 		UTdate=`LANG=C TZ=UTC0 date`
    487 		TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
    488 		UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
    489 		case $TZsec in
    490 		$UTsec)
    491 			extra_info="
    492 Local time is now:	$TZdate.
    493 Universal Time is now:	$UTdate."
    494 			break
    495 		esac
    496 	done
    497 
    498 
    499 	# Output TZ info and ask the user to confirm.
    500 
    501 	echo >&2 ""
    502 	echo >&2 "The following information has been given:"
    503 	echo >&2 ""
    504 	case $country%$region%$coord in
    505 	?*%?*%)	echo >&2 "	$country$newline	$region";;
    506 	?*%%)	echo >&2 "	$country";;
    507 	%?*%?*) echo >&2 "	coord $coord$newline	$region";;
    508 	%%?*)	echo >&2 "	coord $coord";;
    509 	+)	echo >&2 "	TZ='$TZ'"
    510 	esac
    511 	echo >&2 ""
    512 	echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
    513 	echo >&2 "Is the above information OK?"
    514 
    515 	doselect Yes No
    516 	ok=$select_result
    517 	case $ok in
    518 	Yes) break
    519 	esac
    520 do coord=
    521 done
    522 
    523 case $SHELL in
    524 *csh) file=.login line="setenv TZ '$TZ'";;
    525 *) file=.profile line="TZ='$TZ'; export TZ"
    526 esac
    527 
    528 echo >&2 "
    529 You can make this change permanent for yourself by appending the line
    530 	$line
    531 to the file '$file' in your home directory; then log out and log in again.
    532 
    533 Here is that TZ value again, this time on standard output so that you
    534 can use the $0 command in shell scripts:"
    535 
    536 echo "$TZ"
    537