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