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