#!/bin/bash # Shell connector run background task and install functions for deal with. # (C) 2018-2022 Felix Hauri - felix@f-hauri.ch # Licensed under terms of GPL v3. www.gnu.org # 2020-09-08 use of `local -n`, `coproc` and `exec {FDNAM}<> >(: O)` # Version: 0.99.010 -- Last-Update: 2022 10:38:10 CET # # Sourced, this script will define mostly 3 functions: # "newConnector" connect current bash with any tool like `date -f`, `bc`, ... # "newSqlConnector" connect with a DB client, like mysql, psql, sqlite... # "mySqlReq" to submit request with connect DB, as argument or STDIN # newConnector function will define function prepended by `my[CAP]` for # each backgrounded tasks. Sample: `newConnector /usr/bin/bc "-l"` will # define a myBc function to deal with backgrounded `bc`. # # Run as a script, without argument, this will run a demo, using `sqlite', # `bc`, `date`, run together, to show a recomputed ouptut of `/bin/df`, # store output of `/bin/ls` into a sqlite DB, then request DB for # output content on terminal. # Finally, show date returned by `sqlite`, `date` and `bash`, then # run `ps fw` on current session id, for showing running background # tasks, just before ending thems. # SQLITE=/usr/bin/sqlite3 mkBound() { # Building some uniq 30 random char string from 12 $RANDOM values: # RANDOM is 15 bits. 15 x 2 = 30 bits -> 5 x 6 bits char local -n result=${1:-bound} local _Bash64_refstr _out= _l _i _num printf -v _Bash64_refstr "%s" {0..9} {a..z} {A..Z} @ _ 0 for ((_l=6;_num=(RANDOM<<15|RANDOM),_l--;));do for ((_i=0;_i<30;_i+=6));do _out+=${_Bash64_refstr:(_num>>_i)&63:1} done done printf -v result -- "--%s-%s-%s-%s-%s-%s-" ${_out:0:7} \ ${_out:7:4} ${_out:11:4} ${_out:15:4} \ ${_out:19:4} ${_out:23}; } # instanciator for myXxx() function associated to a started and long-running # instance of the requested command and arguments and initial input data # (the command will be associated to two descriptors XXIN and XXOUT, local to # the instanciated function) newConnector() { local command="$1" cmd=${1##*/} args="$2" check="$3" verif="$4" shift 4 local initfile input local -n cinfd=${cmd^^}IN coutfd=${cmd^^}OUT exec {cinfd}<> <(: =) exec {coutfd}< <(stdbuf -o0 $command $args 2>&1 <&${cinfd}) # coproc stdbuf -o0 $command $args 2>&1 # cinfd=${COPROC[1]} coutfd=$COPROC for initfile ;do cat >&${cinfd} $initfile done source /dev/stdin <<-EOF my${cmd^}() { local -n result=\${2:-${cmd}Out} echo >&\${${cmd^^}IN} "\$1" && read -u \${${cmd^^}OUT} -t 3 result ((\$#>1)) || echo \$result } EOF my${cmd^} $check input [ "$input" = "$verif" ] || printf >&2 "WARNING: Don't match! '%s' <> '%s'.\n" "$verif" "$input" } # SQL Connector # Stronger, because output length is not fixed, could even by empty. # this work with "sqlite", but also with "mysql", "mariadb" or "postgresql" declare bound sqlreqbound SQLIN SQLOUT SQLERR lastsqlread newSqlConnector() { local command="$1" cmd=${1##*/} args check="$3" verif="$4" COPROC IFS=' ' read -a args <<<"$2" local -n _sqlin=SQLIN _sqlout=SQLOUT mkBound bound case $cmd in psql ) sqlreqbound='EXTRACT(\047EPOCH\047 FROM now())' ;; mysql|mariadb ) sqlreqbound='UNIX_TIMESTAMP()' ;; sqlite* ) sqlreqbound='STRFTIME(\047%%s\047,DATETIME(\047now\047))' ;; * ) echo >&2 "WARNING '$cmd' not known as SQL client";; esac exec {SQLERR}<> <(: p) coproc stdbuf -o0 $command "${args[@]}" 2>&$SQLERR _sqlin=${COPROC[1]} _sqlout=$COPROC unset COPROC } # newSqlConnector /usr/bin/sqlite3 $'-separator \t -header /dev/shm/test.sqlite' # newSqlConnector /usr/bin/psql $'-Anh host -F \t --pset=footer=off user' # newSqlConnector /usr/bin/mysql '-f --abort-source-on-error=0 -h host -B -p db' mySqlReq() { # return nothing but set two (or tree if error) variables: `$1`, containing # sql answer and `${1}_h` containing header fields and ${1}_e for # errors if . local -n result=$1 result_h=${1}_h result_e=${1}_e result=() result_h=() result_e=() local line head="" shift ### Send request and request for outputing bound... if (($#)) ;then printf >&$SQLIN '%s;\n' "${*%;}" else local -a _req mapfile -t _req printf >&$SQLIN '%s;\n' "${_req[*]%;}" fi printf >&$SQLIN 'SELECT '"${sqlreqbound}"' AS "%s";\n' $bound read -ru $SQLOUT line if [ "$line" != "$bound" ] ;then IFS=$'\t' read -a result_h <<< "$line"; while read -ru $SQLOUT line && [ "$line" != "$bound" ] ;do result+=("$line") done fi read -ru $SQLOUT line lastsqlread="$line" # then once bound readed without timeout, we could read SQLERR # read -t 0 don't read, but success only if data available if read -u $SQLERR -t 0 ;then while read -ru $SQLERR -t .02 line;do result_e+=("$line") done fi } ### ### End of ``connectors'' definitions ### # # Exit here if script is sourced [ "$0" = "$BASH_SOURCE" ] || { true return 0 } ### ### Demo-sample: define/open 3 connectors to `bc`, `date` and `sqlite`, ### for interactive use from main bash script. ### # Test if sqlite is present if [ ! -x $SQLITE ];then echo >&2 "Tool '$SQLITE' not found (please correct SQLITE var if installed)." exit 1 fi # 1. instanciate a long-running bc command which we will then be # able to interact with with myBc(). # Initialize them by declaring `mil` function, to compute # human readable, returning `A,XX.YYY` where A mean power of 1K # and XX.YYY is value divided by 1024**A. newConnector /usr/bin/bc "-l" 'mil(0)' '0,0' - <<-"EOF" define void mil (s) { if (s==0) { print "0,0\n"; return;}; p=l(s)/l(1024); scale=0; p=p/1; scale=20; print p,",",s/1024^p,"\n"; } EOF # 2. instanciate a long-running date command which we will then be able # to interact with with myDate(), convert date input to UnixTimeStamp. newConnector /bin/date '-f - +%s' @0 0 # 3. instanciate a long-running sqlite command with file based on /dev/shm # Interact with mySqlReq newSqlConnector $SQLITE $'-separator \t -header /dev/shm/test.sqlite' # Database initialisation... cat >&$SQLIN <<-EOSQLInit DROP TABLE IF EXISTS files; CREATE TABLE files (perms, user, date UNSIGNED BIGINT, size UNSIGNED BIGINT,name); EOSQLInit declare -a ABR=(K M G T P) # First: A simple demo using backgrounded `bc -l` { read headline echo "dtot=0;duse=0;" >&$BCIN while read filesystem type size used free prct mpoint;do echo >&$BCIN "dtot+=$used+$free;duse+=$used;" myBc "100*$used/($used+$free)" mpct # Compute % of use myBc "mil($used)" Used # Human readable form myBc "mil($used+$free)" Total printf "%-26s %-8s %7.2f%s %7.2f%s %9s %7.2f%%\n" "$mpoint" "$type" \ ${Used#*,} ${ABR[${Used%,*}]} ${Total#*,} ${ABR[${Total%,*}]} \ $prct $mpct done } < <(LANG=C df -kT) myBc "mil(dtot)" Total myBc "mil(duse)" Used printf "%-36s%7.2f%s %7.2f%s\n" Total \ ${Used#*,} ${ABR[${Used%,*}]} ${Total#*,} ${ABR[${Total%,*}]} # Prepare to work agains files ABR=('' ${ABR[@]}) # lowest is byte, not kb. echo "ftot=0;" >&$BCIN # file size total = 0, but as BC variable # Let play with SQL { read headline while read perm blk user group size month day yot name;do echo >&$BCIN "ftot+=$size" # Increment BC variable ftot. myDate "$month $day $yot" Date # Convert `ls` date format to EPOCH printf >&$SQLIN "INSERT INTO files values('%s','%s',%s,%s,'%s');" \ "$perm" "$user" $Date $size "$name" # DB INSERT done } < <(LANG=C /bin/ls --full-time -Al) mySqlReq myarray "SELECT * from files order by date;" # DB SELECT files by date printf "%-12s %-12s %16s %10s %s\n" "${myarray_h[@]}" # print header for line in "${myarray[@]}";do IFS=$'\t' read perm user date size name <<<"$line" myBc "mil($size)" Size printf " %-11s %-12s %(%F %H:%M)T %7.2f%-2s %s\n" $perm $user $date \ ${Size#*,} ${ABR[${Size%,*}]}b "$name" done myBc "mil(ftot)" Total # Total computed by bc mySqlReq stot 'SELECT SUM(size) FROM files;' # DB SELECT SUM of file size myBc "mil($stot)" STotal # Total computed by DB printf "%18sby bc:%7.2f%-2s, by sql:%7.2f%-2s %s\n" '' \ ${Total#*,} ${ABR[${Total%,*}]}b ${STotal#*,} ${ABR[${STotal%,*}]}b Total myBc ftot bctot # bc variable ftot to bash var bctot (hope BC and SQL match!) [ "$bctot" = "$stot" ] || echo "WARN: BC:'$bctot' and SQL:'$stot' don't match!" myDate now now # Date from backgrounded date task printf "%-14s: %(%a %d %b %T)T\n" \ "Last SQL read" $lastsqlread "now by date" $now "now by bash" -1 mySid=$(ps ho sid $$) ps --sid $mySid fw