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