1#! /bin/bash 2# 3# $NetBSD: tzselect.ksh,v 1.10 2013/12/26 18:34:28 christos Exp $ 4# 5PKGVERSION='(tzcode) ' 6TZVERSION=see_Makefile 7REPORT_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 49coord= 50location_limit=10 51 52usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT] 53Select a time zone interactively. 54 55Options: 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 74Report 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. 80if 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 87then 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 ' 109else 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 } 142fi 143 144while getopts c:n:-: opt 145do 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 160done 161 162shift `expr $OPTIND - 1` 163case $# in 1640) ;; 165*) echo >&2 "$0: $1: unknown argument"; exit 1 ;; 166esac 167 168# Make sure the tables are readable. 169TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab 170TZ_ZONE_TABLE=$TZDIR/zone.tab 171for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE 172do 173 <$f || { 174 echo >&2 "$0: time zone files are not set up correctly" 175 exit 1 176 } 177done 178 179newline=' 180' 181IFS=$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'. 186output_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. 245while 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=" 462Local time is now: $TZdate. 463Universal 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 490do coord= 491done 492 493case $SHELL in 494*csh) file=.login line="setenv TZ '$TZ'";; 495*) file=.profile line="TZ='$TZ'; export TZ" 496esac 497 498echo >&2 " 499You can make this change permanent for yourself by appending the line 500 $line 501to the file '$file' in your home directory; then log out and log in again. 502 503Here is that TZ value again, this time on standard output so that you 504can use the $0 command in shell scripts:" 505 506echo "$TZ" 507