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

/timezone/tzselect.ksh

https://gitlab.com/Namal/glibc
Korn Shell | 556 lines | 443 code | 55 blank | 58 comment | 19 complexity | 5025b3a57f9c2e6312c1e7c2d62d2717 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.
  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 = "[^-+,0-9][^-+,0-9][^-+,0-9]+"
  313. time = "[0-2]?[0-9](:[0-5][0-9](:[0-5][0-9])?)?"
  314. offset = "[-+]?" time
  315. date = "(J?[0-9]+|M[0-9]+\\.[0-9]+\\.[0-9]+)"
  316. datetime = "," date "(/" time ")?"
  317. tzpattern = "^(:.*|" tzname offset "(" tzname \
  318. "(" offset ")?(" datetime datetime ")?)?)$"
  319. if (TZ ~ tzpattern) exit 1
  320. exit 0
  321. }'
  322. do
  323. say >&2 "'$TZ' is not a conforming Posix time zone string."
  324. done
  325. TZ_for_date=$TZ;;
  326. *)
  327. case $continent in
  328. coord)
  329. case $coord in
  330. '')
  331. echo >&2 'Please enter coordinates' \
  332. 'in ISO 6709 notation.'
  333. echo >&2 'For example, +4042-07403 stands for'
  334. echo >&2 '40 degrees 42 minutes north,' \
  335. '74 degrees 3 minutes west.'
  336. read coord;;
  337. esac
  338. distance_table=`$AWK \
  339. -v coord="$coord" \
  340. -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
  341. "$output_distances" <"$TZ_ZONE_TABLE" |
  342. sort -n |
  343. sed "${location_limit}q"
  344. `
  345. regions=`say "$distance_table" | $AWK '
  346. BEGIN { FS = "\t" }
  347. { print $NF }
  348. '`
  349. echo >&2 'Please select one of the following' \
  350. 'time zone regions,'
  351. echo >&2 'listed roughly in increasing order' \
  352. "of distance from $coord".
  353. doselect $regions
  354. region=$select_result
  355. TZ=`say "$distance_table" | $AWK -v region="$region" '
  356. BEGIN { FS="\t" }
  357. $NF == region { print $4 }
  358. '`
  359. ;;
  360. *)
  361. # Get list of names of countries in the continent or ocean.
  362. countries=`$AWK \
  363. -v continent="$continent" \
  364. -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
  365. '
  366. BEGIN { FS = "\t" }
  367. /^#/ { next }
  368. $3 ~ ("^" continent "/") {
  369. ncc = split($1, cc, /,/)
  370. for (i = 1; i <= ncc; i++)
  371. if (!cc_seen[cc[i]]++) cc_list[++ccs] = cc[i]
  372. }
  373. END {
  374. while (getline <TZ_COUNTRY_TABLE) {
  375. if ($0 !~ /^#/) cc_name[$1] = $2
  376. }
  377. for (i = 1; i <= ccs; i++) {
  378. country = cc_list[i]
  379. if (cc_name[country]) {
  380. country = cc_name[country]
  381. }
  382. print country
  383. }
  384. }
  385. ' <"$TZ_ZONE_TABLE" | sort -f`
  386. # If there's more than one country, ask the user which one.
  387. case $countries in
  388. *"$newline"*)
  389. echo >&2 'Please select a country' \
  390. 'whose clocks agree with yours.'
  391. doselect $countries
  392. country=$select_result;;
  393. *)
  394. country=$countries
  395. esac
  396. # Get list of names of time zone rule regions in the country.
  397. regions=`$AWK \
  398. -v country="$country" \
  399. -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
  400. '
  401. BEGIN {
  402. FS = "\t"
  403. cc = country
  404. while (getline <TZ_COUNTRY_TABLE) {
  405. if ($0 !~ /^#/ && country == $2) {
  406. cc = $1
  407. break
  408. }
  409. }
  410. }
  411. /^#/ { next }
  412. $1 ~ cc { print $4 }
  413. ' <"$TZ_ZONE_TABLE"`
  414. # If there's more than one region, ask the user which one.
  415. case $regions in
  416. *"$newline"*)
  417. echo >&2 'Please select one of the following' \
  418. 'time zone regions.'
  419. doselect $regions
  420. region=$select_result;;
  421. *)
  422. region=$regions
  423. esac
  424. # Determine TZ from country and region.
  425. TZ=`$AWK \
  426. -v country="$country" \
  427. -v region="$region" \
  428. -v TZ_COUNTRY_TABLE="$TZ_COUNTRY_TABLE" \
  429. '
  430. BEGIN {
  431. FS = "\t"
  432. cc = country
  433. while (getline <TZ_COUNTRY_TABLE) {
  434. if ($0 !~ /^#/ && country == $2) {
  435. cc = $1
  436. break
  437. }
  438. }
  439. }
  440. /^#/ { next }
  441. $1 ~ cc && $4 == region { print $3 }
  442. ' <"$TZ_ZONE_TABLE"`
  443. esac
  444. # Make sure the corresponding zoneinfo file exists.
  445. TZ_for_date=$TZDIR/$TZ
  446. <"$TZ_for_date" || {
  447. say >&2 "$0: time zone files are not set up correctly"
  448. exit 1
  449. }
  450. esac
  451. # Use the proposed TZ to output the current date relative to UTC.
  452. # Loop until they agree in seconds.
  453. # Give up after 8 unsuccessful tries.
  454. extra_info=
  455. for i in 1 2 3 4 5 6 7 8
  456. do
  457. TZdate=`LANG=C TZ="$TZ_for_date" date`
  458. UTdate=`LANG=C TZ=UTC0 date`
  459. TZsec=`expr "$TZdate" : '.*:\([0-5][0-9]\)'`
  460. UTsec=`expr "$UTdate" : '.*:\([0-5][0-9]\)'`
  461. case $TZsec in
  462. $UTsec)
  463. extra_info="
  464. Local time is now: $TZdate.
  465. Universal Time is now: $UTdate."
  466. break
  467. esac
  468. done
  469. # Output TZ info and ask the user to confirm.
  470. echo >&2 ""
  471. echo >&2 "The following information has been given:"
  472. echo >&2 ""
  473. case $country%$region%$coord in
  474. ?*%?*%) say >&2 " $country$newline $region";;
  475. ?*%%) say >&2 " $country";;
  476. %?*%?*) say >&2 " coord $coord$newline $region";;
  477. %%?*) say >&2 " coord $coord";;
  478. *) say >&2 " TZ='$TZ'"
  479. esac
  480. say >&2 ""
  481. say >&2 "Therefore TZ='$TZ' will be used.$extra_info"
  482. say >&2 "Is the above information OK?"
  483. doselect Yes No
  484. ok=$select_result
  485. case $ok in
  486. Yes) break
  487. esac
  488. do coord=
  489. done
  490. case $SHELL in
  491. *csh) file=.login line="setenv TZ '$TZ'";;
  492. *) file=.profile line="TZ='$TZ'; export TZ"
  493. esac
  494. say >&2 "
  495. You can make this change permanent for yourself by appending the line
  496. $line
  497. to the file '$file' in your home directory; then log out and log in again.
  498. Here is that TZ value again, this time on standard output so that you
  499. can use the $0 command in shell scripts:"
  500. say "$TZ"