"Fossies" - the Fresh Open Source Software Archive

Member "tzselect.ksh" (16 Jul 2018, 15302 Bytes) of package /linux/misc/tzcode2018i.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. 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": 2018e_vs_2018f.

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