#!/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. if [[ ! -z ${zuper_version} ]]; then warning "zuper version ::1 version:: was already loaded -- doing nothing" ${zuper_version} return; fi ########################## typeset -aU vars typeset -aU arrs typeset -aU maps typeset -aU funs vars=(DEBUG QUIET LOG) arrs=(req freq) vars+=(zuper_version) zuper_version=0.4 # load necessary zsh extensions zmodload zsh/regex zmodload zsh/system zmodload zsh/net/tcp zmodload zsh/mapfile # {{{ Messaging # Messaging function with pretty coloring autoload colors colors vars+=(last_act last_func last_notice) function _msg() { local msg="$2" local i command -v gettext 1>/dev/null 2>/dev/null && msg="$(gettext -s "$2")" for i in {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 } function 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 } # dump all variables, arrays and maps declared as global in zuper # do not print out what is empty zdump() { fn zdump [[ ${#vars} -gt 0 ]] && { print "Global variables:" for _v in $vars; do _c=${(P)_v} [[ "$_c" = "" ]] || print " $_v = \t $_c" done } [[ ${#arrs} -gt 0 ]] && { print "Global arrays:" for _a in $arrs; do _c=${(P)_a} [[ "$_c" = "" ]] || print " $_a \t ( ${(P)_a} )" done } [[ ${#maps} -gt 0 ]] && { print "Global maps:" for _m in $maps; do [[ "${(Pv)_m}" = "" ]] || { 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) # 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." } funs+=(__test_fn) __test_fn(){ echo "foo" } function zuper_end endgame() { fn "endgame $*" # execute all no matter what TRAPZERR() { } # process registered destructors for d in $destruens; do fn "destructor: $d" $d done # unset all the variables included in "vars" for v in $vars; do unset $v done # unset all the assoc-arrays included in "arrs" for a in $arrs; do unset $a done # unset all the maps included in "maps" for m in $maps; do unset $m done ## We should also undefine the core zuper functions to make it ## really idempotent. I have added an array "funs" which contains ## the names of the functions to be undefined by endgame/zuper_end ## FIXME!!!! The only "registered" function so far is __test_fn, ## but if we like this we should register all the core zuper ## functions as soon as they are declared for f in $funs; do unfunction $f done unset maps unset arrs unset vars unset funs return 0 } ## This function should reinitialise zuper and all the variables # zuper_restart(){ # endgame # source zuper # } # Use this to make sure endgame() is called at exit. # unlike TRAPEXIT, the zshexit() hook is not called when functions exit. function zuper.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) } vars+=(ztmpdir) # ztmpd() fills in $ztmpdir global. Caller must copy that variable as # it will be overwritten at every call. ztmpd() { fn ztmpd ztmpdir=`mktemp -d` tmpdirs+=($ztmpdir) } # All tempfiles are freed in endgame() _ztmp_destructor() { fn _ztmp_destructor for f in $tmpfiles; do rm -f "$f" done for d in $tmpdirs; do [[ $d == "" || ! -d $d ]] && continue pushd $d [[ `pwd` == "/" ]] && {popd; continue} popd rm -rf "$d" done tmpfiles=() tmpdirs=() } arrs+=(tmpfiles) arrs+=(tmpdirs) destruens+=(_ztmp_destructor) # }}} Tempfiles # {{{ Strings # tokenizer, works only with one char length delimiters # saves everything in global array tok=() arrs+=(tok) function string.strtok 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))]}" if [[ "$t" == "" ]]; then tok+=("null") else tok+=("$t") fi # save last found f=$c fi done # add last token t=${_string[(e)$(($f + 1)),$c]} if [[ "$t" == "" ]]; then tok+=("null") else tok+=("$t") fi } # remote leading and trailing spaces in a string taken from stdin function string.trim trim() { sed -e 's/^[[:space:]]*//g ; s/[[:space:]]*\$//g' } # extract all emails found in a text from stdin # outputs them one per line function string.extract_emails extract_emails() { awk '{ for (i=1;i<=NF;i++) if ( $i ~ /[[:alnum:]]@[[:alnum:]]/ ) { gsub(/<|>|,/ , "" , $i); print $i } }' } # takes a string as argument, returns success if is an email function string.isemail isemail() { [[ "$1" =~ "\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}\b" ]] && return 0 # print "$1" | grep -q -E '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}' && return 0 return 1 } # takes a numeric argument and prints out a human readable size function string.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 string.html_strip xml_strip html_strip() { sed 's/<[^>]\+>//g' } # changes stdin string special chars to be shown in html function string.escape_html escape_html() { sed -e ' s/\&/\&/g s/>/\>/g s/> $_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 (WIP, needs more testing) # 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 # {{{ Parse commandline options # for example usage, see Tomb http://tomb.dyne.org vars+=(subcommand) arrs+=(option_main option_params) maps+=(option option_subcommands) # Hi, dear developer! Are you trying to add a new subcommand, or # to add some options? Well, keep in mind that option names are # global: they cannot bear a different meaning or behaviour across # subcommands. The only exception is "-o" which means: "options # passed to the local subcommand", and thus can bear a different # meaning for different subcommands. # # For example, "-s" means "size" and accepts one argument. If you # are tempted to add an alternate option "-s" (e.g., to mean # "silent", and that doesn't accept any argument) DON'T DO IT! # # There are two reasons for that: # I. Usability; users expect that "-s" is "size" # II. Option parsing WILL EXPLODE if you do this kind of bad # things (it will complain: "option defined more than once") # # If you want to use the same option in multiple commands then you # can only use the non-abbreviated long-option version like: # -force and NOT -f option.is_set() { # Check whether a commandline option is set. # # Synopsis: option_is_set -flag [out] # # First argument is the commandline flag (e.g., "-s"). # If the second argument is present and set to 'out', print out the # result: either 'set' or 'unset' (useful for if conditions). # # Return 0 if is set, 1 otherwise local -i r # the return code (0 = set, 1 = unset) [[ -n ${(k)option[$1]} ]]; r=$? [[ $2 == "out" ]] && { [[ $r == 0 ]] && { print 'set' } || { print 'unset' } } return $r; } # Print the option value matching the given flag # Unique argument is the commandline flag (e.g., "-s"). option.value() { print -n - "${option[$1]}" } option.parse() { ### Detect subcommand local -aU every_opts #every_opts behave like a set; that is, an array with unique elements for optspec in ${option_subcommands}${option_main}; do for opt in ${=optspec}; do every_opts+=${opt} done done local -a oldstar oldstar=("${(@)argv}") #### detect early: useful for --option-parsing zparseopts -M -D -Adiscardme ${every_opts} if [[ -n ${(k)discardme[--option-parsing]} ]]; then print $1 if [[ -n "$1" ]]; then return 1 fi return 0 fi unset discardme if ! zparseopts -M -E -D -Adiscardme ${every_opts}; then _failure "Command parses error." return 1 fi unset discardme subcommand=${1} if [[ -z $subcommand ]]; then subcommand="__empty" fi if [[ -z ${(k)option_subcommands[$subcommand]} ]]; then subcommand="__unknown:$subcommand" # _warning "There's no such command \"::1 subcommand::\"." $subcommand # _failure "Please try -h for help." fi argv=("${(@)oldstar}") unset oldstar ### Parsing global + command-specific options # zsh magic: ${=string} will split to multiple arguments when spaces occur set -A cmd_opts ${option_main} ${=option_subcommands[$subcommand]} # if there is no option, we don't need parsing if [[ -n $cmd_opts ]]; then zparseopts -M -E -D -Aoption ${cmd_opts} if [[ $? != 0 ]]; then _warning "Some error occurred during option processing." _failure "See zuper option.parse for more info." return 1 fi fi #build option_params (array of arguments) and check if there are unrecognized options ok=0 option_params=() for arg in $*; do if [[ $arg == '--' || $arg == '-' ]]; then ok=1 continue #it shouldn't be appended to option_params elif [[ $arg[1] == '-' ]]; then if [[ $ok == 0 ]]; then _failure "Unrecognized option ::1 arg:: for subcommand ::2 subcommand::" $arg $subcommand return 1 fi fi option_params+=$arg done # First parameter actually is the subcommand: delete it and shift [[ $subcommand != '__empty' ]] && { option_params[1]=(); shift } ### End parsing command-specific options [[ "$option_params" == "" ]] && { func "arg command: ::1 subcommand::" $subcommand } || { func "arg command: ::1 subcommand:: ::2 param::" $subcommand $option_params } } # Later: process subcommand # case "$subcommand" in # help) # print "TODO: help" # ;; # __empty) # zdump # ;; # # Reject unknown command and suggest help # *) # _warning "Command \"::1 subcommand::\" not recognized." $subcommand # _message "Try -h for help." # return 1 # ;; # esac # }}} # {{{ Helpers function helper.isfound isfound() { command -v $1 1>/dev/null 2>/dev/null return $? } # faster substitute for cat function helper.printfile printfile() { print ${mapfile[$1]} } # }}} Helpers # {{{ Config # This is not a full config parser, but its a mechanism to read single # sections of configuration files that are separated using various # syntax methods. The only method supported is now org-mode whose # sections start with #+ . It fills in the global array # $config_section which can be read out to a file or interpreted in # memory, whatever syntax it may contain. vars+=(config_section_type) arrs+=(config_section) config_section_type=org-mode config.section_type() { fn config.section.type _type=$1 req=(_type) ckreq || return $? case $_type in org-mode) config_section_type=org-mode ;; *) error "Unknown config type:$_type" return 1 ;; esac act "$_type config section parser initialized" return 0 } # fills in contents of section in array config_section config.section_read() { fn config.section.read _file=$1 _section=$2 req=(_file _section) freq=($_file) ckreq || return $? case $config_section_type in org-mode) _contents=`awk ' BEGIN { found=0 } /^#\+ '"$_section"'$/ { found=1; next } /^#\+/ { if(found==1) exit 0 } /^$/ { next } { if(found==1) print $0 } ' $_file` ;; *) error "Unknown config type:$_type" ;; esac config_section=() for c in ${(f)_contents}; do config_section+=("$c") done return 0 } # }}} Config