e7f6beea5ef8a75f12ab3285eee3ead06ac5ac2b
[u/mrichter/AliRoot.git] / PWGPP / scripts / utilities.sh
1 #!/usr/bin/env bash
2 #library of useful PWGPP related bash functions
3 #it REQUIRES BASH 4 !!!!
4 #blame: Mikolaj Krzewicki, mkrzewic@cern.ch
5
6 if [ ${BASH_VERSINFO} -lt 4 ]; then
7   echo "bash version >= 4 needed, you have ${BASH_VERSION}, exiting..."
8   exit 1
9 fi
10
11 PWGPP_runMap="
12 2010 108350 139517
13 2011 140441 170593
14 2012 171590 193766
15 2013 194308 199146
16 2014 202369 206695
17 2015 999999 999999
18 2016 999999 999999
19 "
20
21 parseConfig()
22 {
23   #parse command line arguments, they have to be in the form
24   #  option=value
25   #they are then set in the environment
26   #
27   #optionally a config file can be specified in the arguments:
28   #  configFile=<someFile>
29   #config file sets variables: option=value
30   #command line options override config file
31   #
32   #recommended way of using (at the beginning of your funcion/script):
33   #  if ! parseConfig "${@}"; then return; fi
34   
35   local args=("$@")
36   local opt=""
37   
38   #first check if we will need to decode spaces
39   local encodedSpaces=""
40   for opt in "${args[@]}"; do
41     [[ "${opt}" =~ encodedSpaces=.* ]] \
42       && encodedSpaces=1 \
43       && break
44   done
45
46   #then look for a configFile (if any)
47   for opt in "${args[@]}"; do
48     if [[ ${opt} =~ configFile=.* ]]; then
49       eval "${opt}"
50       [[ ! -f ${configFile} ]] \
51         && echo "configFile ${configFile} not found, exiting..." \
52         && return 1
53       echo "using config file: ${configFile}"
54       source "${configFile}"
55       break
56     fi
57   done
58
59   #then, parse the options as they override the options from configFile
60   for opt in "${args[@]}"; do
61     [[ -n ${encodedSpaces} ]] && opt="$(decSpaces ${opt})"
62     if [[ ! "${opt}" =~ .*=.* ]]; then
63       echo "badly formatted option ${var}, should be: option=value, stopping..."
64       return 1
65     fi
66     local var="${opt%%=*}"
67     local value="${opt#*=}"
68     #echo "${var}=${value}"
69     export ${var}="${value}"
70   done
71   return 0
72 }
73
74 guessRunData()
75 {
76   #guess the period from the path, pick the rightmost one
77   #if $ocdbStorage is set it will be reset to the anchorYear (for MC)
78   period=""
79   runNumber=""
80   year=""
81   pass=""
82   legoTrainRunNumber=""
83   dataType=""
84   originalPass=""
85   originalPeriod=""
86   anchorYear=""
87   shortRunNumber=""
88
89   local oldIFS=${IFS}
90   local IFS="/"
91   [[ -z ${1} ]] && return 1
92   declare -a path=( $1 )
93   IFS="${oldIFS}"
94   local dirDepth=$(( ${#path[*]}-1 ))
95   for ((x=${dirDepth};x>=0;x--)); do
96
97     [[ $((x-1)) -ge 0 ]] && local fieldPrev=${path[$((x-1))]}
98     local field=${path[${x}]}
99     local fieldNext=${path[$((x+1))]}
100
101     [[ ${field} =~ ^[0-9]*$ && ${fieldNext} =~ (.*\.zip$|.*\.root$) ]] && legoTrainRunNumber=${field}
102     [[ -n ${legoTrainRunNumber} && -z ${pass} ]] && pass=${fieldPrev}
103     [[ ${field} =~ ^LHC[0-9][0-9][a-z].*$ ]] && period=${field%_*} && originalPeriod=${field}
104     [[ ${field} =~ ^000[0-9][0-9][0-9][0-9][0-9][0-9]$ ]] && runNumber=${field#000}
105     [[ ${field} =~ ^[0-9][0-9][0-9][0-9][0-9][0-9]$ ]] && shortRunNumber=${field}
106     [[ ${field} =~ ^20[0-9][0-9]$ ]] && year=${field}
107     [[ ${field} =~ ^(^sim$|^data$) ]] && dataType=${field}
108   done
109   originalPass=${pass}
110
111   if [[ ${dataType} =~ sim ]]; then
112     [[ -n ${shortRunNumber} && -z ${runNumber} ]] && runNumber=${shortRunNumber}
113     pass="passMC"
114     originalPass="" #for MC not from lego, the runNumber is identified as lego train number, thus needs to be nulled
115     anchorYear=$(run2year $runNumber)
116     if [[ -z "${anchorYear}" ]]; then
117       echo "WARNING: anchorYear not available for this production: ${originalPeriod}, runNumber: ${runNumber}. Cannot set the OCDB."
118       return 1
119     fi
120     #modify the OCDB: set the year
121     ocdbStorage=$(setYear ${anchorYear} ${ocdbStorage})
122   else
123     ocdbStorage=$(setYear ${year} ${ocdbStorage})
124   fi
125
126   [[ -n ${shortRunNumber} && -z ${runNumber} && -z {dataType} ]] && runNumber=${shortRunNumber}
127   [[ -n ${shortRunNumber} && "${legoTrainRunNumber}" =~ ${shortRunNumber} ]] && legoTrainRunNumber=""
128   [[ -z ${legoTrainRunNumber} && ${dataType} == "data" ]] && pass=${path[$((dirDepth-1))]}
129   [[ -n ${legoTrainRunNumber} ]] && pass+="_lego${legoTrainRunNumber}"
130   
131   #if [[ -z ${dataType} || -z ${year} || -z ${period} || -z ${runNumber}} || -z ${pass} ]];
132   if [[ -z ${runNumber} ]]
133   then
134     #error condition
135     return 1
136   fi
137   
138   #ALL OK
139   return 0
140 }
141
142 guessRunNumber()
143 (
144   #guess the run number from the path, pick the rightmost one
145   if guessRunData "${1}"; then
146     echo ${runNumber}
147     return 0
148   fi
149   return 1
150 )
151
152 guessYear()
153 (
154   #guess the year from the path, pick the rightmost one
155   if guessRunData "${1}"; then
156     echo ${year}
157     return 0
158   fi
159   return 1
160 )
161
162 guessPeriod()
163 (
164   #guess the period from the path, pick the rightmost one
165   if guessRunData "${1}"; then
166     echo ${period}
167     return 0
168   fi
169   return 1
170 )
171
172 setYear()
173 {
174   #set the year
175   #  ${1} - year to be set
176   #  ${2} - where to set the year
177   local year1=$(guessYearFast ${1})
178   local year2=$(guessYearFast ${2})
179   local path=${2}
180   [[ ${year1} -ne ${year2} && -n ${year2} && -n ${year1} ]] && path=${2/\/${year2}\//\/${year1}\/}
181   echo ${path}
182   return 0
183 }
184
185 guessYearFast()
186 {
187   #guess the year from the path, pick the rightmost one
188   local IFS="/"
189   declare -a pathArray=( ${1} )
190   local field
191   local year
192   for field in ${pathArray[@]}; do
193     [[ ${field} =~ ^20[0-9][0-9]$ ]] && year=${field}
194   done
195   echo ${year}
196   return 0
197 }
198
199 run2year()
200 {
201   #for a given run print the year.
202   #the run-year table is ${PWGPP_runMap} (a string)
203   #one line per year, format: year runMin runMax
204   local run=$1
205   [[ -z ${run} ]] && return 1
206   local year=""
207   local runMin=""
208   local runMax=""
209   while read year runMin runMax; do
210     [[ -z ${year} || -z ${runMin} || -z ${runMax} ]] && continue
211     [[ ${run} -ge ${runMin} && ${run} -le ${runMax} ]] && echo ${year} && break
212   done < <(echo "${PWGPP_runMap}")
213   return 0
214 }
215
216 hostInfo(){
217 #
218 # Hallo world -  Print AliRoot/Root/Alien system info
219 #
220
221 #
222 # HOST info
223 #
224     echo --------------------------------------
225         echo 
226         echo HOSTINFO
227         echo 
228         echo HOSTINFO HOSTNAME"      "$HOSTNAME
229         echo HOSTINFO DATE"          "`date`
230         echo HOSTINFO gccpath"       "`which gcc` 
231         echo HOSTINFO gcc version"   "`gcc --version | grep gcc`
232         echo --------------------------------------    
233
234 #
235 # ROOT info
236 #
237         echo --------------------------------------
238         echo
239         echo ROOTINFO
240         echo 
241         echo ROOTINFO ROOT"           "`which root`
242         echo ROOTINFO VERSION"        "`root-config --version`
243         echo 
244         echo --------------------------------------
245
246
247 #
248 # ALIROOT info
249 #
250         echo --------------------------------------
251         echo
252         echo ALIROOTINFO
253         echo 
254         echo ALIROOTINFO ALIROOT"        "`which aliroot`
255         echo ALIROOTINFO VERSION"        "`echo $ALICE_LEVEL`
256         echo ALIROOTINFO TARGET"         "`echo $ALICE_TARGET`
257         echo 
258         echo --------------------------------------
259
260 #
261 # Alien info
262 #
263 #echo --------------------------------------
264 #echo
265 #echo ALIENINFO
266 #for a in `alien --printenv`; do echo ALIENINFO $a; done 
267 #echo
268 #echo --------------------------------------
269
270 #
271 # Local Info
272 #
273         echo PWD `pwd`
274         echo Dir 
275         ls -al
276         echo
277         echo
278         echo
279   
280   return 0
281 }
282
283 summarizeLogs()
284 {
285   #validate and summarize the status of logs
286   #input is a list of logs, or a glob:
287   #example (summarizes logs in current and subdirs):
288   #  summarizeLogs * */*
289   #if no args given, process all files in PWD
290   #exit code 1 if some logs are not validated
291
292   #print a summary of logs
293   local input
294   local file=""
295   declare -A files
296   input=("${@}")
297   [[ -z "${input[*]}" ]] && input=( "${PWD}"/* )
298   
299   #double inclusion protection+make full paths
300   for file in "${input[@]}"; do
301     [[ ! "${file}" =~ ^/ ]] && file="${PWD}/${file}"
302     files["${file}"]="${file}"
303   done
304
305   local logFiles
306   logFiles="\.*log$|^stdout$|^stderr$"
307
308   #check logs
309   local logStatus=0
310   local errorSummary=""
311   local validationStatus=""
312   declare -A coreFiles
313   for file in "${files[@]}"; do
314     [[ ! -f ${file} ]] && continue
315     #keep track of core files for later processing
316     [[ "${file##*/}" =~ ^core$ ]] && coreFiles[${file}]="${file}" && continue
317     [[ ! "${file##*/}" =~ ${logFiles} ]] && continue
318     errorSummary=$(validateLog ${file})
319     validationStatus=$?
320     [[ validationStatus -ne 0 ]] && logStatus=1
321     if [[ ${validationStatus} -eq 0 ]]; then 
322       #in pretend mode randomly report an error in rec.log some cases
323       echo "${file} OK"
324     elif [[ ${validationStatus} -eq 1 ]]; then
325       echo "${file} BAD ${errorSummary}"
326     elif [[ ${validationStatus} -eq 2 ]]; then
327       echo "${file} OK MWAH ${errorSummary}"
328     fi
329   done
330
331   #report core files
332   for x in "${coreFiles[@]}"; do
333     echo ${x}
334     chmod 644 ${x}
335     #gdb --batch --quiet -ex "bt" -ex "quit" aliroot ${x} > stacktrace_${x//\//_}.log
336     gdb --batch --quiet -ex "bt" -ex "quit" aliroot ${x} > stacktrace.log
337     local nLines[2]
338     #nLines=($(wc -l stacktrace_${x//\//_}.log))
339     nLines=($(wc -l stacktrace.log))
340     if [[ ${nLines[0]} -eq 0 ]]; then
341       #rm stacktrace_${x//\//_}.log
342       rm stacktrace.log
343     else
344       logStatus=1
345       echo "${x%/*}/stacktrace.log"
346     fi
347   done
348
349   return ${logStatus}
350 }
351
352 validateLog()
353 {
354   #validate one log file
355   #input is path to log file
356   #output an error summary on stdout
357   #exit code is 0 if validated, 1 otherwise
358   log=${1}
359   errorConditions=(
360             'There was a crash'
361             'floating'
362             'error while loading shared libraries'
363             'std::bad_alloc'
364             's_err_syswatch_'
365             'Thread [0-9]* (Thread'
366             'AliFatal'
367             '\.C.*error:.*\.h: No such file'
368             'segmentation'
369             'Segmentation fault'
370             'Interpreter error recovered'
371             ': command not found'
372             ': comando non trovato'
373             'core dumped'
374   )
375
376   warningConditions=(
377             'This is serious'
378   )
379
380   local logStatus=0
381   local errorSummary=""
382   local warningSummary=""
383   local errorCondition=""
384   for errorCondition in "${errorConditions[@]}"; do
385     local tmp=$(grep -m1 -e "${errorCondition}" ${log})
386     local error=""
387     [[ -n ${tmp} ]] && error=" : ${errorCondition}"
388     errorSummary+=${error}
389   done
390
391   local warningCondition=""
392   for warningCondition in "${warningConditions[@]}"; do
393     local tmp=$(grep -m1 -e "${warningCondition}" ${log})
394     local warning=""
395     [[ -n ${tmp} ]] && warning=" : ${warningCondition}"
396     warningSummary+=${warning}
397   done
398
399   if [[ -n ${errorSummary} ]]; then 
400     echo "${errorSummary}"
401     return 1
402   fi
403
404   if [[ -n ${warningSummary} ]]; then
405     echo "${warningSummary}"
406     return 2
407   fi
408
409   return 0
410 }
411
412 mergeSysLogs()
413 {
414   if [[ $# -lt 2 ]]; then
415     echo 'merge syslogs to an output file'
416     echo 'usage:'
417     echo 'mergeSysLogs outputFile inputFile1 inputFile2 ...'
418     return 0
419   fi
420   local outputFile
421   local inputFiles
422   local i
423   local x
424   local runNumber
425   outputFile=${1}
426   shift
427   inputFiles="$@"
428   i=0
429   if ! ls -1 ${inputFiles} &>/dev/null; then echo "the files dont exist!: ${inputFiles}"; return 1; fi
430   while read x; do 
431     runNumber=$(guessRunNumber ${x})
432     [[ -z ${runNumber} ]] && echo "run number cannot be guessed for ${x}" && continue
433     awk -v run=${runNumber} -v i=${i} 'NR > 1 {print run" "$0} NR==1 && i==0 {print "run/I:"$0}' ${x}
434     (( i++ ))
435   done < <(ls -1 ${inputFiles}) > ${outputFile}
436   return 0
437 }
438
439 stackTraceTree()
440 {
441   if [[ $# -lt 1 ]]; then
442     echo 'make stacktrace processing  in case of standard root crash log'
443     echo 'input is a (list of) text files with the stack trace (either gdb aoutput'
444     echo 'produced with e.g. gdb --batch --quiet -ex "bt" -ex "quit" aliroot core,'
445     echo 'or the root crash log), output is a TTree formatted table.'
446     echo 'example usage:'
447     echo 'benchmark.sh stackTraceTree /foo/*/rec.log'
448     echo 'benchmark.sh stackTraceTree $(cat file.list)'
449     echo 'benchmark.sh stackTraceTree `cat file.list`'
450     return 0
451   fi
452   #cat "${@}" | gawk '
453   gawk '
454        BEGIN { 
455        print "frame/I:method/C:line/C:cpass/I:aliroot/I:file/C";
456                RS="#[0-9]*";
457                aliroot=0;
458                read=1;
459              } 
460       /There was a crash/ {read=1;}
461       /The lines below might hint at the cause of the crash/ {read=0;}
462       read==1 { 
463                if ($3 ~ /Ali*/) aliroot=1; else aliroot=0;
464                gsub("#","",RT); 
465                if ($NF!="" && RT!="" && $3!="") print RT" "$3" "$NF" "0" "aliroot" "FILENAME
466              }
467       ' "${@}" 2>/dev/null
468 }
469
470 plotStackTraceTree()
471 {
472   #plot the stacktrace tree,
473   #first arg    is the text file in the root tree format
474   #second arg   is optional: a plot is written to file instead of screen
475   #third arg    is optional: selection for plotting, default skip G_ stuff
476   local tree=$1
477   local plot=${2:-"crashes.png"}
478   local selection=${3:-'!strstr(method,\"G__\")'}
479   [[ ! -f ${tree} ]] && echo "plotStackTraceTree: no input file given" && return 1
480   aliroot -b <<EOF
481 TTree* t=AliSysInfo::MakeTree("${tree}");
482 TCanvas* canvas = new TCanvas("QA crashes","QA crashes",1);
483 t->Draw("method","${selection}","");
484 canvas->SaveAs("${plot}");
485 .q
486 EOF
487   return 0
488 }
489
490 encSpaces() 
491
492   echo "${1// /±@@±}" 
493 }
494
495 decSpaces() 
496
497   echo "${1//±@@±/ }" 
498 }
499
500 get_realpath() 
501 {
502   if [[ $# -lt 1 ]]; then
503     echo "print the full path of a file or directory, like \"readlink -f\" on linux"
504     echo "Usage:"
505     echo "  get_realpath <someFileOrDir>"
506     return 0
507   fi
508   if [[ -f "$1" ]]
509   then
510     # file *must* exist
511     if cd "$(echo "${1%/*}")" &>/dev/null
512     then
513       # file *may* not be local
514       # exception is ./file.ext
515       # try 'cd .; cd -;' *works!*
516       local tmppwd="$PWD"
517       cd - &>/dev/null
518     else
519       # file *must* be local
520       local tmppwd="$PWD"
521     fi
522   elif [[ -d "$1" ]]; then
523     if cd "$1" &>/dev/null; then
524       local tmppwd="$PWD"
525       cd - &>/dev/null
526       echo "$tmppwd"
527       return 0
528     else
529       return 1
530     fi
531   else
532     # file *cannot* exist
533     return 1 # failure
534   fi
535   # reassemble realpath
536   echo "$tmppwd"/"${1##*/}"
537   return 0 # success
538 }
539
540 printLogStatistics()
541 {
542   #this function processes the summary logs and prints some stats
543   #relies on the summary log format produced by summarizeLogs()
544   # - how many checked logs in total
545   # - number of each type of problem
546   # example usage:
547   #   printLogStatistics */*.log
548   [[ ! -f $1 ]] && return 1
549   echo "log statistics from: ${1%/*}"
550   #cat "${@}" | awk '
551   awk '
552   BEGIN {nOK=0; nCores=0; nStackTraces=0;}
553   /\/core/ {nCores++}
554   /\/stacktrace.log/ {nStackTraces++}
555   /OK/ {nOK++; nLogs++;}
556   /BAD/ {
557     nLogs++
558     err=""
559     write=0
560     for (i=3; i<=NF; i++)
561     { 
562       if ($i ~ /^\:$/) 
563         write=1
564       else
565         write=0
566
567       if (write==0)
568       {
569         if (err=="") err=$i
570         else err=(err FS $i)
571       }
572
573       if (err != "" && (write==1 || i==NF))
574       {
575         sumBAD[err]++
576         err=""
577       }
578     }
579   } 
580   END {
581     print ("number of succesful jobs: " nOK" out of "nLogs )
582     for (key in sumBAD)
583     {
584       print key": "sumBAD[key]
585     }
586     if (nCores>0) print "core files: "nCores", stack traces: "nStackTraces 
587   }
588   ' "${@}"
589 }
590
591 createUniquePID()
592 {
593   #create a unique ID for jobs running in parallel
594   #consists of the ip address of the default network interface, PID,
595   #if an argument is given, append it (e.g. a production ID)
596   #the fields are space separated with a tag for easy parsing
597   #spaces in the productionID will be encoded using encSpaces()
598   local productionID=""
599   [[ -n "${1}" ]] && productionID=$(encSpaces "${1}")
600   local defaultIP=$(/sbin/route | awk '$1=="default" {print $8}' | xargs /sbin/ifconfig | awk '/inet / {print $2}' | sed 's/.*\([0-9]?\.[0-9]?\.[0-9]?\.[0-9]?\)/$1/')
601   local id="ip:${defaultIP} pid:${BASHPID}"
602   [[ -n "${productionID}" ]] && id+=" prod:${productionID}"
603   echo "${id}"
604 }
605
606 copyFileToLocal()
607 (
608   #copies a single file to a local destination: the file may either come from
609   #a local filesystem or from a remote location (whose protocol must be
610   #supported)
611   #copy is "robust" and it is repeated some times in case of failure before
612   #giving up (1 is returned in that case)
613   #origin: Dario Berzano, dario.berzano@cern.ch
614   src="$1"
615   dst="$2"
616   ok=0
617   [[ -z "${maxCopyTries}" ]] && maxCopyTries=10
618
619   proto="${src%%://*}"
620
621   echo "copy file to local dest started: $src -> $dst"
622
623   for (( i=1 ; i<=maxCopyTries ; i++ )) ; do
624
625     echo "...attempt $i of $maxCopyTries"
626     rm -f "$dst"
627
628     if [[ "$proto" == "$src" ]]; then
629       cp "$src" "$dst"
630     else
631       case "$proto" in
632         root)
633           xrdcp -f "$src" "$dst"
634         ;;
635         http)
636           curl -L "$src" -O "$dst"
637         ;;
638         *)
639           echo "protocol not supported: $proto"
640           return 2
641         ;;
642       esac
643     fi
644
645     if [ $? == 0 ] ; then
646       ok=1
647       break
648     fi
649
650   done
651
652   if [[ "$ok" == 1 ]] ; then
653     echo "copy file to local dest OK after $i attempt(s): $src -> $dst"
654     return 0
655   fi
656
657   echo "copy file to local dest FAILED after $maxCopyTries attempt(s): $src -> $dst"
658   return 1
659 )
660
661 paranoidCp()
662 (
663   #recursively copy files and directories
664   #if target is a directory - it must exist!
665   #to avoid using find and the like as they kill
666   #the performance on some cluster file systems
667   #does not copy links to avoid problems
668   sourceFiles=("${@}")
669   destination="${sourceFiles[@]:(-1)}" #last element
670   unset sourceFiles[${#sourceFiles[@]}-1] #remove last element (dst)
671   #[[ ! -f "${destination}" ]] 
672   for src in "${sourceFiles[@]}"; do
673     if [[ -f "${src}" && ! -h  "${src}" ]]; then
674       paranoidCopyFile "${src}" "${destination}"
675     elif [[ -d "${src}" && ! -h "${src}" ]]; then
676       src="${src%/}"
677       dst="${destination}/${src##*/}"
678       mkdir -p "${dst}"
679       paranoidCp "${src}"/* "${dst}"
680     fi
681   done
682 )
683
684 paranoidCopyFile()
685 (
686   #copy a single file to a target in an existing dir
687   #repeat a few times if copy fails
688   #returns 1 on failure, 0 on success
689   src=$(get_realpath "${1}")
690   dst=$(get_realpath "${2}")
691   [[ -d "${dst}" ]] && dst="${dst}/${src##*/}"
692   #some sanity check
693   [[ -z "${src}" ]] && echo "$1 does not exist" && return 1
694   [[ -z "${dst}" ]] && echo "$2 does not exist" && return 1
695   #check if we are not trying to copy to the same file
696   [[ "${src}" == "${dst}" ]] && echo "$dst==$src, not copying" && return 0
697   ok=0
698   [[ -z "${maxCopyTries}" ]] && maxCopyTries=10
699
700   echo "paranoid copy started: $src -> $dst"
701   for (( i=1 ; i<=maxCopyTries ; i++ )) ; do
702
703     echo "...attempt $i of $maxCopyTries"
704     rm -f "$dst"
705     cp "$src" "$dst"
706
707     cmp -s "$src" "$dst"
708     if [ $? == 0 ] ; then
709       ok=1
710       break
711     fi
712
713   done
714
715   if [[ "$ok" == 1 ]] ; then
716     echo "paranoid copy OK after $i attempt(s): $src -> $dst"
717     return 0
718   fi
719
720   echo "paranoid copy FAILED after $maxCopyTries attempt(s): $src -> $dst"
721   return 1
722 )
723
724 generPWD(){ 
725   #
726   # generate semirandom pwd using 2 keys 
727   # Example usage:
728   # generPWD  myserviceaccount10 key11
729   key0=$1
730   key1=$2
731   heslo0=`md5sum <<< "$key0 $key1" | cut -c 1-16`
732   heslo=`echo $heslo0 | cut -c 1-8| awk '{print toupper($0)}'`
733   heslo=$heslo`echo $heslo0 | cut -c 8-15| awk '{print tolower($0)}'`%
734   echo $heslo;
735 }
736
737 #this makes debugging easier:
738 #executes the command given as an argument in this environment
739 #use case:
740 #  bashdb utilities.sh summarizeLogs * */*
741 [[ $# != 0 ]] && eval "$@"