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