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