Home | History | Annotate | Line # | Download | only in time
      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 
      5 PKGVERSION='(tzcode) '
      6 TZVERSION=see_Makefile
      7 REPORT_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.
     38 set -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 '-'.
     46 say() {
     47   printf '%s\n' "$1"
     48 }
     49 
     50 coord=
     51 location_limit=10
     52 zonetabtype=zone1970
     53 
     54 usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
     55 Select a timezone interactively.
     56 
     57 Options:
     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 
     76 Report 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.
     82 if
     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
     89 then
     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   '
    103 else
    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   }
    136 fi
    137 
    138 while getopts c:n:t:-: opt
    139 do
    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
    156 done
    157 
    158 shift $(($OPTIND - 1))
    159 case $# in
    160 0) ;;
    161 *) say >&2 "$0: $1: unknown argument"; exit 1
    162 esac
    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 $'...'.
    169 CUNEIFORM_SIGN_URU_TIMES_KI=$'\360\222\215\205'
    170 if test ${#CUNEIFORM_SIGN_URU_TIMES_KI} = 1
    171 then translit=false
    172 else translit=true
    173 fi
    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'.
    180 read_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 }
    190 read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab"
    191 read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab"
    192 TZ_ZONENOW_TABLE=
    193 
    194 newline='
    195 '
    196 IFS=$newline
    197 
    198 # Awk script to output a country list.
    199 output_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.
    256 output_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.
    371 while
    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="
    752 Selected time is now:	$TZdate.
    753 Universal 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
    784 do coord=
    785 done
    786 
    787 case $SHELL in
    788 *csh) file=.login line="setenv TZ '$tz'";;
    789 *)    file=.profile line="export TZ='$tz'"
    790 esac
    791 
    792 test -t 1 && say >&2 "
    793 You can make this change permanent for yourself by appending the line
    794 	$line
    795 to the file '$file' in your home directory; then log out and log in again.
    796 
    797 Here is that TZ value again, this time on standard output so that you
    798 can use the $0 command in shell scripts:"
    799 
    800 say "$tz"
    801