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