xref: /netbsd-src/lib/libc/time/tzselect.ksh (revision 6a493d6bc668897c91594964a732d38505b70cbb)
1#! /bin/bash
2#
3#	$NetBSD: tzselect.ksh,v 1.10 2013/12/26 18:34:28 christos Exp $
4#
5PKGVERSION='(tzcode) '
6TZVERSION=see_Makefile
7REPORT_BUGS_TO=tz@iana.org
8
9# Ask the user about the time zone, and output the resulting TZ value to stdout.
10# Interact with the user via stderr and stdin.
11
12# Contributed by Paul Eggert.
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 <http://www.gnu.org/software/bash/bash.html>
23#	Korn Shell <http://www.kornshell.com/>
24#	Public Domain Korn Shell <http://www.cs.mun.ca/~michael/pdksh/>
25#
26# For portability to Solaris 9 /bin/sh this script avoids some POSIX
27# features and common extensions, such as $(...) (which works sometimes
28# but not others), $((...)), and $10.
29#
30# This script also uses several features of modern awk programs.
31# If your host lacks awk, or has an old awk that does not conform to Posix,
32# you can use either of the following free programs instead:
33#
34#	Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
35#	mawk <http://invisible-island.net/mawk/>
36
37
38# Specify default values for environment variables if they are unset.
39: ${AWK=awk}
40: ${TZDIR=`pwd`}
41
42# Check for awk Posix compliance.
43($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
44[ $? = 123 ] || {
45	echo >&2 "$0: Sorry, your \`$AWK' program is not Posix compatible."
46	exit 1
47}
48
49coord=
50location_limit=10
51
52usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
53Select a time zone interactively.
54
55Options:
56
57  -c COORD
58    Instead of asking for continent and then country and then city,
59    ask for selection from time zones whose largest cities
60    are closest to the location with geographical coordinates COORD.
61    COORD should use ISO 6709 notation, for example, '-c +4852+00220'
62    for Paris (in degrees and minutes, North and East), or
63    '-c -35-058' for Buenos Aires (in degrees, South and West).
64
65  -n LIMIT
66    Display at most LIMIT locations when -c is used (default $location_limit).
67
68  --version
69    Output version information.
70
71  --help
72    Output this help.
73
74Report bugs to $REPORT_BUGS_TO."
75
76# Ask the user to select from the function's arguments,
77# and assign the selected argument to the variable 'select_result'.
78# Exit on EOF or I/O error.  Use the shell's 'select' builtin if available,
79# falling back on a less-nice but portable substitute otherwise.
80if
81  case $BASH_VERSION in
82  ?*) : ;;
83  '')
84    # '; exit' should be redundant, but Dash doesn't properly fail without it.
85    (eval 'set --; select x; do break; done; exit') 2>/dev/null
86  esac
87then
88  # Do this inside 'eval', as otherwise the shell might exit when parsing it
89  # even though it is never executed.
90  eval '
91    doselect() {
92      select select_result
93      do
94	case $select_result in
95	"") echo >&2 "Please enter a number in range." ;;
96	?*) break
97	esac
98      done || exit
99    }
100
101    # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
102    case $BASH_VERSION in
103    [01].*)
104      case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
105      ?*) PS3=
106      esac
107    esac
108  '
109else
110  doselect() {
111    # Field width of the prompt numbers.
112    select_width=`expr $# : '.*'`
113
114    select_i=
115
116    while :
117    do
118      case $select_i in
119      '')
120	select_i=0
121	for select_word
122	do
123	  select_i=`expr $select_i + 1`
124	  printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
125	done ;;
126      *[!0-9]*)
127	echo >&2 'Please enter a number in range.' ;;
128      *)
129	if test 1 -le $select_i && test $select_i -le $#; then
130	  shift `expr $select_i - 1`
131	  select_result=$1
132	  break
133	fi
134	echo >&2 'Please enter a number in range.'
135      esac
136
137      # Prompt and read input.
138      printf >&2 %s "${PS3-#? }"
139      read select_i || exit
140    done
141  }
142fi
143
144while getopts c:n:-: opt
145do
146    case $opt$OPTARG in
147    c*)
148	coord=$OPTARG ;;
149    n*)
150	location_limit=$OPTARG ;;
151    -help)
152	exec echo "$usage" ;;
153    -version)
154	exec echo "tzselect $PKGVERSION$TZVERSION" ;;
155    -*)
156	echo >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
157    *)
158	echo >&2 "$0: try '$0 --help'"; exit 1 ;;
159    esac
160done
161
162shift `expr $OPTIND - 1`
163case $# in
1640) ;;
165*) echo >&2 "$0: $1: unknown argument"; exit 1 ;;
166esac
167
168# Make sure the tables are readable.
169TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
170TZ_ZONE_TABLE=$TZDIR/zone.tab
171for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
172do
173	<$f || {
174		echo >&2 "$0: time zone files are not set up correctly"
175		exit 1
176	}
177done
178
179newline='
180'
181IFS=$newline
182
183
184# Awk script to read a time zone table and output the same table,
185# with each column preceded by its distance from 'here'.
186output_distances='
187  BEGIN {
188    FS = "\t"
189    while (getline <TZ_COUNTRY_TABLE)
190      if ($0 ~ /^[^#]/)
191        country[$1] = $2
192    country["US"] = "US" # Otherwise the strings get too long.
193  }
194  function convert_coord(coord, deg, min, ilen, sign, sec) {
195    if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
196      degminsec = coord
197      intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
198      minsec = degminsec - intdeg * 10000
199      intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
200      sec = minsec - intmin * 100
201      deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
202    } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
203      degmin = coord
204      intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
205      min = degmin - intdeg * 100
206      deg = (intdeg * 60 + min) / 60
207    } else
208      deg = coord
209    return deg * 0.017453292519943296
210  }
211  function convert_latitude(coord) {
212    match(coord, /..*[-+]/)
213    return convert_coord(substr(coord, 1, RLENGTH - 1))
214  }
215  function convert_longitude(coord) {
216    match(coord, /..*[-+]/)
217    return convert_coord(substr(coord, RLENGTH))
218  }
219  # Great-circle distance between points with given latitude and longitude.
220  # Inputs and output are in radians.  This uses the great-circle special
221  # case of the Vicenty formula for distances on ellipsoids.
222  function dist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
223    dlong = long2 - long1
224    x = cos (lat2) * sin (dlong)
225    y = cos (lat1) * sin (lat2) - sin (lat1) * cos (lat2) * cos (dlong)
226    num = sqrt (x * x + y * y)
227    denom = sin (lat1) * sin (lat2) + cos (lat1) * cos (lat2) * cos (dlong)
228    return atan2(num, denom)
229  }
230  BEGIN {
231    coord_lat = convert_latitude(coord)
232    coord_long = convert_longitude(coord)
233  }
234  /^[^#]/ {
235    here_lat = convert_latitude($2)
236    here_long = convert_longitude($2)
237    line = $1 "\t" $2 "\t" $3 "\t" country[$1]
238    if (NF == 4)
239      line = line " - " $4
240    printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
241  }
242'
243
244# Begin the main loop.  We come back here if the user wants to retry.
245while
246
247	echo >&2 'Please identify a location' \
248		'so that time zone rules can be set correctly.'
249
250	continent=
251	country=
252	region=
253
254	case $coord in
255	?*)
256		continent=coord;;
257	'')
258
259	# Ask the user for continent or ocean.
260
261	echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
262
263        quoted_continents=`
264	  $AWK '
265	    BEGIN { FS = "\t" }
266	    /^[^#]/ {
267              entry = substr($3, 1, index($3, "/") - 1)
268              if (entry == "America")
269		entry = entry "s"
270              if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
271		entry = entry " Ocean"
272              printf "'\''%s'\''\n", entry
273            }
274          ' $TZ_ZONE_TABLE |
275	  sort -u |
276	  tr '\n' ' '
277	  echo ''
278	`
279
280	eval '
281	    doselect '"$quoted_continents"' \
282		"coord - I want to use geographical coordinates." \
283		"TZ - I want to specify the time zone using the Posix TZ format."
284	    continent=$select_result
285	    case $continent in
286	    Americas) continent=America;;
287	    *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
288	    esac
289	'
290	esac
291
292	case $continent in
293	TZ)
294		# Ask the user for a Posix TZ string.  Check that it conforms.
295		while
296			echo >&2 'Please enter the desired value' \
297				'of the TZ environment variable.'
298			echo >&2 'For example, GST-10 is a zone named GST' \
299				'that is 10 hours ahead (east) of UTC.'
300			read TZ
301			$AWK -v TZ="$TZ" 'BEGIN {
302				tzname = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
303				time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
304				offset = "[-+]?" time
305				date = "(J?[0-9]+|M[0-9]+\.[0-9]+\.[0-9]+)"
306				datetime = "," date "(/" time ")?"
307				tzpattern = "^(:.*|" tzname offset "(" tzname \
308				  "(" offset ")?(" datetime datetime ")?)?)$"
309				if (TZ ~ tzpattern) exit 1
310				exit 0
311			}'
312		do
313			echo >&2 "\`$TZ' is not a conforming" \
314				'Posix time zone string.'
315		done
316		TZ_for_date=$TZ;;
317	*)
318		case $continent in
319		coord)
320		    case $coord in
321		    '')
322			echo >&2 'Please enter coordinates' \
323				'in ISO 6709 notation.'
324			echo >&2 'For example, +4042-07403 stands for'
325			echo >&2 '40 degrees 42 minutes north,' \
326				'74 degrees 3 minutes west.'
327			read coord;;
328		    esac
329		    distance_table=`$AWK \
330			    -v coord="$coord" \
331			    -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
332			    "$output_distances" <$TZ_ZONE_TABLE |
333		      sort -n |
334		      sed "${location_limit}q"
335		    `
336		    regions=`echo "$distance_table" | $AWK '
337		      BEGIN { FS = "\t" }
338		      { print $NF }
339		    '`
340		    echo >&2 'Please select one of the following' \
341			    'time zone regions,'
342		    echo >&2 'listed roughly in increasing order' \
343			    "of distance from $coord".
344		    doselect $regions
345		    region=$select_result
346		    TZ=`echo "$distance_table" | $AWK -v region="$region" '
347		      BEGIN { FS="\t" }
348		      $NF == region { print $4 }
349		    '`
350		    ;;
351		*)
352		# Get list of names of countries in the continent or ocean.
353		countries=`$AWK \
354			-v continent="$continent" \
355			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
356		'
357			BEGIN { FS = "\t" }
358			/^#/ { next }
359			$3 ~ ("^" continent "/") {
360				if (!cc_seen[$1]++) cc_list[++ccs] = $1
361			}
362			END {
363				while (getline <TZ_COUNTRY_TABLE) {
364					if ($0 !~ /^#/) cc_name[$1] = $2
365				}
366				for (i = 1; i <= ccs; i++) {
367					country = cc_list[i]
368					if (cc_name[country]) {
369					  country = cc_name[country]
370					}
371					print country
372				}
373			}
374		' <$TZ_ZONE_TABLE | sort -f`
375
376
377		# If there's more than one country, ask the user which one.
378		case $countries in
379		*"$newline"*)
380			echo >&2 'Please select a country' \
381				'whose clocks agree with yours.'
382			doselect $countries
383			country=$select_result;;
384		*)
385			country=$countries
386		esac
387
388
389		# Get list of names of time zone rule regions in the country.
390		regions=`$AWK \
391			-v country="$country" \
392			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
393		'
394			BEGIN {
395				FS = "\t"
396				cc = country
397				while (getline <TZ_COUNTRY_TABLE) {
398					if ($0 !~ /^#/  &&  country == $2) {
399						cc = $1
400						break
401					}
402				}
403			}
404			$1 == cc { print $4 }
405		' <$TZ_ZONE_TABLE`
406
407
408		# If there's more than one region, ask the user which one.
409		case $regions in
410		*"$newline"*)
411			echo >&2 'Please select one of the following' \
412				'time zone regions.'
413			doselect $regions
414			region=$select_result;;
415		*)
416			region=$regions
417		esac
418
419		# Determine TZ from country and region.
420		TZ=`$AWK \
421			-v country="$country" \
422			-v region="$region" \
423			-v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
424		'
425			BEGIN {
426				FS = "\t"
427				cc = country
428				while (getline <TZ_COUNTRY_TABLE) {
429					if ($0 !~ /^#/  &&  country == $2) {
430						cc = $1
431						break
432					}
433				}
434			}
435			$1 == cc && $4 == region { print $3 }
436		' <$TZ_ZONE_TABLE`
437		esac
438
439		# Make sure the corresponding zoneinfo file exists.
440		TZ_for_date=$TZDIR/$TZ
441		<$TZ_for_date || {
442			echo >&2 "$0: time zone files are not set up correctly"
443			exit 1
444		}
445	esac
446
447
448	# Use the proposed TZ to output the current date relative to UTC.
449	# Loop until they agree in seconds.
450	# Give up after 8 unsuccessful tries.
451
452	extra_info=
453	for i in 1 2 3 4 5 6 7 8
454	do
455		TZdate=`LANG=C TZ="$TZ_for_date" date`
456		UTdate=`LANG=C TZ=UTC0 date`
457		TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
458		UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
459		case $TZsec in
460		$UTsec)
461			extra_info="
462Local time is now:	$TZdate.
463Universal Time is now:	$UTdate."
464			break
465		esac
466	done
467
468
469	# Output TZ info and ask the user to confirm.
470
471	echo >&2 ""
472	echo >&2 "The following information has been given:"
473	echo >&2 ""
474	case $country%$region%$coord in
475	?*%?*%)	echo >&2 "	$country$newline	$region";;
476	?*%%)	echo >&2 "	$country";;
477	%?*%?*) echo >&2 "	coord $coord$newline	$region";;
478	%%?*)	echo >&2 "	coord $coord";;
479	+)	echo >&2 "	TZ='$TZ'"
480	esac
481	echo >&2 ""
482	echo >&2 "Therefore TZ='$TZ' will be used.$extra_info"
483	echo >&2 "Is the above information OK?"
484
485	doselect Yes No
486	ok=$select_result
487	case $ok in
488	Yes) break
489	esac
490do coord=
491done
492
493case $SHELL in
494*csh) file=.login line="setenv TZ '$TZ'";;
495*) file=.profile line="TZ='$TZ'; export TZ"
496esac
497
498echo >&2 "
499You can make this change permanent for yourself by appending the line
500	$line
501to the file '$file' in your home directory; then log out and log in again.
502
503Here is that TZ value again, this time on standard output so that you
504can use the $0 command in shell scripts:"
505
506echo "$TZ"
507