1#!/bin/bash 2# Ask the user about the time zone, and output the resulting TZ value to stdout. 3# Interact with the user via stderr and stdin. 4 5PKGVERSION='(tzcode) ' 6TZVERSION=see_Makefile 7REPORT_BUGS_TO=tz@iana.org 8 9# Contributed by Paul Eggert. This file is in the public domain. 10 11# Porting notes: 12# 13# This script requires a POSIX-like shell and prefers the extension of a 14# 'select' statement. The 'select' statement was introduced in the 15# Korn shell and is available in Bash and other shell implementations. 16# If your host lacks both Bash and the Korn shell, you can get their 17# source from one of these locations: 18# 19# Bash <https://www.gnu.org/software/bash/> 20# Korn Shell <http://www.kornshell.com/> 21# MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm> 22# 23# This script also uses several features of POSIX awk. 24# If your host lacks awk, or has an old awk that does not conform to POSIX, 25# you can use any of the following free programs instead: 26# 27# Gawk (GNU awk) <https://www.gnu.org/software/gawk/> 28# mawk <https://invisible-island.net/mawk/> 29# nawk <https://github.com/onetrueawk/awk> 30# 31# Because 'awk "VAR=VALUE" ...' and 'awk -v "VAR=VALUE" ...' are not portable 32# if VALUE contains \, ", or newline, awk scripts in this file use: 33# awk 'BEGIN { VAR = substr(ARGV[1], 2); ARGV[1] = "" } ...' ="VALUE" 34# The substr avoids problems when VALUE is of the form X=Y and would be 35# misinterpreted as an assignment. 36 37# This script does not want path expansion. 38set -f 39 40# Specify default values for environment variables if they are unset. 41: ${AWK=awk} 42: ${TZDIR=$PWD} 43 44# Output one argument as-is to standard output, with trailing newline. 45# Safer than 'echo', which can mishandle '\' or leading '-'. 46say() { 47 printf '%s\n' "$1" 48} 49 50coord= 51location_limit=10 52zonetabtype=zone1970 53 54usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT] 55Select a timezone interactively. 56 57Options: 58 59 -c COORD 60 Instead of asking for continent and then country and then city, 61 ask for selection from time zones whose largest cities 62 are closest to the location with geographical coordinates COORD. 63 COORD should use ISO 6709 notation, for example, '-c +4852+00220' 64 for Paris (in degrees and minutes, North and East), or 65 '-c -35-058' for Buenos Aires (in degrees, South and West). 66 67 -n LIMIT 68 Display at most LIMIT locations when -c is used (default $location_limit). 69 70 --version 71 Output version information. 72 73 --help 74 Output this help. 75 76Report bugs to $REPORT_BUGS_TO." 77 78# Ask the user to select from the function's arguments, 79# and assign the selected argument to the variable 'select_result'. 80# Exit on EOF or I/O error. Use the shell's nicer 'select' builtin if 81# available, falling back on a portable substitute otherwise. 82if 83 case $BASH_VERSION in 84 ?*) :;; 85 '') 86 # '; exit' should be redundant, but Dash doesn't properly fail without it. 87 (eval 'set --; select x; do break; done; exit') <>/dev/null 2>&0 88 esac 89then 90 # Do this inside 'eval', as otherwise the shell might exit when parsing it 91 # even though it is never executed. 92 eval ' 93 doselect() { 94 select select_result 95 do 96 case $select_result in 97 "") echo >&2 "Please enter a number in range.";; 98 ?*) break 99 esac 100 done || exit 101 } 102 ' 103else 104 doselect() { 105 # Field width of the prompt numbers. 106 select_width=${##} 107 108 select_i= 109 110 while : 111 do 112 case $select_i in 113 '') 114 select_i=0 115 for select_word 116 do 117 select_i=$(($select_i + 1)) 118 printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word" 119 done;; 120 *[!0-9]*) 121 echo >&2 'Please enter a number in range.';; 122 *) 123 if test 1 -le $select_i && test $select_i -le $#; then 124 shift $(($select_i - 1)) 125 select_result=$1 126 break 127 fi 128 echo >&2 'Please enter a number in range.' 129 esac 130 131 # Prompt and read input. 132 printf >&2 %s "${PS3-#? }" 133 read select_i || exit 134 done 135 } 136fi 137 138while getopts c:n:t:-: opt 139do 140 case $opt$OPTARG in 141 c*) 142 coord=$OPTARG;; 143 n*) 144 location_limit=$OPTARG;; 145 t*) # Undocumented option, used for developer testing. 146 zonetabtype=$OPTARG;; 147 -help) 148 say "$usage" 149 exit;; 150 -version) 151 say "tzselect $PKGVERSION$TZVERSION" 152 exit;; 153 -*) 154 say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1;; 155 *) 156 say >&2 "$0: try '$0 --help'"; exit 1 157 esac 158done 159 160shift $(($OPTIND - 1)) 161case $# in 1620) ;; 163*) say >&2 "$0: $1: unknown argument"; exit 1 164esac 165 166# translit=: to try transliteration. 167# This is false if U+12345 CUNEIFORM SIGN URU TIMES KI has length 1 168# which means the shell and (presumably) awk do not need transliteration. 169# It is ':' if the byte string has some other length in characters, or 170# if this is a POSIX.1-2017 or earlier shell that does not support $'...'. 171CUNEIFORM_SIGN_URU_TIMES_KI=$'\360\222\215\205' 172if test ${#CUNEIFORM_SIGN_URU_TIMES_KI} = 1 173then translit=false 174else translit=: 175fi 176 177# Read into shell variable $1 the contents of file $2. 178# Convert to the current locale's encoding if possible, 179# as the shell aligns columns better that way. 180# If GNU iconv's //TRANSLIT does not work, fall back on POSIXish iconv; 181# if that does not work, fall back on 'cat'. 182read_file() { 183 { $translit && { 184 eval "$1=\$( (iconv -f UTF-8 -t //TRANSLIT) 2>/dev/null <\"\$2\")" || 185 eval "$1=\$( (iconv -f UTF-8) 2>/dev/null <\"\$2\")" 186 }; } || 187 eval "$1=\$(cat <\"\$2\")" || { 188 say >&2 "$0: time zone files are not set up correctly" 189 exit 1 190 } 191} 192read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab" 193read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab" 194TZ_ZONENOW_TABLE= 195 196newline=' 197' 198IFS=$newline 199 200# Awk script to output a country list. 201output_country_list=' 202 BEGIN { 203 continent_re = substr(ARGV[1], 2) 204 TZ_COUNTRY_TABLE = substr(ARGV[2], 2) 205 TZ_ZONE_TABLE = substr(ARGV[3], 2) 206 ARGV[1] = ARGV[2] = ARGV[3] = "" 207 FS = "\t" 208 nlines = split(TZ_ZONE_TABLE, line, /\n/) 209 for (iline = 1; iline <= nlines; iline++) { 210 $0 = line[iline] 211 commentary = $0 ~ /^#@/ 212 if (commentary) { 213 if ($0 !~ /^#@/) 214 continue 215 col1ccs = substr($1, 3) 216 conts = $2 217 } else { 218 col1ccs = $1 219 conts = $3 220 } 221 ncc = split(col1ccs, cc, /,/) 222 ncont = split(conts, cont, /,/) 223 for (i = 1; i <= ncc; i++) { 224 elsewhere = commentary 225 for (ci = 1; ci <= ncont; ci++) { 226 if (cont[ci] ~ continent_re) { 227 if (!cc_seen[cc[i]]++) 228 cc_list[++ccs] = cc[i] 229 elsewhere = 0 230 } 231 } 232 if (elsewhere) 233 for (i = 1; i <= ncc; i++) 234 cc_elsewhere[cc[i]] = 1 235 } 236 } 237 nlines = split(TZ_COUNTRY_TABLE, line, /\n/) 238 for (i = 1; i <= nlines; i++) { 239 $0 = line[i] 240 if ($0 !~ /^#/) 241 cc_name[$1] = $2 242 } 243 for (i = 1; i <= ccs; i++) { 244 country = cc_list[i] 245 if (cc_elsewhere[country]) 246 continue 247 if (cc_name[country]) 248 country = cc_name[country] 249 print country 250 } 251 } 252' 253 254# Awk script to process a time zone table and output the same table, 255# with each row preceded by its distance from 'here'. 256# If output_times is set, each row is instead preceded by its local time 257# and any apostrophes are escaped for the shell. 258output_distances_or_times=' 259 BEGIN { 260 coord = substr(ARGV[1], 2) 261 TZ_COUNTRY_TABLE = substr(ARGV[2], 2) 262 TZ_ZONE_TABLE = substr(ARGV[3], 2) 263 ARGV[1] = ARGV[2] = ARGV[3] = "" 264 FS = "\t" 265 if (!output_times) { 266 nlines = split(TZ_COUNTRY_TABLE, line, /\n/) 267 for (i = 1; i <= nlines; i++) { 268 $0 = line[i] 269 if ($0 ~ /^#/) 270 continue 271 country[$1] = $2 272 } 273 country["US"] = "US" # Otherwise the strings get too long. 274 } 275 } 276 function abs(x) { 277 return x < 0 ? -x : x; 278 } 279 function min(x, y) { 280 return x < y ? x : y; 281 } 282 function convert_coord(coord, deg, minute, ilen, sign, sec) { 283 if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) { 284 degminsec = coord 285 intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000) 286 minsec = degminsec - intdeg * 10000 287 intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100) 288 sec = minsec - intmin * 100 289 deg = (intdeg * 3600 + intmin * 60 + sec) / 3600 290 } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) { 291 degmin = coord 292 intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100) 293 minute = degmin - intdeg * 100 294 deg = (intdeg * 60 + minute) / 60 295 } else 296 deg = coord 297 return deg * 0.017453292519943296 298 } 299 function convert_latitude(coord) { 300 match(coord, /..*[-+]/) 301 return convert_coord(substr(coord, 1, RLENGTH - 1)) 302 } 303 function convert_longitude(coord) { 304 match(coord, /..*[-+]/) 305 return convert_coord(substr(coord, RLENGTH)) 306 } 307 # Great-circle distance between points with given latitude and longitude. 308 # Inputs and output are in radians. This uses the great-circle special 309 # case of the Vicenty formula for distances on ellipsoids. 310 function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) { 311 dlong = long2 - long1 312 x = cos(lat2) * sin(dlong) 313 y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong) 314 num = sqrt(x * x + y * y) 315 denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong) 316 return atan2(num, denom) 317 } 318 # Parallel distance between points with given latitude and longitude. 319 # This is the product of the longitude difference and the cosine 320 # of the latitude of the point that is further from the equator. 321 # I.e., it considers longitudes to be further apart if they are 322 # nearer the equator. 323 function pardist(lat1, long1, lat2, long2) { 324 return abs(long1 - long2) * min(cos(lat1), cos(lat2)) 325 } 326 # The distance function is the sum of the great-circle distance and 327 # the parallel distance. It could be weighted. 328 function dist(lat1, long1, lat2, long2) { 329 return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2) 330 } 331 BEGIN { 332 coord_lat = convert_latitude(coord) 333 coord_long = convert_longitude(coord) 334 nlines = split(TZ_ZONE_TABLE, line, /\n/) 335 for (h = 1; h <= nlines; h++) { 336 $0 = line[h] 337 if ($0 ~ /^#/) 338 continue 339 inline[inlines++] = $0 340 ncc = split($1, cc, /,/) 341 for (i = 1; i <= ncc; i++) 342 cc_used[cc[i]]++ 343 } 344 for (h = 0; h < inlines; h++) { 345 $0 = inline[h] 346 outline = $1 "\t" $2 "\t" $3 347 sep = "\t" 348 ncc = split($1, cc, /,/) 349 split("", item_seen) 350 item_seen[""] = 1 351 for (i = 1; i <= ncc; i++) { 352 item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4 353 if (item_seen[item]++) 354 continue 355 outline = outline sep item 356 sep = "; " 357 } 358 if (output_times) { 359 fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n" 360 gsub(/'\''/, "&\\\\&&", outline) 361 printf fmt, $3, h, outline 362 } else { 363 here_lat = convert_latitude($2) 364 here_long = convert_longitude($2) 365 printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), \ 366 outline 367 } 368 } 369 } 370' 371 372# Begin the main loop. We come back here if the user wants to retry. 373while 374 375 echo >&2 'Please identify a location' \ 376 'so that time zone rules can be set correctly.' 377 378 continent= 379 country= 380 country_result= 381 region= 382 time= 383 TZ_ZONE_TABLE=$TZ_ZONETABTYPE_TABLE 384 385 case $coord in 386 ?*) 387 continent=coord;; 388 '') 389 390 # Ask the user for continent or ocean. 391 392 echo >&2 \ 393 'Please select a continent, ocean, "coord", "TZ", "time", or "now".' 394 395 quoted_continents=$( 396 $AWK ' 397 function handle_entry(entry) { 398 entry = substr(entry, 1, index(entry, "/") - 1) 399 if (entry == "America") 400 entry = entry "s" 401 if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/) 402 entry = entry " Ocean" 403 printf "'\''%s'\''\n", entry 404 } 405 BEGIN { 406 TZ_ZONETABTYPE_TABLE = substr(ARGV[1], 2) 407 ARGV[1] = "" 408 FS = "\t" 409 nlines = split(TZ_ZONETABTYPE_TABLE, line, /\n/) 410 for (i = 1; i <= nlines; i++) { 411 $0 = line[i] 412 if ($0 ~ /^[^#]/) 413 handle_entry($3) 414 else if ($0 ~ /^#@/) { 415 ncont = split($2, cont, /,/) 416 for (ci = 1; ci <= ncont; ci++) 417 handle_entry(cont[ci]) 418 } 419 } 420 } 421 ' ="$TZ_ZONETABTYPE_TABLE" | 422 sort -u | 423 tr '\n' ' ' 424 echo '' 425 ) 426 427 eval ' 428 doselect '"$quoted_continents"' \ 429 "coord - I want to use geographical coordinates." \ 430 "TZ - I want to specify the timezone using a proleptic TZ string." \ 431 "time - I know local time already." \ 432 "now - Like \"time\", but configure only for timestamps from now on." 433 continent=$select_result 434 case $continent in 435 Americas) continent=America;; 436 *) 437 # Get the first word of $continent. Path expansion is disabled 438 # so this works even with "*", which should not happen. 439 IFS=" " 440 for continent in $continent ""; do break; done 441 IFS=$newline;; 442 esac 443 case $zonetabtype,$continent in 444 zonenow,*) ;; 445 *,now) 446 ${TZ_ZONENOW_TABLE:+:} read_file TZ_ZONENOW_TABLE "$TZDIR/zonenow.tab" 447 TZ_ZONE_TABLE=$TZ_ZONENOW_TABLE 448 esac 449 ' 450 esac 451 452 case $continent in 453 TZ) 454 # Ask the user for a proleptic TZ string. Check that it conforms. 455 check_POSIX_TZ_string=' 456 BEGIN { 457 tz = substr(ARGV[1], 2) 458 ARGV[1] = "" 459 tzname = ("(<[[:alnum:]+-][[:alnum:]+-][[:alnum:]+-]+>" \ 460 "|[[:alpha:]][[:alpha:]][[:alpha:]]+)") 461 sign = "[-+]?" 462 hhmm = "(:[0-5][0-9](:[0-5][0-9])?)?" 463 offset = sign "(2[0-4]|[0-1]?[0-9])" hhmm 464 time = sign "(16[0-7]|(1[0-5]|[0-9]?)[0-9])" hhmm 465 mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]" 466 jdate = ("((J[1-9]|[0-9]|J?[1-9][0-9]" \ 467 "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])") 468 datetime = ",(" mdate "|" jdate ")(/" time ")?" 469 tzpattern = ("^(:.*|" tzname offset "(" tzname \ 470 "(" offset ")?(" datetime datetime ")?)?)$") 471 exit tz ~ tzpattern 472 } 473 ' 474 475 while 476 echo >&2 'Please enter the desired value' \ 477 'of the TZ environment variable.' 478 echo >&2 'For example, AEST-10 is abbreviated' \ 479 'AEST and is 10 hours' 480 echo >&2 'ahead (east) of Greenwich,' \ 481 'with no daylight saving time.' 482 read tz 483 $AWK "$check_POSIX_TZ_string" ="$tz" 484 do 485 say >&2 "'$tz' is not a conforming POSIX proleptic TZ string." 486 done 487 TZ_for_date=$tz;; 488 *) 489 case $continent in 490 coord) 491 case $coord in 492 '') 493 echo >&2 'Please enter coordinates' \ 494 'in ISO 6709 notation.' 495 echo >&2 'For example, +4042-07403 stands for' 496 echo >&2 '40 degrees 42 minutes north,' \ 497 '74 degrees 3 minutes west.' 498 read coord 499 esac 500 distance_table=$( 501 $AWK \ 502 "$output_distances_or_times" \ 503 ="$coord" ="$TZ_COUNTRY_TABLE" ="$TZ_ZONE_TABLE" | 504 sort -n | 505 $AWK "{print} NR == $location_limit { exit }" 506 ) 507 regions=$( 508 $AWK ' 509 BEGIN { 510 distance_table = substr(ARGV[1], 2) 511 ARGV[1] = "" 512 nlines = split(distance_table, line, /\n/) 513 for (nr = 1; nr <= nlines; nr++) { 514 nf = split(line[nr], f, /\t/) 515 print f[nf] 516 } 517 } 518 ' ="$distance_table" 519 ) 520 echo >&2 'Please select one of the following timezones,' 521 say >&2 "listed roughly in increasing order of distance from $coord." 522 doselect $regions 523 region=$select_result 524 tz=$( 525 $AWK ' 526 BEGIN { 527 distance_table = substr(ARGV[1], 2) 528 region = substr(ARGV[2], 2) 529 ARGV[1] = ARGV[2] = "" 530 nlines = split(distance_table, line, /\n/) 531 for (nr = 1; nr <= nlines; nr++) { 532 nf = split(line[nr], f, /\t/) 533 if (f[nf] == region) 534 print f[4] 535 } 536 } 537 ' ="$distance_table" ="$region" 538 );; 539 *) 540 case $continent in 541 now|time) 542 minute_format='%a %b %d %H:%M' 543 old_minute=$(TZ=UTC0 date +"$minute_format") 544 for i in 1 2 3 545 do 546 time_table_command=$( 547 $AWK \ 548 -v output_times=1 \ 549 "$output_distances_or_times" \ 550 = = ="$TZ_ZONE_TABLE" 551 ) 552 time_table=$(eval "$time_table_command") 553 new_minute=$(TZ=UTC0 date +"$minute_format") 554 case $old_minute in 555 "$new_minute") break 556 esac 557 old_minute=$new_minute 558 done 559 echo >&2 "The system says Universal Time is $new_minute." 560 echo >&2 "Assuming that's correct, what is the local time?" 561 sorted_table=$(say "$time_table" | sort -k2n -k2,5 -k1n) || { 562 say >&2 "$0: cannot sort time table" 563 exit 1 564 } 565 eval doselect $( 566 $AWK ' 567 BEGIN { 568 sorted_table = substr(ARGV[1], 2) 569 ARGV[1] = "" 570 nlines = split(sorted_table, line, /\n/) 571 for (i = 1; i <= nlines; i++) { 572 $0 = line[i] 573 outline = $6 " " $7 " " $4 " " $5 574 if (outline == oldline) 575 continue 576 oldline = outline 577 gsub(/'\''/, "&\\\\&&", outline) 578 printf "'\''%s'\''\n", outline 579 } 580 } 581 ' ="$sorted_table" 582 ) 583 time=$select_result 584 continent_re='^' 585 zone_table=$( 586 $AWK ' 587 BEGIN { 588 time = substr(ARGV[1], 2) 589 time_table = substr(ARGV[2], 2) 590 ARGV[1] = ARGV[2] = "" 591 nlines = split(time_table, line, /\n/) 592 for (i = 1; i <= nlines; i++) { 593 $0 = line[i] 594 if ($6 " " $7 " " $4 " " $5 == time) { 595 sub(/[^\t]*\t/, "") 596 print 597 } 598 } 599 } 600 ' ="$time" ="$time_table" 601 ) 602 countries=$( 603 $AWK \ 604 "$output_country_list" \ 605 ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" | 606 sort -f 607 ) 608 ;; 609 *) 610 continent_re="^$continent/" 611 zone_table=$TZ_ZONE_TABLE 612 esac 613 614 # Get list of names of countries in the continent or ocean. 615 countries=$( 616 $AWK \ 617 "$output_country_list" \ 618 ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" | 619 sort -f 620 ) 621 # If all zone table entries have comments, and there are 622 # at most 22 entries, asked based on those comments. 623 # This fits the prompt onto old-fashioned 24-line screens. 624 regions=$( 625 $AWK ' 626 BEGIN { 627 TZ_ZONE_TABLE = substr(ARGV[1], 2) 628 ARGV[1] = "" 629 FS = "\t" 630 nlines = split(TZ_ZONE_TABLE, line, /\n/) 631 for (i = 1; i <= nlines; i++) { 632 $0 = line[i] 633 if ($0 ~ /^[^#]/ && !missing_comment) { 634 if ($4) 635 comment[++inlines] = $4 636 else 637 missing_comment = 1 638 } 639 } 640 if (!missing_comment && inlines <= 22) 641 for (i = 1; i <= inlines; i++) 642 print comment[i] 643 } 644 ' ="$zone_table" 645 ) 646 647 # If there's more than one country, ask the user which one. 648 case $countries in 649 *"$newline"*) 650 echo >&2 'Please select a country' \ 651 'whose clocks agree with yours.' 652 doselect $countries 653 country_result=$select_result 654 country=$select_result;; 655 *) 656 country=$countries 657 esac 658 659 660 # Get list of timezones in the country. 661 regions=$( 662 $AWK ' 663 BEGIN { 664 country = substr(ARGV[1], 2) 665 TZ_COUNTRY_TABLE = substr(ARGV[2], 2) 666 TZ_ZONE_TABLE = substr(ARGV[3], 2) 667 ARGV[1] = ARGV[2] = ARGV[3] = "" 668 FS = "\t" 669 cc = country 670 nlines = split(TZ_COUNTRY_TABLE, line, /\n/) 671 for (i = 1; i <= nlines; i++) { 672 $0 = line[i] 673 if ($0 !~ /^#/ && country == $2) { 674 cc = $1 675 break 676 } 677 } 678 nlines = split(TZ_ZONE_TABLE, line, /\n/) 679 for (i = 1; i <= nlines; i++) { 680 $0 = line[i] 681 if ($0 ~ /^#/) 682 continue 683 if ($1 ~ cc) 684 print $4 685 } 686 } 687 ' ="$country" ="$TZ_COUNTRY_TABLE" ="$zone_table" 688 ) 689 690 # If there's more than one region, ask the user which one. 691 case $regions in 692 *"$newline"*) 693 echo >&2 'Please select one of the following timezones.' 694 doselect $regions 695 region=$select_result 696 esac 697 698 # Determine tz from country and region. 699 tz=$( 700 $AWK ' 701 BEGIN { 702 country = substr(ARGV[1], 2) 703 region = substr(ARGV[2], 2) 704 TZ_COUNTRY_TABLE = substr(ARGV[3], 2) 705 TZ_ZONE_TABLE = substr(ARGV[4], 2) 706 ARGV[1] = ARGV[2] = ARGV[3] = ARGV[4] = "" 707 FS = "\t" 708 cc = country 709 nlines = split(TZ_COUNTRY_TABLE, line, /\n/) 710 for (i = 1; i <= nlines; i++) { 711 $0 = line[i] 712 if ($0 !~ /^#/ && country == $2) { 713 cc = $1 714 break 715 } 716 } 717 nlines = split(TZ_ZONE_TABLE, line, /\n/) 718 for (i = 1; i <= nlines; i++) { 719 $0 = line[i] 720 if ($0 ~ /^#/) 721 continue 722 if ($1 ~ cc && ($4 == region || !region)) 723 print $3 724 } 725 } 726 ' ="$country" ="$region" ="$TZ_COUNTRY_TABLE" ="$zone_table" 727 ) 728 esac 729 730 # Make sure the corresponding zoneinfo file exists. 731 TZ_for_date=$TZDIR/$tz 732 <"$TZ_for_date" || { 733 say >&2 "$0: time zone files are not set up correctly" 734 exit 1 735 } 736 esac 737 738 739 # Use the proposed TZ to output the current date relative to UTC. 740 # Loop until they agree in seconds. 741 # Give up after 8 unsuccessful tries. 742 743 extra_info= 744 for i in 1 2 3 4 5 6 7 8 745 do 746 TZdate=$(LANG=C TZ="$TZ_for_date" date) 747 UTdate=$(LANG=C TZ=UTC0 date) 748 TZsecsetc=${TZdate##*[0-5][0-9]:} 749 UTsecsetc=${UTdate##*[0-5][0-9]:} 750 if test "${TZsecsetc%%[!0-9]*}" = "${UTsecsetc%%[!0-9]*}" 751 then 752 extra_info=" 753Selected time is now: $TZdate. 754Universal Time is now: $UTdate." 755 break 756 fi 757 done 758 759 760 # Output TZ info and ask the user to confirm. 761 762 echo >&2 "" 763 echo >&2 "Based on the following information:" 764 echo >&2 "" 765 case $time%$country_result%$region%$coord in 766 ?*%?*%?*%) 767 say >&2 " $time$newline $country_result$newline $region";; 768 ?*%?*%%|?*%%?*%) say >&2 " $time$newline $country_result$region";; 769 ?*%%%) say >&2 " $time";; 770 %?*%?*%) say >&2 " $country_result$newline $region";; 771 %?*%%) say >&2 " $country_result";; 772 %%?*%?*) say >&2 " coord $coord$newline $region";; 773 %%%?*) say >&2 " coord $coord";; 774 *) say >&2 " TZ='$tz'" 775 esac 776 say >&2 "" 777 say >&2 "TZ='$tz' will be used.$extra_info" 778 say >&2 "Is the above information OK?" 779 780 doselect Yes No 781 ok=$select_result 782 case $ok in 783 Yes) break 784 esac 785do coord= 786done 787 788case $SHELL in 789*csh) file=.login line="setenv TZ '$tz'";; 790*) file=.profile line="export TZ='$tz'" 791esac 792 793test -t 1 && say >&2 " 794You can make this change permanent for yourself by appending the line 795 $line 796to the file '$file' in your home directory; then log out and log in again. 797 798Here is that TZ value again, this time on standard output so that you 799can use the $0 command in shell scripts:" 800 801say "$tz" 802