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