PageRenderTime 55ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/libc/time/tzselect.ksh

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