#!/usr/bin/env zsh ## -*- origami-fold-style: triple-braces -*- # # Zuper - Zsh Ultimate Programmer's Extensions Refurbished # # Copyright (C) 2015 Dyne.org Foundation # # Zuper is designed, written and maintained by Denis Roio # # This source code is free software; you can redistribute it and/or # modify it under the terms of the GNU Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This source code is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # Please refer to the GNU Public License for more details. # # You should have received a copy of the GNU Public License along with # this source code; if not, write to: # Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. ########################## typeset -aU vars typeset -aU arrs typeset -aU maps vars=(DEBUG QUIET LOG) arrs=(req freq) vars+=(zuper_version) zuper_version=0.2 zmodload zsh/system zmodload zsh/net/tcp # {{{ Messaging # Messaging function with pretty coloring autoload colors colors vars+=(last_act last_func last_notice) function _msg() { local msg="$2" command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")" for i in $(seq 3 ${#}); do msg=${(S)msg//::$(($i - 2))*::/$*[$i]} done local command="print -P" local progname="$fg[magenta]${PROGRAM##*/}$reset_color" local message="$fg_bold[normal]$fg_no_bold[normal]$msg$reset_color" local -i returncode case "$1" in inline) command+=" -n"; pchars=" > "; pcolor="yellow" ;; message) last_act="$msg" pchars=" . "; pcolor="white"; message="$fg_no_bold[$pcolor]$msg$reset_color" ;; verbose) last_func="$msg" pchars="[D]"; pcolor="blue" ;; success) last_notice="$msg" pchars="(*)"; pcolor="green"; message="$fg_no_bold[$pcolor]$msg$reset_color" ;; warning) pchars="[W]"; pcolor="yellow"; message="$fg_no_bold[$pcolor]$msg$reset_color" ;; failure) pchars="[E]"; pcolor="red"; message="$fg_no_bold[$pcolor]$msg$reset_color" returncode=1 ;; print) progname="" ;; *) pchars="[F]"; pcolor="red" message="Developer oops! Usage: _msg MESSAGE_TYPE \"MESSAGE_CONTENT\"" returncode=127 zerr ;; esac ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >&2 # write the log if its configured [[ "$LOG" = "" ]] || { touch $LOG || return $? ${=command} "${progname} $fg_bold[$pcolor]$pchars$reset_color ${message}$color[reset_color]" >> $LOG } return $returncode } function _message say act() { local notice="message" [[ "$1" = "-n" ]] && shift && notice="inline" [[ $QUIET = 1 ]] || _msg "$notice" $@ return 0 } function _verbose xxx func() { [[ $DEBUG = 1 ]] && _msg verbose $@ return 0 } function _success yes notice() { [[ $QUIET = 1 ]] || _msg success $@ return 0 } function _warning no warn warning() { [[ $QUIET = 1 ]] || _msg warning $@ return 0 } function _failure fatal die error() { # typeset -i exitcode=${exitv:-1} [[ $QUIET = 1 ]] || _msg failure $@ return 1 } function _print() { [[ $QUIET = 1 ]] || _msg print $@ return 0 } # }}} Messaging # {{{ Debugging fn() { fun="$@" req=() freq=() func "$fun" } zerr() { error "error in: ${fun:-$last_notice}" [[ "$last_func" = "" ]] || warn "called in: $last_func" [[ "$last_act" = "" ]] || warn "called in: $last_act" [[ "$last_notice" = "" ]] || warn "called in: $last_notice" # [[ "$fun" = "" ]] || warn "called in: $fun" TRAPEXIT() { error "error reported, operation aborted." } return 1 } ckreq reqck() { err=0 for v in $req; do [[ "${(P)v}" = "" ]] && { warn "${fun[(ws: :)1]}(): required setting is blank: $v" err=1 } done [[ $err = 1 ]] && return $err for f in $freq; do # exists and has size greater than zero [[ -s $f ]] || { warn "required file empty: $f" err=1 } done [[ $err == 1 ]] && zerr return $err } zdump() { fn zdump [[ ${#vars} -gt 0 ]] && { print "Global variables:" for _v in $vars; do print " $_v = \t ${(P)_v}" done } [[ ${#arrs} -gt 0 ]] && { print "Global arrays:" for _a in $arrs; do print " $_a \t ( ${(P)_a} )" done } [[ ${#maps} -gt 0 ]] && { print "Global maps:" for _m in $maps; do print " $_m [key] \t ( ${(Pk)_m} )" print " $_m [val] \t ( ${(Pv)_m} )" done } } # handy wrappers for throw/catch execution of blocks where we need the # program to exit on any error (non-zero) returned by any function throw() { function TRAPZERR() { zerr; return 1 } } catch() { function TRAPZERR() { } } ########################## # Endgame handling arrs+=(destruens) destruens=() # Trap functions for the endgame event TRAPINT() { endgame INT; return $? } # TRAPEXIT() { endgame EXIT; return $? } TRAPHUP() { endgame HUP; return $? } TRAPQUIT() { endgame QUIT; return $? } TRAPABRT() { endgame ABORT; return $? } TRAPKILL() { endgame KILL; return $? } # TRAPPIPE() { endgame PIPE; return $? } TRAPTERM() { endgame TERM; return $? } TRAPSTOP() { endgame STOP; return $? } # TRAPZERR() { func "function returns non-zero." } endgame() { fn "endgame $*" # execute all no matter what TRAPZERR() { } # process registered destructors for d in $destruens; do fn "destructor: $d" $d done return 0 } # Register endgame() to be called at exit. # unlike TRAPEXIT, the zshexit() hook is not called when functions exit. zshexit() { endgame EXIT; return $? } # }}} Debugging # {{{ Tempfiles ########################## # Temp file handling vars+=(ztmpfile) # ztmp() fills in $ztmpfile global. Caller must copy that variable as # it will be overwritten at every call. ztmp() { fn ztmp ztmpfile=`mktemp` tmpfiles+=($ztmpfile) } # All tempfiles are freed in endgame() _ztmp_destructor() { fn _ztmp_destructor for f in $tmpfiles; do rm -f "$f" done tmpfiles=() } arrs+=(tmpfiles) destruens+=(_ztmp_destructor) # }}} Tempfiles # {{{ Strings # tokenizer, works only with one char length delimiters # saves everything in global array tok=() arrs+=(tok) strtok() { fn "strtok $*" _string="$1" _delim="$2" req=(_string _delim) ckreq || return $? tok=() f=0 c=0 for c in {1..${#_string}}; do if [[ "${_string[(e)$c]}" == "$_delim" ]]; then # check if not empty t=${_string[(e)$(($f + 1)),$(($c - 1))]} [[ "$t" == "" ]] || tok+=($t) # save last found f=$c fi done # add last token t=${_string[(e)$(($f + 1)),$c]} [[ "$t" == "" ]] || tok+=($t) } # TODO: move in here some helpers # }}} Strings # {{{ Networking # This is only tested on GNU/Linux and makes use of sysfs # index of all network devices arrs+=(net_devices) # map of ipv4 assigned addresses: [dev addr] maps+=(net_ip4_addr) # map of ipv6 assigned addresses: [dev addr] maps+=(net_ip6_addr) # map of dhcp served ipv4 maps+=(ip4dhcps) # map of dhcp served ipv6 maps+=(ip6dhcps) # map of external ipv4 addresses maps+=(net_ip4_exit) # map of internal ipv6 addresses # maps+=(ip6exits) net.scan_devices() { for i in `find /sys/devices/ -name net`; do dev=`ls $i` # skip the loopback device [[ "$dev" = "lo" ]] && continue net_devices+=($dev) done # return error if no device found if [[ ${#net_devices} = 0 ]]; then return 1 else return 0; fi } net.scan_addresses() { [[ ${#net_devices} = 0 ]] && { error "No network device found." func "Have you ran net.scan_devices() first?" return 1 } for dev in ${net_devices}; do # check ipv4 connections conn=`ip addr show $dev | awk '/inet / {print $2}'` [[ "$conn" = "" ]] || { net_ip4_addr+=($dev $conn) } # check ipv6 connections conn=`ip addr show $dev | awk '/inet6/ {print $2}'` [[ "$conn" = "" ]] || { net_ip6_addr+=($dev $conn) } done # list ipv4 notice "${#net_ip4_addr} ipv4 connected devices found" for c in ${(k)net_ip4_addr}; do act " $c ${net_ip4_addr[$c]}" done # list ipv6 notice "${#net_ip6_addr} ipv6 connected devices found" for c in ${(k)net_ip6_addr}; do act " $c ${net_ip6_addr[$c]}" done # find out network addresses return 0 } net.scan_exits() { # just ipv4 for now, also we use curl to drive the call over the # specific interface, but if that wouldn't matter then rest.get is # better to avoid this dependency for dev in ${(k)net_ip4_addr}; do addr=`curl --silent --interface $dev https://api.ipify.org` if [[ "$?" != "0" ]]; then error "curl returns $?: $addr" else [[ "$addr" = "" ]] || { notice "$dev external ip: $addr" net_ip4_exit+=($dev $addr) } fi done for dev in ${(k)net_ip6_addr}; do addr=`curl --silent --ipv6 --interface $dev https://api.ipify.org` if [[ $? != 0 ]]; then error "curl returns $?: $addr" else [[ "$addr" = "" ]] || { notice "$dev external ip: $addr" net_ip4_exit+=($dev $addr) } fi done } # }}} Networking # {{{ Key/Value filesave # optional: define zkv=1 on source [[ "$zkv" = "" ]] || { ########################## # Key/Value file storage using ZSh associative maps # load a map from a file # map must be already instantiated with typeset -A by called # name of map is defined inside the file function zkv.load() { fn "zkv-load $*" file=$1 [[ "$file" = "" ]] && { error "zkv-open() missing argument: file-path" zerr return 1 } [[ -r "$file" ]] || { error "zkv-open() file not found $file" zerr return 1 } [[ -s "$file" ]] || { error "zkv-open() file is empty" zerr return 1 } source $file } # save a map in a file # $1 = name of the map associative array # $2 = full path to the file function zkv.save() { fn "zkv.save $*" _map=$1 _path=$2 [[ "$_path" = "" ]] && { error "zkv.save() missing argument: map-name path-to-file" zerr return 1 } [[ -r $_path ]] && { func "zkv.close() overwriting $_path" func "backup turd left behind: ${_path}~" mv $_path $_path~ } touch $_path # wondering about http://www.zsh.org/mla/users/2015/msg00286.html # meanwhile solved using a double array, wasting a full map memcpy _karr=(${(Pk)_map}) _varr=(${(Pv)_map}) _num="${#_karr}" for c in {1..$_num}; do # can also be cat here, however for speed we use builtins # switch to cat if compatibility is an issue sysread -o 1 <> $_path $_map+=("${_karr[$c]}" "${(v)_varr[$c]}") EOF done func "$_num key/values stored in $_path" } } # }}} Key/Value filesave # {{{ Get/Set REST API ######## # Restful API client # there is a clear zsh optimization here in get/set kv # using zsh/tcp instead of spawning curl # and perhaps querying with one call using ?recursive vars+=(rest_reply_body rest_reply_header) maps+=(rest_header) function rest.put() { fn "rest.put $*" # $1 = hostname # $2 = port # $3 = path # value from stdin | # to check if the http service is running is up to the caller _host=${1} # ip address _port=${2} _path=${3} sysread _v req=(_host) ckreq || return $? if ztcp $_host $_port; then # TODO: work out various parsers, this one works with consul.io _fd=$REPLY # func "tcp open on fd $fd" cat <& $_fd PUT ${_path} HTTP/1.1 User-Agent: Zuper/$zuper_version Host: ${_host}:${_port} Accept: */* Content-Length: ${#_v} Content-Type: application/x-www-form-urlencoded EOF print -n "$_v" >& $_fd sysread -i $_fd _res # close connection ztcp -c $_fd [[ "$_res" =~ "true" ]] || { warn "failed PUT on restful key/value" warn "host: ${_host}" warn "port: ${_port}" warn "path: ${_path}" warn "value: $_v" print - "$_res" zerr return 1 } else error "cannot connect to restful service: $_host:$_port" zerr return 1 fi return 0 } function rest.get() { fn "rest.get $*" _host=${1} _port=${2} _path=${3} req=(_host _port) ckreq || return $? ztcp $_host $_port || { zerr return 1 } _fd=$REPLY # TODO: work out various parsers, this one works with consul.io cat <& $_fd GET ${_path} HTTP/1.1 User-Agent: Zuper/$zuper_version Host: $_host:$_port Accept: */* EOF # read header response rest_reply=`sysread -i $_fd -o 1` for i in "${(f)rest_reply}"; do print $i | hexdump -C # first line is the response code [[ "$i" -regex-match "\x0d\x0a$" ]] && { func BLANK break } # # save other lines in map for fast retrieval # _field=${i[(ws@:@)1]} # func "$_field - header field parsed" # rest_header[$_field]="${i[(ws@:@)2]}" # c=$(( $c + 1 )) done # rest_reply_header="${(f)$(cat <&$_fd)}" func "${#rest_reply_header} bytes response header stored in rest_reply_header" # | awk -F: ' #/"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d # TODO: read content-length and use it here rest_reply_body="${(f)$(sysread -i $_fd -o 1)}" func "${#rest_reply_body} bytes response body stored in rest_reply_body" # close connection ztcp -c $_fd return 0 } # }}} Get/Set REST API # {{{ Helpers [[ "$helpers" = "" ]] || { function helper.isfound isfound() { command -v $1 1>/dev/null 2>/dev/null return $? } # remote leading and trailing spaces in a string taken from stdin function helper.trim trim() { sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g' } zmodload zsh/mapfile # faster substitute for cat function helper.printfile printfile() { print ${mapfile[$1]} } # extract all emails found in a text from stdin # outputs them one per line function helper.extract-emails extract_emails() { awk '{ for (i=1;i<=NF;i++) if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) { gsub(/<|>|,/ , "" , $i); print $i } }' } zmodload zsh/regex # takes a string as argument, returns success if is an email function helper.isemail isemail() { [[ "$1" -regex-match "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0 return 1 } # takes a numeric argument and prints out a human readable size function helper.human-size human_size() { [[ $1 -gt 0 ]] || { error "human_size() called with invalid argument" return 1 } # we use the binary operation for speed # shift right 10 is divide by 1024 # gigabytes [[ $1 -gt 1073741824 ]] && { print -n "$(( $1 >> 30 )) GB" return 0 } # megabytes [[ $1 -gt 1048576 ]] && { print -n "$(( $1 >> 20 )) MB" return 0 } # kilobytes [[ $1 -gt 1024 ]] && { print -n "$(( $1 >> 10 )) KB" return 0 } # bytes print -n "$1 Bytes" return 0 } # strips out all html/xml tags (everything between < >) function helper.html-strip xml_strip html_strip() { sed 's/<[^>]\+>//g' } # changes stdin string special chars to be shown in html function helper.escape-html escape_html() { sed -e ' s/\&/\&/g s/>/\>/g s/