#!/usr/bin/env zsh # # 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 # 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 } 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 "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 $? } ########################## # 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) # tokenizer, works only with one char length delimiters # saves everything in global array tok=() arrs+=(tok) strtok() { fun="strtok $*" _string="$1" _delim="$2" req=(_string _delim) ckreq || return $? tok=() local f=0 local 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) } # optional: define zkv=1 on source [[ "$zkv" = "" ]] || { ########################## # Key/Value file storage using ZSh associative maps zmodload zsh/system # 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" } } # optional: define restful=1 on source [[ "$restful" = "" ]] || { ######## # 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 zmodload zsh/net/tcp function restful.put() { # $1 = hostname # $2 = port # $3 = path # $4 = key # $5 = value fn "restful.put $*" # to check if the http service is running is up to the caller _host=${1} # ip address _port=${2} _path=${3} _k="$4" # key name _v="$5" # value req=(_host _k _v) 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}${_k} 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 "endpoint: ${_host}:${_port}${_path}" warn "resource: $_k = $_v" print - "$_res" zerr return 1 } else error "cannot connect to restful service: $_host:$_port" zerr return 1 fi return 0 } restful.get() { fn "restful.get $*" _host=${1} _port=${2} _path=${3} _k=$4 # key name req=(_host _k) ckreq || return $? _k=$1 ztcp $_host $_port || { zerr return 1 } _fd=$REPLY # TODO: work out various parsers, this one works with consul.io cat <& $_fd GET ${_path}${_k} HTTP/1.1 User-Agent: Zuper/$zuper_version Host: $_host:$_port Accept: */* EOF sysread -i $_fd -o 1 | awk -F: ' /"Value":/ { gsub(/"|}]/,"",$7) ; print $7 }' | base64 -d # close connection ztcp -c $_fd return 0 } } # {{{ 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/