mirror of https://github.com/dyne/zuper.git
1100 lines
25 KiB
Bash
Executable File
1100 lines
25 KiB
Bash
Executable File
#!/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 <jaromil@dyne.org>
|
|
#
|
|
# 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/</\</g
|
|
s/"/\"/g
|
|
'
|
|
}
|
|
|
|
# escapes special chars in urls
|
|
function string.decode_url decode_url urldecode() {
|
|
sed -e '
|
|
s/%25/%/gi
|
|
s/%20/ /gi
|
|
s/%09/ /gi
|
|
s/%21/!/gi
|
|
s/%22/"/gi
|
|
s/%23/#/gi
|
|
s/%24/\$/gi
|
|
s/%26/\&/gi
|
|
s/%27/'\''/gi
|
|
s/%28/(/gi
|
|
s/%29/)/gi
|
|
s/%2a/\*/gi
|
|
s/%2b/+/gi
|
|
s/%2c/,/gi
|
|
s/%2d/-/gi
|
|
s/%2e/\./gi
|
|
s/%2f/\//gi
|
|
s/%3a/:/gi
|
|
s/%3b/;/gi
|
|
s/%3d/=/gi
|
|
s/%3e//gi
|
|
s/%3f/?/gi
|
|
s/%40/@/gi
|
|
s/%5b/\[/gi
|
|
s/%5c/\\/gi
|
|
s/%5d/\]/gi
|
|
s/%5e/\^/gi
|
|
s/%5f/_/gi
|
|
s/%60/`/gi
|
|
s/%7b/{/gi
|
|
s/%7c/|/gi
|
|
s/%7d/}/gi
|
|
s/%7e/~/gi
|
|
s/%09/ /gi
|
|
'
|
|
}
|
|
|
|
function helper.encode-url encode_url urlencode() {
|
|
sed -e '
|
|
s/%/%25/g
|
|
s/ /%20/g
|
|
s/ /%09/g
|
|
s/!/%21/g
|
|
s/"/%22/g
|
|
s/#/%23/g
|
|
s/\$/%24/g
|
|
s/\&/%26/g
|
|
s/'\''/%27/g
|
|
s/(/%28/g
|
|
s/)/%29/g
|
|
s/\*/%2a/g
|
|
s/+/%2b/g
|
|
s/,/%2c/g
|
|
s/-/%2d/g
|
|
s/\./%2e/g
|
|
s/\//%2f/g
|
|
s/:/%3a/g
|
|
s/;/%3b/g
|
|
s//%3e/g
|
|
s/?/%3f/g
|
|
s/@/%40/g
|
|
s/\[/%5b/g
|
|
s/\\/%5c/g
|
|
s/\]/%5d/g
|
|
s/\^/%5e/g
|
|
s/_/%5f/g
|
|
s/`/%60/g
|
|
s/{/%7b/g
|
|
s/|/%7c/g
|
|
s/}/%7d/g
|
|
s/~/%7e/g
|
|
s/ /%09/g
|
|
'
|
|
}
|
|
|
|
# Check if a version number (semantic format) is greater than the other
|
|
# returns 0 if the first argument is greater or equal than the second.
|
|
function string.version_greatoreq version_greatoreq() {
|
|
[[ "$(printf '%s\n' "$@" | sort -rV | head -n 1)" = "$1" ]] && return 0
|
|
return 1
|
|
}
|
|
|
|
# }}} 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 ${(f)"$(find /sys/devices/ -name net)"}; do
|
|
for dev in ${(f)"$(ls --indicator-style=none $i)"}; do
|
|
# skip the loopback device
|
|
[[ "$dev" =~ "^lo" ]] && continue
|
|
func "found network device: $dev"
|
|
net_devices+=($dev)
|
|
done
|
|
done
|
|
|
|
# return error if no device found
|
|
if [[ ${#net_devices} = 0 ]]; then return 1
|
|
else
|
|
act "${#net_devices} devices found: ${net_devices}"
|
|
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
|
|
act "${#net_ip4_addr} ipv4 connected devices found"
|
|
for c in ${(k)net_ip4_addr}; do
|
|
act " $c ${net_ip4_addr[$c]}"
|
|
done
|
|
|
|
# list ipv6
|
|
act "${#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
|
|
|
|
##########################
|
|
# 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 <<EOF >> $_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 <<EOF >& $_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 <<EOF >& $_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
|