PageRenderTime 136ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/timezone/tzselect.ksh

https://gitlab.com/gbenson/glibc
Korn Shell | 559 lines | 446 code | 55 blank | 58 comment | 20 complexity | 7240be1d2d5a9a693faafc71c552694e MD5 | raw file
  1. #!/bin/bash
  2. PKGVERSION='(tzcode) '
  3. TZVERSION=see_Makefile
  4. REPORT_BUGS_TO=tz@iana.org
  5. # Ask the user about the time zone, and output the resulting TZ value to stdout.
  6. # Interact with the user via stderr and stdin.
  7. # Contributed by Paul Eggert. This file is in the public domain.
  8. # Porting notes:
  9. #
  10. # This script requires a Posix-like shell and prefers the extension of a
  11. # 'select' statement. The 'select' statement was introduced in the
  12. # Korn shell and is available in Bash and other shell implementations.
  13. # If your host lacks both Bash and the Korn shell, you can get their
  14. # source from one of these locations:
  15. #
  16. # Bash <http://www.gnu.org/software/bash/bash.html>
  17. # Korn Shell <http://www.kornshell.com/>
  18. # Public Domain Korn Shell <http://www.cs.mun.ca/~michael/pdksh/>
  19. #
  20. # For portability to Solaris 9 /bin/sh this script avoids some POSIX
  21. # features and common extensions, such as $(...) (which works sometimes
  22. # but not others), $((...)), and $10.
  23. #
  24. # This script also uses several features of modern awk programs.
  25. # If your host lacks awk, or has an old awk that does not conform to Posix,
  26. # you can use either of the following free programs instead:
  27. #
  28. # Gawk (GNU awk) <http://www.gnu.org/software/gawk/>
  29. # mawk <http://invisible-island.net/mawk/>
  30. # Specify default values for environment variables if they are unset.
  31. : ${AWK=awk}
  32. : ${TZDIR=`pwd`}
  33. # Output one argument as-is to standard output.
  34. # Safer than 'echo', which can mishandle '\' or leading '-'.
  35. say() {
  36. printf '%s\n' "$1"
  37. }
  38. # Check for awk Posix compliance.
  39. ($AWK -v x=y 'BEGIN { exit 123 }') </dev/null >/dev/null 2>&1
  40. [ $? = 123 ] || {
  41. say >&2 "$0: Sorry, your '$AWK' program is not Posix compatible."
  42. exit 1
  43. }
  44. coord=
  45. location_limit=10
  46. zonetabtype=zone1970
  47. usage="Usage: tzselect [--version] [--help] [-c COORD] [-n LIMIT]
  48. Select a time zone interactively.
  49. Options:
  50. -c COORD
  51. Instead of asking for continent and then country and then city,
  52. ask for selection from time zones whose largest cities
  53. are closest to the location with geographical coordinates COORD.
  54. COORD should use ISO 6709 notation, for example, '-c +4852+00220'
  55. for Paris (in degrees and minutes, North and East), or
  56. '-c -35-058' for Buenos Aires (in degrees, South and West).
  57. -n LIMIT
  58. Display at most LIMIT locations when -c is used (default $location_limit).
  59. --version
  60. Output version information.
  61. --help
  62. Output this help.
  63. Report bugs to $REPORT_BUGS_TO."
  64. # Ask the user to select from the function's arguments,
  65. # and assign the selected argument to the variable 'select_result'.
  66. # Exit on EOF or I/O error. Use the shell's 'select' builtin if available,
  67. # falling back on a less-nice but portable substitute otherwise.
  68. if
  69. case $BASH_VERSION in
  70. ?*) : ;;
  71. '')
  72. # '; exit' should be redundant, but Dash doesn't properly fail without it.
  73. (eval 'set --; select x; do break; done; exit') </dev/null 2>/dev/null
  74. esac
  75. then
  76. # Do this inside 'eval', as otherwise the shell might exit when parsing it
  77. # even though it is never executed.
  78. eval '
  79. doselect() {
  80. select select_result
  81. do
  82. case $select_result in
  83. "") echo >&2 "Please enter a number in range." ;;
  84. ?*) break
  85. esac
  86. done || exit
  87. }
  88. # Work around a bug in bash 1.14.7 and earlier, where $PS3 is sent to stdout.
  89. case $BASH_VERSION in
  90. [01].*)
  91. case `echo 1 | (select x in x; do break; done) 2>/dev/null` in
  92. ?*) PS3=
  93. esac
  94. esac
  95. '
  96. else
  97. doselect() {
  98. # Field width of the prompt numbers.
  99. select_width=`expr $# : '.*'`
  100. select_i=
  101. while :
  102. do
  103. case $select_i in
  104. '')
  105. select_i=0
  106. for select_word
  107. do
  108. select_i=`expr $select_i + 1`
  109. printf >&2 "%${select_width}d) %s\\n" $select_i "$select_word"
  110. done ;;
  111. *[!0-9]*)
  112. echo >&2 'Please enter a number in range.' ;;
  113. *)
  114. if test 1 -le $select_i && test $select_i -le $#; then
  115. shift `expr $select_i - 1`
  116. select_result=$1
  117. break
  118. fi
  119. echo >&2 'Please enter a number in range.'
  120. esac
  121. # Prompt and read input.
  122. printf >&2 %s "${PS3-#? }"
  123. read select_i || exit
  124. done
  125. }
  126. fi
  127. while getopts c:n:t:-: opt
  128. do
  129. case $opt$OPTARG in
  130. c*)
  131. coord=$OPTARG ;;
  132. n*)
  133. location_limit=$OPTARG ;;
  134. t*) # Undocumented option, used for developer testing.
  135. zonetabtype=$OPTARG ;;
  136. -help)
  137. exec echo "$usage" ;;
  138. -version)
  139. exec echo "tzselect $PKGVERSION$TZVERSION" ;;
  140. -*)
  141. say >&2 "$0: -$opt$OPTARG: unknown option; try '$0 --help'"; exit 1 ;;
  142. *)
  143. say >&2 "$0: try '$0 --help'"; exit 1 ;;
  144. esac
  145. done
  146. shift `expr $OPTIND - 1`
  147. case $# in
  148. 0) ;;
  149. *) say >&2 "$0: $1: unknown argument"; exit 1 ;;
  150. esac
  151. # Make sure the tables are readable.
  152. TZ_COUNTRY_TABLE=$TZDIR/iso3166.tab
  153. TZ_ZONE_TABLE=$TZDIR/$zonetabtype.tab
  154. for f in $TZ_COUNTRY_TABLE $TZ_ZONE_TABLE
  155. do
  156. <"$f" || {
  157. say >&2 "$0: time zone files are not set up correctly"
  158. exit 1
  159. }
  160. done
  161. # If the current locale does not support UTF-8, convert data to current
  162. # locale's format if possible, as the shell aligns columns better that way.
  163. # Check the UTF-8 of U+12345 CUNEIFORM SIGN URU TIMES KI.
  164. ! $AWK 'BEGIN { u12345 = "\360\222\215\205"; exit length(u12345) != 1 }' &&
  165. { tmp=`(mktemp -d) 2>/dev/null` || {
  166. tmp=${TMPDIR-/tmp}/tzselect.$$ &&
  167. (umask 77 && mkdir -- "$tmp")
  168. };} &&
  169. trap 'status=$?; rm -fr -- "$tmp"; exit $status' 0 HUP INT PIPE TERM &&
  170. (iconv -f UTF-8 -t //TRANSLIT <"$TZ_COUNTRY_TABLE" >$tmp/iso3166.tab) \
  171. 2>/dev/null &&
  172. TZ_COUNTRY_TABLE=$tmp/iso3166.tab &&
  173. iconv -f UTF-8 -t //TRANSLIT <"$TZ_ZONE_TABLE" >$tmp/$zonetabtype.tab &&
  174. TZ_ZONE_TABLE=$tmp/$zonetabtype.tab
  175. newline='
  176. '
  177. IFS=$newline
  178. # Awk script to read a time zone table and output the same table,
  179. # with each column preceded by its distance from 'here'.
  180. output_distances='
  181. BEGIN {
  182. FS = "\t"
  183. while (getline <TZ_COUNTRY_TABLE)
  184. if ($0 ~ /^[^#]/)
  185. country[$1] = $2
  186. country["US"] = "US" # Otherwise the strings get too long.
  187. }
  188. function abs(x) {
  189. return x < 0 ? -x : x;
  190. }
  191. function min(x, y) {
  192. return x < y ? x : y;
  193. }
  194. function convert_coord(coord, deg, minute, ilen, sign, sec) {
  195. if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9][0-9][0-9]([^0-9]|$)/) {
  196. degminsec = coord
  197. intdeg = degminsec < 0 ? -int(-degminsec / 10000) : int(degminsec / 10000)
  198. minsec = degminsec - intdeg * 10000
  199. intmin = minsec < 0 ? -int(-minsec / 100) : int(minsec / 100)
  200. sec = minsec - intmin * 100
  201. deg = (intdeg * 3600 + intmin * 60 + sec) / 3600
  202. } else if (coord ~ /^[-+]?[0-9]?[0-9][0-9][0-9][0-9]([^0-9]|$)/) {
  203. degmin = coord
  204. intdeg = degmin < 0 ? -int(-degmin / 100) : int(degmin / 100)
  205. minute = degmin - intdeg * 100
  206. deg = (intdeg * 60 + minute) / 60
  207. } else
  208. deg = coord
  209. return deg * 0.017453292519943296
  210. }
  211. function convert_latitude(coord) {
  212. match(coord, /..*[-+]/)
  213. return convert_coord(substr(coord, 1, RLENGTH - 1))
  214. }
  215. function convert_longitude(coord) {
  216. match(coord, /..*[-+]/)
  217. return convert_coord(substr(coord, RLENGTH))
  218. }
  219. # Great-circle distance between points with given latitude and longitude.
  220. # Inputs and output are in radians. This uses the great-circle special
  221. # case of the Vicenty formula for distances on ellipsoids.
  222. function gcdist(lat1, long1, lat2, long2, dlong, x, y, num, denom) {
  223. dlong = long2 - long1
  224. x = cos(lat2) * sin(dlong)
  225. y = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlong)
  226. num = sqrt(x * x + y * y)
  227. denom = sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(dlong)
  228. return atan2(num, denom)
  229. }
  230. # Parallel distance between points with given latitude and longitude.
  231. # This is the product of the longitude difference and the cosine
  232. # of the latitude of the point that is further from the equator.
  233. # I.e., it considers longitudes to be further apart if they are
  234. # nearer the equator.
  235. function pardist(lat1, long1, lat2, long2) {
  236. return abs(long1 - long2) * min(cos(lat1), cos(lat2))
  237. }
  238. # The distance function is the sum of the great-circle distance and
  239. # the parallel distance. It could be weighted.
  240. function dist(lat1, long1, lat2, long2) {
  241. return gcdist(lat1, long1, lat2, long2) + pardist(lat1, long1, lat2, long2)
  242. }
  243. BEGIN {
  244. coord_lat = convert_latitude(coord)
  245. coord_long = convert_longitude(coord)
  246. }
  247. /^[^#]/ {
  248. here_lat = convert_latitude($2)
  249. here_long = convert_longitude($2)
  250. line = $1 "\t" $2 "\t" $3
  251. sep = "\t"
  252. ncc = split($1, cc, /,/)
  253. for (i = 1; i <= ncc; i++) {
  254. line = line sep country[cc[i]]
  255. sep = ", "
  256. }
  257. if (NF == 4)
  258. line = line " - " $4
  259. printf "%g\t%s\n", dist(coord_lat, coord_long, here_lat, here_long), line
  260. }
  261. '
  262. # Begin the main loop. We come back here if the user wants to retry.
  263. while
  264. echo >&2 'Please identify a location' \
  265. 'so that time zone rules can be set correctly.'
  266. continent=
  267. country=
  268. region=
  269. case $coord in
  270. ?*)
  271. continent=coord;;
  272. '')
  273. # Ask the user for continent or ocean.
  274. echo >&2 'Please select a continent, ocean, "coord", or "TZ".'
  275. quoted_continents=`
  276. $AWK '
  277. BEGIN { FS = "\t" }
  278. /^[^#]/ {
  279. entry = substr($3, 1, index($3, "/") - 1)
  280. if (entry == "America")
  281. entry = entry "s"
  282. if (entry ~ /^(Arctic|Atlantic|Indian|Pacific)$/)
  283. entry = entry " Ocean"
  284. printf "'\''%s'\''\n", entry
  285. }
  286. ' <"$TZ_ZONE_TABLE" |
  287. sort -u |
  288. tr '\n' ' '
  289. echo ''
  290. `
  291. eval '
  292. doselect '"$quoted_continents"' \
  293. "coord - I want to use geographical coordinates." \
  294. "TZ - I want to specify the time zone using the Posix TZ format."
  295. continent=$select_result
  296. case $continent in
  297. Americas) continent=America;;
  298. *" "*) continent=`expr "$continent" : '\''\([^ ]*\)'\''`
  299. esac
  300. '
  301. esac
  302. case $continent in
  303. TZ)
  304. # Ask the user for a Posix TZ string. Check that it conforms.
  305. while
  306. echo >&2 'Please enter the desired value' \
  307. 'of the TZ environment variable.'
  308. echo >&2 'For example, GST-10 is a zone named GST' \
  309. 'that is 10 hours ahead (east) of UTC.'
  310. read TZ
  311. $AWK -v TZ="$TZ" 'BEGIN {
  312. tzname = "(<[[:alnum:]+-]{3,}>|[[:alpha:]]{3,})"
  313. time = "(2[0-4]|[0-1]?[0-9])" \
  314. "(:[0-5][0-9](:[0-5][0-9])?)?"
  315. offset = "[-+]?" time
  316. mdate = "M([1-9]|1[0-2])\\.[1-5]\\.[0-6]"
  317. jdate = "((J[1-9]|[0-9]|J?[1-9][0-9]" \
  318. "|J?[1-2][0-9][0-9])|J?3[0-5][0-9]|J?36[0-5])"
  319. datetime = ",(" mdate "|" jdate ")(/" 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. Selected 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. test -t 1 && 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"