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