Zsh Ultimate Programmer's Extensions Refurbished

zuper 25KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100
  1. #!/usr/bin/env zsh
  2. ## -*- origami-fold-style: triple-braces -*-
  3. #
  4. # Zuper - Zsh Ultimate Programmer's Extensions Refurbished
  5. #
  6. # Copyright (C) 2015 Dyne.org Foundation
  7. #
  8. # Zuper is designed, written and maintained by Denis Roio <jaromil@dyne.org>
  9. #
  10. # This source code is free software; you can redistribute it and/or
  11. # modify it under the terms of the GNU Public License as published by
  12. # the Free Software Foundation; either version 3 of the License, or
  13. # (at your option) any later version.
  14. #
  15. # This source code is distributed in the hope that it will be useful,
  16. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  18. # Please refer to the GNU Public License for more details.
  19. #
  20. # You should have received a copy of the GNU Public License along with
  21. # this source code; if not, write to:
  22. # Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
  23. if [[ ! -z ${zuper_version} ]]; then
  24. warning "zuper version ::1 version:: was already loaded -- doing nothing" ${zuper_version}
  25. return;
  26. fi
  27. ##########################
  28. typeset -aU vars
  29. typeset -aU arrs
  30. typeset -aU maps
  31. typeset -aU funs
  32. vars=(DEBUG QUIET LOG)
  33. arrs=(req freq)
  34. vars+=(zuper_version)
  35. zuper_version=0.4
  36. # load necessary zsh extensions
  37. zmodload zsh/regex
  38. zmodload zsh/system
  39. zmodload zsh/net/tcp
  40. zmodload zsh/mapfile
  41. # {{{ Messaging
  42. # Messaging function with pretty coloring
  43. autoload colors
  44. colors
  45. vars+=(last_act last_func last_notice)
  46. function _msg() {
  47. local msg="$2"
  48. local i
  49. command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")"
  50. for i in {3..${#}}; do
  51. msg=${(S)msg//::$(($i - 2))*::/$*[$i]}
  52. done
  53. local command="print -P"
  54. local progname="$fg[magenta]${PROGRAM##*/}$reset_color"
  55. local message="$fg_bold[normal]$fg_no_bold[normal]$msg$reset_color"
  56. local -i returncode
  57. case "$1" in
  58. inline)
  59. command+=" -n"; pchars=" > "; pcolor="yellow"
  60. ;;
  61. message)
  62. last_act="$msg"
  63. pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]$msg$reset_color"
  64. ;;
  65. verbose)
  66. last_func="$msg"
  67. pchars="[D]"; pcolor="blue"
  68. ;;
  69. success)
  70. last_notice="$msg"
  71. pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]$msg$reset_color"
  72. ;;
  73. warning)
  74. pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]$msg$reset_color"
  75. ;;
  76. failure)
  77. pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]$msg$reset_color"
  78. returncode=1
  79. ;;
  80. print)
  81. progname=""
  82. ;;
  83. *)
  84. pchars="[F]"; pcolor="red"
  85. message="Developer oops! Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\""
  86. returncode=127
  87. zerr
  88. ;;
  89. esac
  90. ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >&2
  91. # write the log if its configured
  92. [[ "$LOG" = "" ]] || {
  93. touch $LOG || return $?
  94. ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >> $LOG
  95. }
  96. return $returncode
  97. }
  98. function _message say act() {
  99. local notice="message"
  100. [[ "$1" = "-n" ]] && shift && notice="inline"
  101. [[ $QUIET = 1 ]] || _msg "$notice" $@
  102. return 0
  103. }
  104. function _verbose xxx func() {
  105. [[ $DEBUG = 1 ]] && _msg verbose $@
  106. return 0
  107. }
  108. function _success yes notice() {
  109. [[ $QUIET = 1 ]] || _msg success $@
  110. return 0
  111. }
  112. function _warning no warn warning() {
  113. [[ $QUIET = 1 ]] || _msg warning $@
  114. return 0
  115. }
  116. function _failure fatal die error() {
  117. # typeset -i exitcode=${exitv:-1}
  118. [[ $QUIET = 1 ]] || _msg failure $@
  119. return 1
  120. }
  121. function _print() {
  122. [[ $QUIET = 1 ]] || _msg print $@
  123. return 0
  124. }
  125. # }}} Messaging
  126. # {{{ Debugging
  127. fn() {
  128. fun="$@"
  129. req=()
  130. freq=()
  131. func "$fun"
  132. }
  133. zerr() {
  134. error "error in: ${fun:-$last_notice}"
  135. [[ "$last_func" = "" ]] || warn "called in: $last_func"
  136. [[ "$last_act" = "" ]] || warn "called in: $last_act"
  137. [[ "$last_notice" = "" ]] || warn "called in: $last_notice"
  138. # [[ "$fun" = "" ]] || warn "called in: $fun"
  139. TRAPEXIT() {
  140. error "error reported, operation aborted."
  141. }
  142. return 1
  143. }
  144. function ckreq reqck() {
  145. err=0
  146. for v in $req; do
  147. [[ "${(P)v}" = "" ]] && {
  148. warn "${fun[(ws: :)1]}(): required setting is blank: $v"
  149. err=1
  150. }
  151. done
  152. [[ $err = 1 ]] && return $err
  153. for f in $freq; do
  154. # exists and has size greater than zero
  155. [[ -s $f ]] || {
  156. warn "required file empty: $f"
  157. err=1
  158. }
  159. done
  160. [[ $err == 1 ]] && zerr
  161. return $err
  162. }
  163. # dump all variables, arrays and maps declared as global in zuper
  164. # do not print out what is empty
  165. zdump() {
  166. fn zdump
  167. [[ ${#vars} -gt 0 ]] && {
  168. print "Global variables:"
  169. for _v in $vars; do
  170. _c=${(P)_v}
  171. [[ "$_c" = "" ]] ||
  172. print " $_v = \t $_c"
  173. done
  174. }
  175. [[ ${#arrs} -gt 0 ]] && {
  176. print "Global arrays:"
  177. for _a in $arrs; do
  178. _c=${(P)_a}
  179. [[ "$_c" = "" ]] ||
  180. print " $_a \t ( ${(P)_a} )"
  181. done
  182. }
  183. [[ ${#maps} -gt 0 ]] && {
  184. print "Global maps:"
  185. for _m in $maps; do
  186. [[ "${(Pv)_m}" = "" ]] || {
  187. print " $_m [key] \t ( ${(Pk)_m} )"
  188. print " $_m [val] \t ( ${(Pv)_m} )"
  189. }
  190. done
  191. }
  192. }
  193. # handy wrappers for throw/catch execution of blocks where we need the
  194. # program to exit on any error (non-zero) returned by any function
  195. throw() { function TRAPZERR() { zerr; return 1 } }
  196. catch() { function TRAPZERR() { } }
  197. ##########################
  198. # Endgame handling
  199. arrs+=(destruens)
  200. # Trap functions for the endgame event
  201. # TRAPINT() { endgame INT; return $? }
  202. # TRAPEXIT() { endgame EXIT; return $? }
  203. TRAPHUP() { endgame HUP; return $? }
  204. TRAPQUIT() { endgame QUIT; return $? }
  205. TRAPABRT() { endgame ABORT; return $? }
  206. TRAPKILL() { endgame KILL; return $? }
  207. # TRAPPIPE() { endgame PIPE; return $? }
  208. TRAPTERM() { endgame TERM; return $? }
  209. TRAPSTOP() { endgame STOP; return $? }
  210. # TRAPZERR() { func "function returns non-zero." }
  211. funs+=(__test_fn)
  212. __test_fn(){
  213. echo "foo"
  214. }
  215. function zuper_end endgame() {
  216. fn "endgame $*"
  217. # execute all no matter what
  218. TRAPZERR() { }
  219. # process registered destructors
  220. for d in $destruens; do
  221. fn "destructor: $d"
  222. $d
  223. done
  224. # unset all the variables included in "vars"
  225. for v in $vars; do
  226. unset $v
  227. done
  228. # unset all the assoc-arrays included in "arrs"
  229. for a in $arrs; do
  230. unset $a
  231. done
  232. # unset all the maps included in "maps"
  233. for m in $maps; do
  234. unset $m
  235. done
  236. ## We should also undefine the core zuper functions to make it
  237. ## really idempotent. I have added an array "funs" which contains
  238. ## the names of the functions to be undefined by endgame/zuper_end
  239. ## FIXME!!!! The only "registered" function so far is __test_fn,
  240. ## but if we like this we should register all the core zuper
  241. ## functions as soon as they are declared
  242. for f in $funs; do
  243. unfunction $f
  244. done
  245. unset maps
  246. unset arrs
  247. unset vars
  248. unset funs
  249. return 0
  250. }
  251. ## This function should reinitialise zuper and all the variables
  252. # zuper_restart(){
  253. # endgame
  254. # source zuper
  255. # }
  256. # Use this to make sure endgame() is called at exit.
  257. # unlike TRAPEXIT, the zshexit() hook is not called when functions exit.
  258. function zuper.exit zshexit() { endgame EXIT; return $? }
  259. # }}} Debugging
  260. # {{{ Tempfiles
  261. ##########################
  262. # Temp file handling
  263. vars+=(ztmpfile)
  264. # ztmp() fills in $ztmpfile global. Caller must copy that variable as
  265. # it will be overwritten at every call.
  266. ztmp() {
  267. fn ztmp
  268. ztmpfile=`mktemp`
  269. tmpfiles+=($ztmpfile)
  270. }
  271. vars+=(ztmpdir)
  272. # ztmpd() fills in $ztmpdir global. Caller must copy that variable as
  273. # it will be overwritten at every call.
  274. ztmpd() {
  275. fn ztmpd
  276. ztmpdir=`mktemp -d`
  277. tmpdirs+=($ztmpdir)
  278. }
  279. # All tempfiles are freed in endgame()
  280. _ztmp_destructor() {
  281. fn _ztmp_destructor
  282. for f in $tmpfiles; do
  283. rm -f "$f"
  284. done
  285. for d in $tmpdirs; do
  286. [[ $d == "" || ! -d $d ]] && continue
  287. pushd $d
  288. [[ `pwd` == "/" ]] && {popd; continue}
  289. popd
  290. rm -rf "$d"
  291. done
  292. tmpfiles=()
  293. tmpdirs=()
  294. }
  295. arrs+=(tmpfiles)
  296. arrs+=(tmpdirs)
  297. destruens+=(_ztmp_destructor)
  298. # }}} Tempfiles
  299. # {{{ Strings
  300. # tokenizer, works only with one char length delimiters
  301. # saves everything in global array tok=()
  302. arrs+=(tok)
  303. function string.strtok strtok() {
  304. fn "strtok $*"
  305. _string="$1"
  306. _delim="$2"
  307. req=(_string _delim)
  308. ckreq || return $?
  309. tok=()
  310. f=0
  311. c=0
  312. for c in {1..${#_string}}; do
  313. if [[ "${_string[(e)$c]}" == "$_delim" ]]; then
  314. # check if not empty
  315. t="${_string[(e)$(($f + 1)),$(($c - 1))]}"
  316. if [[ "$t" == "" ]]; then
  317. tok+=("null")
  318. else
  319. tok+=("$t")
  320. fi
  321. # save last found
  322. f=$c
  323. fi
  324. done
  325. # add last token
  326. t=${_string[(e)$(($f + 1)),$c]}
  327. if [[ "$t" == "" ]]; then
  328. tok+=("null")
  329. else
  330. tok+=("$t")
  331. fi
  332. }
  333. # remote leading and trailing spaces in a string taken from stdin
  334. function string.trim trim() {
  335. sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g'
  336. }
  337. # extract all emails found in a text from stdin
  338. # outputs them one per line
  339. function string.extract_emails extract_emails() {
  340. awk '{ for (i=1;i<=NF;i++)
  341. if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) {
  342. gsub(/<|>|,/ , "" , $i); print $i } }'
  343. }
  344. # takes a string as argument, returns success if is an email
  345. function string.isemail isemail() {
  346. [[ "$1" =~ "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0
  347. # print "$1" | grep -q -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}' && return 0
  348. return 1
  349. }
  350. # takes a numeric argument and prints out a human readable size
  351. function string.human_size human_size() {
  352. [[ $1 -gt 0 ]] || {
  353. error "human_size() called with invalid argument"
  354. return 1
  355. }
  356. # we use the binary operation for speed
  357. # shift right 10 is divide by 1024
  358. # gigabytes
  359. [[ $1 -gt 1073741824 ]] && {
  360. print -n "$(( $1 >> 30 )) GB"
  361. return 0
  362. }
  363. # megabytes
  364. [[ $1 -gt 1048576 ]] && {
  365. print -n "$(( $1 >> 20 )) MB"
  366. return 0
  367. }
  368. # kilobytes
  369. [[ $1 -gt 1024 ]] && {
  370. print -n "$(( $1 >> 10 )) KB"
  371. return 0
  372. }
  373. # bytes
  374. print -n "$1 Bytes"
  375. return 0
  376. }
  377. # strips out all html/xml tags (everything between < >)
  378. function string.html_strip xml_strip html_strip() { sed 's/<[^>]\+>//g' }
  379. # changes stdin string special chars to be shown in html
  380. function string.escape_html escape_html() {
  381. sed -e '
  382. s/\&/\&amp;/g
  383. s/>/\&gt;/g
  384. s/</\&lt;/g
  385. s/"/\&quot;/g
  386. '
  387. }
  388. # escapes special chars in urls
  389. function string.decode_url decode_url urldecode() {
  390. sed -e '
  391. s/%25/%/gi
  392. s/%20/ /gi
  393. s/%09/ /gi
  394. s/%21/!/gi
  395. s/%22/"/gi
  396. s/%23/#/gi
  397. s/%24/\$/gi
  398. s/%26/\&/gi
  399. s/%27/'\''/gi
  400. s/%28/(/gi
  401. s/%29/)/gi
  402. s/%2a/\*/gi
  403. s/%2b/+/gi
  404. s/%2c/,/gi
  405. s/%2d/-/gi
  406. s/%2e/\./gi
  407. s/%2f/\//gi
  408. s/%3a/:/gi
  409. s/%3b/;/gi
  410. s/%3d/=/gi
  411. s/%3e//gi
  412. s/%3f/?/gi
  413. s/%40/@/gi
  414. s/%5b/\[/gi
  415. s/%5c/\\/gi
  416. s/%5d/\]/gi
  417. s/%5e/\^/gi
  418. s/%5f/_/gi
  419. s/%60/`/gi
  420. s/%7b/{/gi
  421. s/%7c/|/gi
  422. s/%7d/}/gi
  423. s/%7e/~/gi
  424. s/%09/ /gi
  425. '
  426. }
  427. function helper.encode-url encode_url urlencode() {
  428. sed -e '
  429. s/%/%25/g
  430. s/ /%20/g
  431. s/ /%09/g
  432. s/!/%21/g
  433. s/"/%22/g
  434. s/#/%23/g
  435. s/\$/%24/g
  436. s/\&/%26/g
  437. s/'\''/%27/g
  438. s/(/%28/g
  439. s/)/%29/g
  440. s/\*/%2a/g
  441. s/+/%2b/g
  442. s/,/%2c/g
  443. s/-/%2d/g
  444. s/\./%2e/g
  445. s/\//%2f/g
  446. s/:/%3a/g
  447. s/;/%3b/g
  448. s//%3e/g
  449. s/?/%3f/g
  450. s/@/%40/g
  451. s/\[/%5b/g
  452. s/\\/%5c/g
  453. s/\]/%5d/g
  454. s/\^/%5e/g
  455. s/_/%5f/g
  456. s/`/%60/g
  457. s/{/%7b/g
  458. s/|/%7c/g
  459. s/}/%7d/g
  460. s/~/%7e/g
  461. s/ /%09/g
  462. '
  463. }
  464. # Check if a version number (semantic format) is greater than the other
  465. # returns 0 if the first argument is greater or equal than the second.
  466. function string.version_greatoreq version_greatoreq() {
  467. [[ "$(printf '%s\n' "$@" | sort -rV | head -n 1)" = "$1" ]] && return 0
  468. return 1
  469. }
  470. # }}} Strings
  471. # {{{ Networking
  472. # This is only tested on GNU/Linux and makes use of sysfs
  473. # index of all network devices
  474. arrs+=(net_devices)
  475. # map of ipv4 assigned addresses: [dev addr]
  476. maps+=(net_ip4_addr)
  477. # map of ipv6 assigned addresses: [dev addr]
  478. maps+=(net_ip6_addr)
  479. # map of dhcp served ipv4
  480. maps+=(ip4dhcps)
  481. # map of dhcp served ipv6
  482. maps+=(ip6dhcps)
  483. # map of external ipv4 addresses
  484. maps+=(net_ip4_exit)
  485. # map of internal ipv6 addresses
  486. # maps+=(ip6exits)
  487. net.scan_devices() {
  488. for i in ${(f)"$(find /sys/devices/ -name net)"}; do
  489. for dev in ${(f)"$(ls --indicator-style=none $i)"}; do
  490. # skip the loopback device
  491. [[ "$dev" =~ "^lo" ]] && continue
  492. func "found network device: $dev"
  493. net_devices+=($dev)
  494. done
  495. done
  496. # return error if no device found
  497. if [[ ${#net_devices} = 0 ]]; then return 1
  498. else
  499. act "${#net_devices} devices found: ${net_devices}"
  500. return 0
  501. fi
  502. }
  503. net.scan_addresses() {
  504. [[ ${#net_devices} = 0 ]] && {
  505. error "No network device found."
  506. func "Have you ran net.scan_devices() first?"
  507. return 1
  508. }
  509. for dev in ${net_devices}; do
  510. # check ipv4 connections
  511. conn=`ip addr show $dev | awk '/inet / {print $2}'`
  512. [[ "$conn" = "" ]] || {
  513. net_ip4_addr+=($dev $conn) }
  514. # check ipv6 connections
  515. conn=`ip addr show $dev | awk '/inet6/ {print $2}'`
  516. [[ "$conn" = "" ]] || {
  517. net_ip6_addr+=($dev $conn) }
  518. done
  519. # list ipv4
  520. act "${#net_ip4_addr} ipv4 connected devices found"
  521. for c in ${(k)net_ip4_addr}; do
  522. act " $c ${net_ip4_addr[$c]}"
  523. done
  524. # list ipv6
  525. act "${#net_ip6_addr} ipv6 connected devices found"
  526. for c in ${(k)net_ip6_addr}; do
  527. act " $c ${net_ip6_addr[$c]}"
  528. done
  529. # find out network addresses
  530. return 0
  531. }
  532. net.scan_exits() {
  533. # just ipv4 for now, also we use curl to drive the call over the
  534. # specific interface, but if that wouldn't matter then rest.get is
  535. # better to avoid this dependency
  536. for dev in ${(k)net_ip4_addr}; do
  537. addr=`curl --silent --interface $dev https://api.ipify.org`
  538. if [[ "$?" != "0" ]]; then
  539. error "curl returns $?: $addr"
  540. else
  541. [[ "$addr" = "" ]] || {
  542. notice "$dev external ip: $addr"
  543. net_ip4_exit+=($dev $addr)
  544. }
  545. fi
  546. done
  547. for dev in ${(k)net_ip6_addr}; do
  548. addr=`curl --silent --ipv6 --interface $dev https://api.ipify.org`
  549. if [[ $? != 0 ]]; then
  550. error "curl returns $?: $addr"
  551. else
  552. [[ "$addr" = "" ]] || {
  553. notice "$dev external ip: $addr"
  554. net_ip4_exit+=($dev $addr)
  555. }
  556. fi
  557. done
  558. }
  559. # }}} Networking
  560. # {{{ Key/Value filesave
  561. # optional: define zkv=1 on source
  562. ##########################
  563. # Key/Value file storage using ZSh associative maps
  564. # load a map from a file
  565. # map must be already instantiated with typeset -A by called
  566. # name of map is defined inside the file
  567. function zkv.load() {
  568. fn "zkv-load $*"
  569. file=$1
  570. [[ "$file" = "" ]] && {
  571. error "zkv-open() missing argument: file-path"
  572. zerr
  573. return 1 }
  574. [[ -r "$file" ]] || {
  575. error "zkv-open() file not found $file"
  576. zerr
  577. return 1 }
  578. [[ -s "$file" ]] || {
  579. error "zkv-open() file is empty"
  580. zerr
  581. return 1 }
  582. source $file
  583. }
  584. # save a map in a file
  585. # $1 = name of the map associative array
  586. # $2 = full path to the file
  587. function zkv.save() {
  588. fn "zkv.save $*"
  589. _map=$1
  590. _path=$2
  591. [[ "$_path" = "" ]] && {
  592. error "zkv.save() missing argument: map-name path-to-file"
  593. zerr
  594. return 1
  595. }
  596. [[ -r $_path ]] && {
  597. func "zkv.close() overwriting $_path"
  598. func "backup turd left behind: ${_path}~"
  599. mv $_path $_path~
  600. }
  601. touch $_path
  602. # wondering about http://www.zsh.org/mla/users/2015/msg00286.html
  603. # meanwhile solved using a double array, wasting a full map memcpy
  604. _karr=(${(Pk)_map})
  605. _varr=(${(Pv)_map})
  606. _num="${#_karr}"
  607. for c in {1..$_num}; do
  608. # can also be cat here, however for speed we use builtins
  609. # switch to cat if compatibility is an issue
  610. sysread -o 1 <<EOF >> $_path
  611. $_map+=("${_karr[$c]}" "${(v)_varr[$c]}")
  612. EOF
  613. done
  614. func "$_num key/values stored in $_path"
  615. }
  616. # }}} Key/Value filesave
  617. # {{{ Get/Set REST API
  618. ########
  619. # Restful API client (WIP, needs more testing)
  620. # there is a clear zsh optimization here in get/set kv
  621. # using zsh/tcp instead of spawning curl
  622. # and perhaps querying with one call using ?recursive
  623. vars+=(rest_reply_body rest_reply_header)
  624. maps+=(rest_header)
  625. function rest.put() {
  626. fn "rest.put $*"
  627. # $1 = hostname
  628. # $2 = port
  629. # $3 = path
  630. # value from stdin |
  631. # to check if the http service is running is up to the caller
  632. _host=${1} # ip address
  633. _port=${2}
  634. _path=${3}
  635. sysread _v
  636. req=(_host)
  637. ckreq || return $?
  638. if ztcp $_host $_port; then
  639. # TODO: work out various parsers, this one works with consul.io
  640. _fd=$REPLY
  641. # func "tcp open on fd $fd"
  642. cat <<EOF >& $_fd
  643. PUT ${_path} HTTP/1.1
  644. User-Agent: Zuper/$zuper_version
  645. Host: ${_host}:${_port}
  646. Accept: */*
  647. Content-Length: ${#_v}
  648. Content-Type: application/x-www-form-urlencoded
  649. EOF
  650. print -n "$_v" >& $_fd
  651. sysread -i $_fd _res
  652. # close connection
  653. ztcp -c $_fd
  654. [[ "$_res" =~ "true" ]] || {
  655. warn "failed PUT on restful key/value"
  656. warn "host: ${_host}"
  657. warn "port: ${_port}"
  658. warn "path: ${_path}"
  659. warn "value: $_v"
  660. print - "$_res"
  661. zerr
  662. return 1
  663. }
  664. else
  665. error "cannot connect to restful service: $_host:$_port"
  666. zerr
  667. return 1
  668. fi
  669. return 0
  670. }
  671. function rest.get() {
  672. fn "rest.get $*"
  673. _host=${1}
  674. _port=${2}
  675. _path=${3}
  676. req=(_host _port)
  677. ckreq || return $?
  678. ztcp $_host $_port || {
  679. zerr
  680. return 1
  681. }
  682. _fd=$REPLY
  683. # TODO: work out various parsers, this one works with consul.io
  684. cat <<EOF >& $_fd
  685. GET ${_path} HTTP/1.1
  686. User-Agent: Zuper/$zuper_version
  687. Host: $_host:$_port
  688. Accept: */*
  689. EOF
  690. # read header response
  691. rest_reply=`sysread -i $_fd -o 1`
  692. for i in "${(f)rest_reply}"; do
  693. print $i | hexdump -C
  694. # first line is the response code
  695. [[ "$i" -regex-match "\x0d\x0a$" ]] && {
  696. func BLANK
  697. break }
  698. # # save other lines in map for fast retrieval
  699. # _field=${i[(ws@:@)1]}
  700. # func "$_field - header field parsed"
  701. # rest_header[$_field]="${i[(ws@:@)2]}"
  702. # c=$(( $c + 1 ))
  703. done
  704. # rest_reply_header="${(f)$(cat <&$_fd)}"
  705. func "${#rest_reply_header} bytes response header stored in rest_reply_header"
  706. # | awk -F: '
  707. #/"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d
  708. # TODO: read content-length and use it here
  709. rest_reply_body="${(f)$(sysread -i $_fd -o 1)}"
  710. func "${#rest_reply_body} bytes response body stored in rest_reply_body"
  711. # close connection
  712. ztcp -c $_fd
  713. return 0
  714. }
  715. # }}} Get/Set REST API
  716. # {{{ Parse commandline options
  717. # for example usage, see Tomb http://tomb.dyne.org
  718. vars+=(subcommand)
  719. arrs+=(option_main option_params)
  720. maps+=(option option_subcommands)
  721. # Hi, dear developer! Are you trying to add a new subcommand, or
  722. # to add some options? Well, keep in mind that option names are
  723. # global: they cannot bear a different meaning or behaviour across
  724. # subcommands. The only exception is "-o" which means: "options
  725. # passed to the local subcommand", and thus can bear a different
  726. # meaning for different subcommands.
  727. #
  728. # For example, "-s" means "size" and accepts one argument. If you
  729. # are tempted to add an alternate option "-s" (e.g., to mean
  730. # "silent", and that doesn't accept any argument) DON'T DO IT!
  731. #
  732. # There are two reasons for that:
  733. # I. Usability; users expect that "-s" is "size"
  734. # II. Option parsing WILL EXPLODE if you do this kind of bad
  735. # things (it will complain: "option defined more than once")
  736. #
  737. # If you want to use the same option in multiple commands then you
  738. # can only use the non-abbreviated long-option version like:
  739. # -force and NOT -f
  740. option.is_set() {
  741. # Check whether a commandline option is set.
  742. #
  743. # Synopsis: option_is_set -flag [out]
  744. #
  745. # First argument is the commandline flag (e.g., "-s").
  746. # If the second argument is present and set to 'out', print out the
  747. # result: either 'set' or 'unset' (useful for if conditions).
  748. #
  749. # Return 0 if is set, 1 otherwise
  750. local -i r # the return code (0 = set, 1 = unset)
  751. [[ -n ${(k)option[$1]} ]];
  752. r=$?
  753. [[ $2 == "out" ]] && {
  754. [[ $r == 0 ]] && { print 'set' } || { print 'unset' }
  755. }
  756. return $r;
  757. }
  758. # Print the option value matching the given flag
  759. # Unique argument is the commandline flag (e.g., "-s").
  760. option.value() {
  761. print -n - "${option[$1]}"
  762. }
  763. option.parse() {
  764. ### Detect subcommand
  765. local -aU every_opts #every_opts behave like a set; that is, an array with unique elements
  766. for optspec in ${option_subcommands}${option_main}; do
  767. for opt in ${=optspec}; do
  768. every_opts+=${opt}
  769. done
  770. done
  771. local -a oldstar
  772. oldstar=("${(@)argv}")
  773. #### detect early: useful for --option-parsing
  774. zparseopts -M -D -Adiscardme ${every_opts}
  775. if [[ -n ${(k)discardme[--option-parsing]} ]]; then
  776. print $1
  777. if [[ -n "$1" ]]; then
  778. return 1
  779. fi
  780. return 0
  781. fi
  782. unset discardme
  783. if ! zparseopts -M -E -D -Adiscardme ${every_opts}; then
  784. _failure "Command parses error."
  785. return 1
  786. fi
  787. unset discardme
  788. subcommand=${1}
  789. if [[ -z $subcommand ]]; then
  790. subcommand="__empty"
  791. fi
  792. if [[ -z ${(k)option_subcommands[$subcommand]} ]]; then
  793. subcommand="__unknown:$subcommand"
  794. # _warning "There's no such command \"::1 subcommand::\"." $subcommand
  795. # _failure "Please try -h for help."
  796. fi
  797. argv=("${(@)oldstar}")
  798. unset oldstar
  799. ### Parsing global + command-specific options
  800. # zsh magic: ${=string} will split to multiple arguments when spaces occur
  801. set -A cmd_opts ${option_main} ${=option_subcommands[$subcommand]}
  802. # if there is no option, we don't need parsing
  803. if [[ -n $cmd_opts ]]; then
  804. zparseopts -M -E -D -Aoption ${cmd_opts}
  805. if [[ $? != 0 ]]; then
  806. _warning "Some error occurred during option processing."
  807. _failure "See zuper option.parse for more info."
  808. return 1
  809. fi
  810. fi
  811. #build option_params (array of arguments) and check if there are unrecognized options
  812. ok=0
  813. option_params=()
  814. for arg in $*; do
  815. if [[ $arg == '--' || $arg == '-' ]]; then
  816. ok=1
  817. continue #it shouldn't be appended to option_params
  818. elif [[ $arg[1] == '-' ]]; then
  819. if [[ $ok == 0 ]]; then
  820. _failure "Unrecognized option ::1 arg:: for subcommand ::2 subcommand::" $arg $subcommand
  821. return 1
  822. fi
  823. fi
  824. option_params+=$arg
  825. done
  826. # First parameter actually is the subcommand: delete it and shift
  827. [[ $subcommand != '__empty' ]] && { option_params[1]=(); shift }
  828. ### End parsing command-specific options
  829. [[ "$option_params" == "" ]] && {
  830. func "arg command: ::1 subcommand::" $subcommand
  831. } || {
  832. func "arg command: ::1 subcommand:: ::2 param::" $subcommand $option_params
  833. }
  834. }
  835. # Later: process subcommand
  836. # case "$subcommand" in
  837. # help)
  838. # print "TODO: help"
  839. # ;;
  840. # __empty)
  841. # zdump
  842. # ;;
  843. # # Reject unknown command and suggest help
  844. # *)
  845. # _warning "Command \"::1 subcommand::\" not recognized." $subcommand
  846. # _message "Try -h for help."
  847. # return 1
  848. # ;;
  849. # esac
  850. # }}}
  851. # {{{ Helpers
  852. function helper.isfound isfound() {
  853. command -v $1 1>/dev/null 2>/dev/null
  854. return $?
  855. }
  856. # faster substitute for cat
  857. function helper.printfile printfile() {
  858. print ${mapfile[$1]}
  859. }
  860. # }}} Helpers
  861. # {{{ Config
  862. # This is not a full config parser, but its a mechanism to read single
  863. # sections of configuration files that are separated using various
  864. # syntax methods. The only method supported is now org-mode whose
  865. # sections start with #+ . It fills in the global array
  866. # $config_section which can be read out to a file or interpreted in
  867. # memory, whatever syntax it may contain.
  868. vars+=(config_section_type)
  869. arrs+=(config_section)
  870. config_section_type=org-mode
  871. config.section_type() {
  872. fn config.section.type
  873. _type=$1
  874. req=(_type)
  875. ckreq || return $?
  876. case $_type in
  877. org-mode)
  878. config_section_type=org-mode
  879. ;;
  880. *)
  881. error "Unknown config type:$_type"
  882. return 1
  883. ;;
  884. esac
  885. act "$_type config section parser initialized"
  886. return 0
  887. }
  888. # fills in contents of section in array config_section
  889. config.section_read() {
  890. fn config.section.read
  891. _file=$1
  892. _section=$2
  893. req=(_file _section)
  894. freq=($_file)
  895. ckreq || return $?
  896. case $config_section_type in
  897. org-mode)
  898. _contents=`awk '
  899. BEGIN { found=0 }
  900. /^#\+ '"$_section"'$/ { found=1; next }
  901. /^#\+/ { if(found==1) exit 0 }
  902. /^$/ { next }
  903. { if(found==1) print $0 }
  904. ' $_file`
  905. ;;
  906. *)
  907. error "Unknown config type:$_type"
  908. ;;
  909. esac
  910. config_section=()
  911. for c in ${(f)_contents}; do
  912. config_section+=("$c")
  913. done
  914. return 0
  915. }
  916. # }}} Config