tzselect.ksh revision 1.24 1 1.24 christos #!/bin/bash
2 1.18 christos # Ask the user about the time zone, and output the resulting TZ value to stdout.
3 1.18 christos # Interact with the user via stderr and stdin.
4 1.24 christos
5 1.8 christos PKGVERSION='(tzcode) '
6 1.8 christos TZVERSION=see_Makefile
7 1.8 christos REPORT_BUGS_TO=tz@iana.org
8 1.5 kleink
9 1.15 christos # Contributed by Paul Eggert. This file is in the public domain.
10 1.1 jtc
11 1.1 jtc # Porting notes:
12 1.1 jtc #
13 1.24 christos # This script requires a POSIX-like shell and prefers the extension of a
14 1.8 christos # 'select' statement. The 'select' statement was introduced in the
15 1.8 christos # Korn shell and is available in Bash and other shell implementations.
16 1.8 christos # If your host lacks both Bash and the Korn shell, you can get their
17 1.8 christos # source from one of these locations:
18 1.1 jtc #
19 1.16 christos # Bash <https://www.gnu.org/software/bash/>
20 1.8 christos # Korn Shell <http://www.kornshell.com/>
21 1.19 christos # MirBSD Korn Shell <http://www.mirbsd.org/mksh.htm>
22 1.1 jtc #
23 1.24 christos # This script also uses several features of POSIX awk.
24 1.24 christos # If your host lacks awk, or has an old awk that does not conform to POSIX,
25 1.24 christos # you can use any of the following free programs instead:
26 1.1 jtc #
27 1.16 christos # Gawk (GNU awk) <https://www.gnu.org/software/gawk/>
28 1.16 christos # mawk <https://invisible-island.net/mawk/>
29 1.21 christos # nawk <https://github.com/onetrueawk/awk>
30 1.24 christos #
31 1.24 christos # Because 'awk "VAR=VALUE" ...' and 'awk -v "VAR=VALUE" ...' are not portable
32 1.24 christos # if VALUE contains \, ", or newline, awk scripts in this file use:
33 1.24 christos # awk 'BEGIN { VAR = substr(ARGV[1], 2); ARGV[1] = "" } ...' ="VALUE"
34 1.24 christos # The substr avoids problems when VALUE is of the form X=Y and would be
35 1.24 christos # misinterpreted as an assignment.
36 1.1 jtc
37 1.23 christos # This script does not want path expansion.
38 1.23 christos set -f
39 1.23 christos
40 1.1 jtc # Specify default values for environment variables if they are unset.
41 1.1 jtc : ${AWK=awk}
42 1.23 christos : ${TZDIR=$PWD}
43 1.1 jtc
44 1.22 christos # Output one argument as-is to standard output, with trailing newline.
45 1.14 christos # Safer than 'echo', which can mishandle '\' or leading '-'.
46 1.14 christos say() {
47 1.24 christos printf '%s\n' "$1"
48 1.1 jtc }
49 1.1 jtc
50 1.9 christos coord=
51 1.9 christos location_limit=10
52 1.11 christos zonetabtype=zone1970
53 1.9 christos
54 1.9 christos usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
55 1.18 christos Select a timezone interactively.
56 1.6 mlelstv
57 1.9 christos Options:
58 1.9 christos
59 1.9 christos -c COORD
60 1.9 christos Instead of asking for continent and then country and then city,
61 1.9 christos ask for selection from time zones whose largest cities
62 1.9 christos are closest to the location with geographical coordinates COORD.
63 1.9 christos COORD should use ISO 6709 notation, for example, '-c +4852+00220'
64 1.9 christos for Paris (in degrees and minutes, North and East), or
65 1.9 christos '-c -35-058' for Buenos Aires (in degrees, South and West).
66 1.9 christos
67 1.9 christos -n LIMIT
68 1.9 christos Display at most LIMIT locations when -c is used (default $location_limit).
69 1.9 christos
70 1.9 christos --version
71 1.9 christos Output version information.
72 1.9 christos
73 1.9 christos --help
74 1.9 christos Output this help.
75 1.9 christos
76 1.9 christos Report bugs to $REPORT_BUGS_TO."
77 1.9 christos
78 1.10 christos # Ask the user to select from the function's arguments,
79 1.10 christos # and assign the selected argument to the variable 'select_result'.
80 1.22 christos # Exit on EOF or I/O error. Use the shell's nicer 'select' builtin if
81 1.22 christos # available, falling back on a portable substitute otherwise.
82 1.10 christos if
83 1.10 christos case $BASH_VERSION in
84 1.24 christos ?*) :;;
85 1.10 christos '')
86 1.10 christos # '; exit' should be redundant, but Dash doesn't properly fail without it.
87 1.24 christos (eval 'set --; select x; do break; done; exit') <>/dev/null 2>&0
88 1.10 christos esac
89 1.10 christos then
90 1.10 christos # Do this inside 'eval', as otherwise the shell might exit when parsing it
91 1.10 christos # even though it is never executed.
92 1.10 christos eval '
93 1.10 christos doselect() {
94 1.10 christos select select_result
95 1.10 christos do
96 1.10 christos case $select_result in
97 1.24 christos "") echo >&2 "Please enter a number in range.";;
98 1.10 christos ?*) break
99 1.10 christos esac
100 1.10 christos done || exit
101 1.10 christos }
102 1.10 christos '
103 1.10 christos else
104 1.10 christos doselect() {
105 1.10 christos # Field width of the prompt numbers.
106 1.24 christos select_width=${##}
107 1.10 christos
108 1.10 christos select_i=
109 1.10 christos
110 1.10 christos while :
111 1.10 christos do
112 1.10 christos case $select_i in
113 1.10 christos '')
114 1.10 christos select_i=0
115 1.10 christos for select_word
116 1.10 christos do
117 1.24 christos select_i=$(($select_i + 1))
118 1.10 christos printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
119 1.24 christos done;;
120 1.10 christos *[!0-9]*)
121 1.24 christos echo >&2 'Please enter a number in range.';;
122 1.10 christos *)
123 1.10 christos if test 1 -le $select_i && test $select_i -le $#; then
124 1.24 christos shift $(($select_i - 1))
125 1.10 christos select_result=$1
126 1.10 christos break
127 1.10 christos fi
128 1.10 christos echo >&2 'Please enter a number in range.'
129 1.10 christos esac
130 1.10 christos
131 1.10 christos # Prompt and read input.
132 1.10 christos printf >&2 %s "${PS3-#? }"
133 1.10 christos read select_i || exit
134 1.10 christos done
135 1.10 christos }
136 1.10 christos fi
137 1.10 christos
138 1.12 christos while getopts c:n:t:-: opt
139 1.9 christos do
140 1.24 christos case $opt$OPTARG in
141 1.24 christos c*)
142 1.24 christos coord=$OPTARG;;
143 1.24 christos n*)
144 1.24 christos location_limit=$OPTARG;;
145 1.24 christos t*) # Undocumented option, used for developer testing.
146 1.24 christos zonetabtype=$OPTARG;;
147 1.24 christos -help)
148 1.24 christos exec echo "$usage";;
149 1.24 christos -version)
150 1.24 christos exec echo "tzselect $PKGVERSION$TZVERSION";;
151 1.24 christos -*)
152 1.24 christos say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1;;
153 1.24 christos *)
154 1.24 christos say >&2 "$0: try '$0 --help'"; exit 1
155 1.24 christos esac
156 1.9 christos done
157 1.9 christos
158 1.24 christos shift $(($OPTIND - 1))
159 1.9 christos case $# in
160 1.9 christos 0) ;;
161 1.24 christos *) say >&2 "$0: $1: unknown argument"; exit 1
162 1.9 christos esac
163 1.6 mlelstv
164 1.24 christos # translit=true to try transliteration.
165 1.24 christos # This is false if U+12345 CUNEIFORM SIGN URU TIMES KI has length 1
166 1.24 christos # which means the shell and (presumably) awk do not need transliteration.
167 1.24 christos # It is true if the byte string has some other length in characters, or
168 1.24 christos # if this is a POSIX.1-2017 or earlier shell that does not support $'...'.
169 1.24 christos CUNEIFORM_SIGN_URU_TIMES_KI=$'\360\222\215\205'
170 1.24 christos if test ${#CUNEIFORM_SIGN_URU_TIMES_KI} = 1
171 1.24 christos then translit=false
172 1.24 christos else translit=true
173 1.24 christos fi
174 1.1 jtc
175 1.24 christos # Read into shell variable $1 the contents of file $2.
176 1.24 christos # Convert to the current locale's encoding if possible,
177 1.24 christos # as the shell aligns columns better that way.
178 1.24 christos # If GNU iconv's //TRANSLIT does not work, fall back on POSIXish iconv;
179 1.24 christos # if that does not work, fall back on 'cat'.
180 1.24 christos read_file() {
181 1.24 christos { $translit && {
182 1.24 christos eval "$1=\$( (iconv -f UTF-8 -t //TRANSLIT) 2>/dev/null <\"\$2\")" ||
183 1.24 christos eval "$1=\$( (iconv -f UTF-8) 2>/dev/null <\"\$2\")"
184 1.24 christos }; } ||
185 1.24 christos eval "$1=\$(cat <\"\$2\")" || {
186 1.24 christos say >&2 "$0: time zone files are not set up correctly"
187 1.24 christos exit 1
188 1.24 christos }
189 1.19 christos }
190 1.24 christos read_file TZ_COUNTRY_TABLE "$TZDIR/iso3166.tab"
191 1.24 christos read_file TZ_ZONETABTYPE_TABLE "$TZDIR/$zonetabtype.tab"
192 1.24 christos TZ_ZONENOW_TABLE=
193 1.14 christos
194 1.1 jtc newline='
195 1.1 jtc '
196 1.1 jtc IFS=$newline
197 1.1 jtc
198 1.22 christos # Awk script to output a country list.
199 1.22 christos output_country_list='
200 1.24 christos BEGIN {
201 1.24 christos continent_re = substr(ARGV[1], 2)
202 1.24 christos TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
203 1.24 christos TZ_ZONE_TABLE = substr(ARGV[3], 2)
204 1.24 christos ARGV[1] = ARGV[2] = ARGV[3] = ""
205 1.24 christos FS = "\t"
206 1.24 christos nlines = split(TZ_ZONE_TABLE, line, /\n/)
207 1.24 christos for (iline = 1; iline <= nlines; iline++) {
208 1.24 christos $0 = line[iline]
209 1.24 christos commentary = $0 ~ /^#@/
210 1.24 christos if (commentary) {
211 1.24 christos if ($0 !~ /^#@/)
212 1.24 christos continue
213 1.24 christos col1ccs = substr($1, 3)
214 1.24 christos conts = $2
215 1.24 christos } else {
216 1.24 christos col1ccs = $1
217 1.24 christos conts = $3
218 1.22 christos }
219 1.24 christos ncc = split(col1ccs, cc, /,/)
220 1.24 christos ncont = split(conts, cont, /,/)
221 1.24 christos for (i = 1; i <= ncc; i++) {
222 1.24 christos elsewhere = commentary
223 1.24 christos for (ci = 1; ci <= ncont; ci++) {
224 1.24 christos if (cont[ci] ~ continent_re) {
225 1.24 christos if (!cc_seen[cc[i]]++)
226 1.24 christos cc_list[++ccs] = cc[i]
227 1.24 christos elsewhere = 0
228 1.24 christos }
229 1.22 christos }
230 1.24 christos if (elsewhere)
231 1.24 christos for (i = 1; i <= ncc; i++)
232 1.24 christos cc_elsewhere[cc[i]] = 1
233 1.22 christos }
234 1.22 christos }
235 1.24 christos nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
236 1.24 christos for (i = 1; i <= nlines; i++) {
237 1.24 christos $0 = line[i]
238 1.24 christos if ($0 !~ /^#/)
239 1.24 christos cc_name[$1] = $2
240 1.24 christos }
241 1.24 christos for (i = 1; i <= ccs; i++) {
242 1.24 christos country = cc_list[i]
243 1.24 christos if (cc_elsewhere[country])
244 1.24 christos continue
245 1.24 christos if (cc_name[country])
246 1.24 christos country = cc_name[country]
247 1.24 christos print country
248 1.24 christos }
249 1.22 christos }
250 1.22 christos '
251 1.1 jtc
252 1.24 christos # Awk script to process a time zone table and output the same table,
253 1.22 christos # with each row preceded by its distance from 'here'.
254 1.22 christos # If output_times is set, each row is instead preceded by its local time
255 1.22 christos # and any apostrophes are escaped for the shell.
256 1.22 christos output_distances_or_times='
257 1.9 christos BEGIN {
258 1.24 christos coord = substr(ARGV[1], 2)
259 1.24 christos TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
260 1.24 christos TZ_ZONE_TABLE = substr(ARGV[3], 2)
261 1.24 christos ARGV[1] = ARGV[2] = ARGV[3] = ""
262 1.9 christos FS = "\t"
263 1.22 christos if (!output_times) {
264 1.24 christos nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
265 1.24 christos for (i = 1; i <= nlines; i++) {
266 1.24 christos $0 = line[i]
267 1.24 christos if ($0 ~ /^#/)
268 1.24 christos continue
269 1.24 christos country[$1] = $2
270 1.24 christos }
271 1.22 christos country["US"] = "US" # Otherwise the strings get too long.
272 1.22 christos }
273 1.9 christos }
274 1.12 christos function abs(x) {
275 1.12 christos return x < 0 ? -x : x;
276 1.12 christos }
277 1.12 christos function min(x, y) {
278 1.12 christos return x < y ? x : y;
279 1.12 christos }
280 1.12 christos function convert_coord(coord, deg, minute, ilen, sign, sec) {
281 1.9 christos if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
282 1.9 christos degminsec = coord
283 1.9 christos intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
284 1.9 christos minsec = degminsec - intdeg * 10000
285 1.9 christos intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
286 1.9 christos sec = minsec - intmin * 100
287 1.9 christos deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
288 1.9 christos } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
289 1.9 christos degmin = coord
290 1.9 christos intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
291 1.12 christos minute = degmin - intdeg * 100
292 1.12 christos deg = (intdeg * 60 + minute) / 60
293 1.9 christos } else
294 1.9 christos deg = coord
295 1.9 christos return deg * 0.017453292519943296
296 1.9 christos }
297 1.9 christos function convert_latitude(coord) {
298 1.9 christos match(coord, /..*[-+]/)
299 1.9 christos return convert_coord(substr(coord, 1, RLENGTH - 1))
300 1.9 christos }
301 1.9 christos function convert_longitude(coord) {
302 1.9 christos match(coord, /..*[-+]/)
303 1.9 christos return convert_coord(substr(coord, RLENGTH))
304 1.9 christos }
305 1.9 christos # Great-circle distance between points with given latitude and longitude.
306 1.9 christos # Inputs and output are in radians. This uses the great-circle special
307 1.9 christos # case of the Vicenty formula for distances on ellipsoids.
308 1.12 christos function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
309 1.9 christos dlong = long2 - long1
310 1.13 christos x = cos(lat2) * sin(dlong)
311 1.13 christos y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
312 1.13 christos num = sqrt(x * x + y * y)
313 1.13 christos denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
314 1.9 christos return atan2(num, denom)
315 1.9 christos }
316 1.12 christos # Parallel distance between points with given latitude and longitude.
317 1.12 christos # This is the product of the longitude difference and the cosine
318 1.12 christos # of the latitude of the point that is further from the equator.
319 1.12 christos # I.e., it considers longitudes to be further apart if they are
320 1.12 christos # nearer the equator.
321 1.12 christos function pardist(lat1, long1, lat2, long2) {
322 1.13 christos return abs(long1 - long2) * min(cos(lat1), cos(lat2))
323 1.12 christos }
324 1.12 christos # The distance function is the sum of the great-circle distance and
325 1.12 christos # the parallel distance. It could be weighted.
326 1.12 christos function dist(lat1, long1, lat2, long2) {
327 1.13 christos return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
328 1.12 christos }
329 1.9 christos BEGIN {
330 1.9 christos coord_lat = convert_latitude(coord)
331 1.9 christos coord_long = convert_longitude(coord)
332 1.24 christos nlines = split(TZ_ZONE_TABLE, line, /\n/)
333 1.24 christos for (h = 1; h <= nlines; h++) {
334 1.24 christos $0 = line[h]
335 1.24 christos if ($0 ~ /^#/)
336 1.24 christos continue
337 1.24 christos inline[inlines++] = $0
338 1.24 christos ncc = split($1, cc, /,/)
339 1.24 christos for (i = 1; i <= ncc; i++)
340 1.24 christos cc_used[cc[i]]++
341 1.22 christos }
342 1.24 christos for (h = 0; h < inlines; h++) {
343 1.24 christos $0 = inline[h]
344 1.24 christos outline = $1 "\t" $2 "\t" $3
345 1.24 christos sep = "\t"
346 1.24 christos ncc = split($1, cc, /,/)
347 1.24 christos split("", item_seen)
348 1.24 christos item_seen[""] = 1
349 1.24 christos for (i = 1; i <= ncc; i++) {
350 1.24 christos item = cc_used[cc[i]] <= 1 ? country[cc[i]] : $4
351 1.24 christos if (item_seen[item]++)
352 1.24 christos continue
353 1.24 christos outline = outline sep item
354 1.24 christos sep = "; "
355 1.24 christos }
356 1.24 christos if (output_times) {
357 1.24 christos fmt = "TZ='\''%s'\'' date +'\''%d %%Y %%m %%d %%H:%%M %%a %%b\t%s'\''\n"
358 1.24 christos gsub(/'\''/, "&\\\\&&", outline)
359 1.24 christos printf fmt, $3, h, outline
360 1.24 christos } else {
361 1.24 christos here_lat = convert_latitude($2)
362 1.24 christos here_long = convert_longitude($2)
363 1.24 christos printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), \
364 1.24 christos outline
365 1.24 christos }
366 1.11 christos }
367 1.9 christos }
368 1.9 christos '
369 1.1 jtc
370 1.1 jtc # Begin the main loop. We come back here if the user wants to retry.
371 1.1 jtc while
372 1.1 jtc
373 1.24 christos echo >&2 'Please identify a location' \
374 1.24 christos 'so that time zone rules can be set correctly.'
375 1.1 jtc
376 1.24 christos continent=
377 1.24 christos country=
378 1.24 christos country_result=
379 1.24 christos region=
380 1.24 christos time=
381 1.24 christos TZ_ZONE_TABLE=$TZ_ZONETABTYPE_TABLE
382 1.24 christos
383 1.24 christos case $coord in
384 1.24 christos ?*)
385 1.24 christos continent=coord;;
386 1.24 christos '')
387 1.1 jtc
388 1.24 christos # Ask the user for continent or ocean.
389 1.1 jtc
390 1.24 christos echo >&2 \
391 1.24 christos 'Please select a continent, ocean, "coord", "TZ", "time", or "now".'
392 1.1 jtc
393 1.24 christos quoted_continents=$(
394 1.24 christos $AWK '
395 1.24 christos function handle_entry(entry) {
396 1.24 christos entry = substr(entry, 1, index(entry, "/") - 1)
397 1.24 christos if (entry == "America")
398 1.24 christos entry = entry "s"
399 1.24 christos if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
400 1.24 christos entry = entry " Ocean"
401 1.24 christos printf "'\''%s'\''\n", entry
402 1.24 christos }
403 1.24 christos BEGIN {
404 1.24 christos TZ_ZONETABTYPE_TABLE = substr(ARGV[1], 2)
405 1.24 christos ARGV[1] = ""
406 1.24 christos FS = "\t"
407 1.24 christos nlines = split(TZ_ZONETABTYPE_TABLE, line, /\n/)
408 1.24 christos for (i = 1; i <= nlines; i++) {
409 1.24 christos $0 = line[i]
410 1.24 christos if ($0 ~ /^[^#]/)
411 1.24 christos handle_entry($3)
412 1.24 christos else if ($0 ~ /^#@/) {
413 1.24 christos ncont = split($2, cont, /,/)
414 1.24 christos for (ci = 1; ci <= ncont; ci++)
415 1.24 christos handle_entry(cont[ci])
416 1.24 christos }
417 1.24 christos }
418 1.24 christos }
419 1.24 christos ' ="$TZ_ZONETABTYPE_TABLE" |
420 1.24 christos sort -u |
421 1.24 christos tr '\n' ' '
422 1.24 christos echo ''
423 1.24 christos )
424 1.24 christos
425 1.24 christos eval '
426 1.24 christos doselect '"$quoted_continents"' \
427 1.24 christos "coord - I want to use geographical coordinates." \
428 1.24 christos "TZ - I want to specify the timezone using a proleptic TZ string." \
429 1.24 christos "time - I know local time already." \
430 1.24 christos "now - Like \"time\", but configure only for timestamps from now on."
431 1.24 christos continent=$select_result
432 1.24 christos case $continent in
433 1.24 christos Americas) continent=America;;
434 1.24 christos *)
435 1.24 christos # Get the first word of $continent. Path expansion is disabled
436 1.24 christos # so this works even with "*", which should not happen.
437 1.24 christos IFS=" "
438 1.24 christos for continent in $continent ""; do break; done
439 1.24 christos IFS=$newline;;
440 1.24 christos esac
441 1.24 christos case $zonetabtype,$continent in
442 1.24 christos zonenow,*) ;;
443 1.24 christos *,now)
444 1.24 christos ${TZ_ZONENOW_TABLE:+:} read_file TZ_ZONENOW_TABLE "$TZDIR/zonenow.tab"
445 1.24 christos TZ_ZONE_TABLE=$TZ_ZONENOW_TABLE
446 1.24 christos esac
447 1.24 christos '
448 1.24 christos esac
449 1.1 jtc
450 1.24 christos case $continent in
451 1.24 christos TZ)
452 1.24 christos # Ask the user for a proleptic TZ string. Check that it conforms.
453 1.24 christos check_POSIX_TZ_string='
454 1.24 christos BEGIN {
455 1.24 christos tz = substr(ARGV[1], 2)
456 1.24 christos ARGV[1] = ""
457 1.24 christos tzname = ("(<[[:alnum:]+-][[:alnum:]+-][[:alnum:]+-]+>" \
458 1.24 christos "|[[:alpha:]][[:alpha:]][[:alpha:]]+)")
459 1.24 christos sign = "[-+]?"
460 1.24 christos hhmm = "(:[0-5][0-9](:[0-5][0-9])?)?"
461 1.24 christos offset = sign "(2[0-4]|[0-1]?[0-9])" hhmm
462 1.24 christos time = sign "(16[0-7]|(1[0-5]|[0-9]?)[0-9])" hhmm
463 1.24 christos mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
464 1.24 christos jdate = ("((J[1-9]|[0-9]|J?[1-9][0-9]" \
465 1.24 christos "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])")
466 1.24 christos datetime = ",(" mdate "|" jdate ")(/" time ")?"
467 1.24 christos tzpattern = ("^(:.*|" tzname offset "(" tzname \
468 1.24 christos "(" offset ")?(" datetime datetime ")?)?)$")
469 1.24 christos exit tz ~ tzpattern
470 1.24 christos }
471 1.24 christos '
472 1.24 christos
473 1.24 christos while
474 1.24 christos echo >&2 'Please enter the desired value' \
475 1.24 christos 'of the TZ environment variable.'
476 1.24 christos echo >&2 'For example, AEST-10 is abbreviated' \
477 1.24 christos 'AEST and is 10 hours'
478 1.24 christos echo >&2 'ahead (east) of Greenwich,' \
479 1.24 christos 'with no daylight saving time.'
480 1.24 christos read tz
481 1.24 christos $AWK "$check_POSIX_TZ_string" ="$tz"
482 1.24 christos do
483 1.24 christos say >&2 "'$tz' is not a conforming POSIX proleptic TZ string."
484 1.24 christos done
485 1.24 christos TZ_for_date=$tz;;
486 1.24 christos *)
487 1.24 christos case $continent in
488 1.24 christos coord)
489 1.24 christos case $coord in
490 1.24 christos '')
491 1.24 christos echo >&2 'Please enter coordinates' \
492 1.24 christos 'in ISO 6709 notation.'
493 1.24 christos echo >&2 'For example, +4042-07403 stands for'
494 1.24 christos echo >&2 '40 degrees 42 minutes north,' \
495 1.24 christos '74 degrees 3 minutes west.'
496 1.24 christos read coord
497 1.24 christos esac
498 1.24 christos distance_table=$(
499 1.24 christos $AWK \
500 1.24 christos "$output_distances_or_times" \
501 1.24 christos ="$coord" ="$TZ_COUNTRY_TABLE" ="$TZ_ZONE_TABLE" |
502 1.24 christos sort -n |
503 1.24 christos $AWK "{print} NR == $location_limit { exit }"
504 1.24 christos )
505 1.24 christos regions=$(
506 1.24 christos $AWK '
507 1.24 christos BEGIN {
508 1.24 christos distance_table = substr(ARGV[1], 2)
509 1.24 christos ARGV[1] = ""
510 1.24 christos nlines = split(distance_table, line, /\n/)
511 1.24 christos for (nr = 1; nr <= nlines; nr++) {
512 1.24 christos nf = split(line[nr], f, /\t/)
513 1.24 christos print f[nf]
514 1.24 christos }
515 1.24 christos }
516 1.24 christos ' ="$distance_table"
517 1.24 christos )
518 1.24 christos echo >&2 'Please select one of the following timezones,'
519 1.24 christos echo >&2 'listed roughly in increasing order' \
520 1.24 christos "of distance from $coord".
521 1.24 christos doselect $regions
522 1.24 christos region=$select_result
523 1.24 christos tz=$(
524 1.24 christos $AWK '
525 1.24 christos BEGIN {
526 1.24 christos distance_table = substr(ARGV[1], 2)
527 1.24 christos region = substr(ARGV[2], 2)
528 1.24 christos ARGV[1] = ARGV[2] = ""
529 1.24 christos nlines = split(distance_table, line, /\n/)
530 1.24 christos for (nr = 1; nr <= nlines; nr++) {
531 1.24 christos nf = split(line[nr], f, /\t/)
532 1.24 christos if (f[nf] == region)
533 1.24 christos print f[4]
534 1.24 christos }
535 1.24 christos }
536 1.24 christos ' ="$distance_table" ="$region"
537 1.24 christos );;
538 1.24 christos *)
539 1.24 christos case $continent in
540 1.24 christos now|time)
541 1.24 christos minute_format='%a %b %d %H:%M'
542 1.24 christos old_minute=$(TZ=UTC0 date +"$minute_format")
543 1.24 christos for i in 1 2 3
544 1.24 christos do
545 1.24 christos time_table_command=$(
546 1.24 christos $AWK \
547 1.24 christos -v output_times=1 \
548 1.24 christos "$output_distances_or_times" \
549 1.24 christos = = ="$TZ_ZONE_TABLE"
550 1.24 christos )
551 1.24 christos time_table=$(eval "$time_table_command")
552 1.24 christos new_minute=$(TZ=UTC0 date +"$minute_format")
553 1.24 christos case $old_minute in
554 1.24 christos "$new_minute") break
555 1.24 christos esac
556 1.24 christos old_minute=$new_minute
557 1.24 christos done
558 1.24 christos echo >&2 "The system says Universal Time is $new_minute."
559 1.24 christos echo >&2 "Assuming that's correct, what is the local time?"
560 1.24 christos sorted_table=$(say "$time_table" | sort -k2n -k2,5 -k1n) || {
561 1.24 christos say >&2 "$0: cannot sort time table"
562 1.24 christos exit 1
563 1.24 christos }
564 1.24 christos eval doselect $(
565 1.24 christos $AWK '
566 1.24 christos BEGIN {
567 1.24 christos sorted_table = substr(ARGV[1], 2)
568 1.24 christos ARGV[1] = ""
569 1.24 christos nlines = split(sorted_table, line, /\n/)
570 1.24 christos for (i = 1; i <= nlines; i++) {
571 1.24 christos $0 = line[i]
572 1.24 christos outline = $6 " " $7 " " $4 " " $5
573 1.24 christos if (outline == oldline)
574 1.24 christos continue
575 1.24 christos oldline = outline
576 1.24 christos gsub(/'\''/, "&\\\\&&", outline)
577 1.24 christos printf "'\''%s'\''\n", outline
578 1.24 christos }
579 1.24 christos }
580 1.24 christos ' ="$sorted_table"
581 1.24 christos )
582 1.24 christos time=$select_result
583 1.24 christos continent_re='^'
584 1.24 christos zone_table=$(
585 1.10 christos $AWK '
586 1.24 christos BEGIN {
587 1.24 christos time = substr(ARGV[1], 2)
588 1.24 christos time_table = substr(ARGV[2], 2)
589 1.24 christos ARGV[1] = ARGV[2] = ""
590 1.24 christos nlines = split(time_table, line, /\n/)
591 1.24 christos for (i = 1; i <= nlines; i++) {
592 1.24 christos $0 = line[i]
593 1.24 christos if ($6 " " $7 " " $4 " " $5 == time) {
594 1.24 christos sub(/[^\t]*\t/, "")
595 1.24 christos print
596 1.24 christos }
597 1.24 christos }
598 1.20 christos }
599 1.24 christos ' ="$time" ="$time_table"
600 1.24 christos )
601 1.24 christos countries=$(
602 1.24 christos $AWK \
603 1.24 christos "$output_country_list" \
604 1.24 christos ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
605 1.24 christos sort -f
606 1.24 christos )
607 1.24 christos ;;
608 1.24 christos *)
609 1.24 christos continent_re="^$continent/"
610 1.24 christos zone_table=$TZ_ZONE_TABLE
611 1.24 christos esac
612 1.24 christos
613 1.24 christos # Get list of names of countries in the continent or ocean.
614 1.24 christos countries=$(
615 1.24 christos $AWK \
616 1.24 christos "$output_country_list" \
617 1.24 christos ="$continent_re" ="$TZ_COUNTRY_TABLE" ="$zone_table" |
618 1.24 christos sort -f
619 1.24 christos )
620 1.24 christos # If all zone table entries have comments, and there are
621 1.24 christos # at most 22 entries, asked based on those comments.
622 1.24 christos # This fits the prompt onto old-fashioned 24-line screens.
623 1.24 christos regions=$(
624 1.24 christos $AWK '
625 1.24 christos BEGIN {
626 1.24 christos TZ_ZONE_TABLE = substr(ARGV[1], 2)
627 1.24 christos ARGV[1] = ""
628 1.24 christos FS = "\t"
629 1.24 christos nlines = split(TZ_ZONE_TABLE, line, /\n/)
630 1.24 christos for (i = 1; i <= nlines; i++) {
631 1.24 christos $0 = line[i]
632 1.24 christos if ($0 ~ /^[^#]/ && !missing_comment) {
633 1.24 christos if ($4)
634 1.24 christos comment[++inlines] = $4
635 1.24 christos else
636 1.24 christos missing_comment = 1
637 1.20 christos }
638 1.20 christos }
639 1.24 christos if (!missing_comment && inlines <= 22)
640 1.24 christos for (i = 1; i <= inlines; i++)
641 1.24 christos print comment[i]
642 1.24 christos }
643 1.24 christos ' ="$zone_table"
644 1.24 christos )
645 1.24 christos
646 1.24 christos # If there's more than one country, ask the user which one.
647 1.24 christos case $countries in
648 1.24 christos *"$newline"*)
649 1.24 christos echo >&2 'Please select a country' \
650 1.24 christos 'whose clocks agree with yours.'
651 1.24 christos doselect $countries
652 1.24 christos country_result=$select_result
653 1.24 christos country=$select_result;;
654 1.24 christos *)
655 1.24 christos country=$countries
656 1.24 christos esac
657 1.9 christos
658 1.1 jtc
659 1.24 christos # Get list of timezones in the country.
660 1.24 christos regions=$(
661 1.24 christos $AWK '
662 1.24 christos BEGIN {
663 1.24 christos country = substr(ARGV[1], 2)
664 1.24 christos TZ_COUNTRY_TABLE = substr(ARGV[2], 2)
665 1.24 christos TZ_ZONE_TABLE = substr(ARGV[3], 2)
666 1.24 christos ARGV[1] = ARGV[2] = ARGV[3] = ""
667 1.24 christos FS = "\t"
668 1.24 christos cc = country
669 1.24 christos nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
670 1.24 christos for (i = 1; i <= nlines; i++) {
671 1.24 christos $0 = line[i]
672 1.24 christos if ($0 !~ /^#/ && country == $2) {
673 1.24 christos cc = $1
674 1.24 christos break
675 1.24 christos }
676 1.24 christos }
677 1.24 christos nlines = split(TZ_ZONE_TABLE, line, /\n/)
678 1.24 christos for (i = 1; i <= nlines; i++) {
679 1.24 christos $0 = line[i]
680 1.24 christos if ($0 ~ /^#/)
681 1.24 christos continue
682 1.24 christos if ($1 ~ cc)
683 1.24 christos print $4
684 1.24 christos }
685 1.24 christos }
686 1.24 christos ' ="$country" ="$TZ_COUNTRY_TABLE" ="$zone_table"
687 1.24 christos )
688 1.1 jtc
689 1.24 christos # If there's more than one region, ask the user which one.
690 1.24 christos case $regions in
691 1.24 christos *"$newline"*)
692 1.24 christos echo >&2 'Please select one of the following timezones.'
693 1.24 christos doselect $regions
694 1.24 christos region=$select_result
695 1.24 christos esac
696 1.1 jtc
697 1.24 christos # Determine tz from country and region.
698 1.24 christos tz=$(
699 1.24 christos $AWK '
700 1.24 christos BEGIN {
701 1.24 christos country = substr(ARGV[1], 2)
702 1.24 christos region = substr(ARGV[2], 2)
703 1.24 christos TZ_COUNTRY_TABLE = substr(ARGV[3], 2)
704 1.24 christos TZ_ZONE_TABLE = substr(ARGV[4], 2)
705 1.24 christos ARGV[1] = ARGV[2] = ARGV[3] = ARGV[4] = ""
706 1.24 christos FS = "\t"
707 1.24 christos cc = country
708 1.24 christos nlines = split(TZ_COUNTRY_TABLE, line, /\n/)
709 1.24 christos for (i = 1; i <= nlines; i++) {
710 1.24 christos $0 = line[i]
711 1.24 christos if ($0 !~ /^#/ && country == $2) {
712 1.24 christos cc = $1
713 1.24 christos break
714 1.24 christos }
715 1.24 christos }
716 1.24 christos nlines = split(TZ_ZONE_TABLE, line, /\n/)
717 1.24 christos for (i = 1; i <= nlines; i++) {
718 1.24 christos $0 = line[i]
719 1.24 christos if ($0 ~ /^#/)
720 1.24 christos continue
721 1.24 christos if ($1 ~ cc && ($4 == region || !region))
722 1.24 christos print $3
723 1.24 christos }
724 1.24 christos }
725 1.24 christos ' ="$country" ="$region" ="$TZ_COUNTRY_TABLE" ="$zone_table"
726 1.24 christos )
727 1.24 christos esac
728 1.1 jtc
729 1.24 christos # Make sure the corresponding zoneinfo file exists.
730 1.24 christos TZ_for_date=$TZDIR/$tz
731 1.24 christos <"$TZ_for_date" || {
732 1.24 christos say >&2 "$0: time zone files are not set up correctly"
733 1.24 christos exit 1
734 1.24 christos }
735 1.24 christos esac
736 1.1 jtc
737 1.1 jtc
738 1.24 christos # Use the proposed TZ to output the current date relative to UTC.
739 1.24 christos # Loop until they agree in seconds.
740 1.24 christos # Give up after 8 unsuccessful tries.
741 1.24 christos
742 1.24 christos extra_info=
743 1.24 christos for i in 1 2 3 4 5 6 7 8
744 1.24 christos do
745 1.24 christos TZdate=$(LANG=C TZ="$TZ_for_date" date)
746 1.24 christos UTdate=$(LANG=C TZ=UTC0 date)
747 1.24 christos TZsecsetc=${TZdate##*[0-5][0-9]:}
748 1.24 christos UTsecsetc=${UTdate##*[0-5][0-9]:}
749 1.24 christos if test "${TZsecsetc%%[!0-9]*}" = "${UTsecsetc%%[!0-9]*}"
750 1.24 christos then
751 1.24 christos extra_info="
752 1.24 christos Selected time is now: $TZdate.
753 1.24 christos Universal Time is now: $UTdate."
754 1.24 christos break
755 1.24 christos fi
756 1.24 christos done
757 1.24 christos
758 1.24 christos
759 1.24 christos # Output TZ info and ask the user to confirm.
760 1.24 christos
761 1.24 christos echo >&2 ""
762 1.24 christos echo >&2 "Based on the following information:"
763 1.24 christos echo >&2 ""
764 1.24 christos case $time%$country_result%$region%$coord in
765 1.24 christos ?*%?*%?*%)
766 1.24 christos say >&2 " $time$newline $country_result$newline $region";;
767 1.24 christos ?*%?*%%|?*%%?*%) say >&2 " $time$newline $country_result$region";;
768 1.24 christos ?*%%%) say >&2 " $time";;
769 1.24 christos %?*%?*%) say >&2 " $country_result$newline $region";;
770 1.24 christos %?*%%) say >&2 " $country_result";;
771 1.24 christos %%?*%?*) say >&2 " coord $coord$newline $region";;
772 1.24 christos %%%?*) say >&2 " coord $coord";;
773 1.24 christos *) say >&2 " TZ='$tz'"
774 1.24 christos esac
775 1.24 christos say >&2 ""
776 1.24 christos say >&2 "TZ='$tz' will be used.$extra_info"
777 1.24 christos say >&2 "Is the above information OK?"
778 1.24 christos
779 1.24 christos doselect Yes No
780 1.24 christos ok=$select_result
781 1.24 christos case $ok in
782 1.24 christos Yes) break
783 1.24 christos esac
784 1.9 christos do coord=
785 1.1 jtc done
786 1.1 jtc
787 1.5 kleink case $SHELL in
788 1.24 christos *csh) file=.login line="setenv TZ '$tz'";;
789 1.24 christos *) file=.profile line="export TZ='$tz'"
790 1.5 kleink esac
791 1.5 kleink
792 1.15 christos test -t 1 && say >&2 "
793 1.5 kleink You can make this change permanent for yourself by appending the line
794 1.5 kleink $line
795 1.5 kleink to the file '$file' in your home directory; then log out and log in again.
796 1.5 kleink
797 1.5 kleink Here is that TZ value again, this time on standard output so that you
798 1.5 kleink can use the $0 command in shell scripts:"
799 1.5 kleink
800 1.24 christos say "$tz"
801