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