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