"Fossies" - the Fresh Open Source Software Archive

Member "tzselect.ksh" (20 Sep 2022, 15925 Bytes) of package /linux/misc/tzcode2022g.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Bash source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "tzselect.ksh": 2021e_vs_2022e.

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