xref: /freebsd/contrib/tzcode/tzselect.ksh (revision 7ca260df8cea7dea41e7a16362a3c5c45e86249c)
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