#!/bin/bash # parShellCheck.sh - Run parallelized shellcheck, compact + summarize outputs # Version: 0.2.3.sh -- Last update: 2024 11:06:01 CET # Copyright (C) 2023, 2024 Félix Hauri - www.F-Hauri.ch # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation, either version 3 of the License, or (at your option) any later # version. # # This program 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. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program. If not, see . # # Félix Hauri disclaims all copyright interest in the program “parShellCheck.sh" # written by Félix Hauri. # TODO: Some cleanins and review, then rename mostly all variables ### # Browse directory for `*.sh` files, run shellcheck parallelized # Re-order outputs and compute stats. # Syntaxe: # parShellCheck.sh [/path] [find arguments] # Arguments are simply passed to `find` command # parShellCheck.sh ~/scriptdir -mtime -100 ! -name '*[.-_][0-9][0-9].sh' # Some variable could be fixed at command line: # maxProc=3 parShellCheck.sh -name 'foo*.sh' # ansiMark='38;5;205;7;4;1' parShellCheck.sh /path/to/browse # The default find command line will look like this: declare -a findArgs=( "$@" -type f -name '*.sh' ) # -print0 hardcoded declare -i maxProc # There are some default values : " ${maxProc:=0} ${ansiMark:=4;1} " ((maxProc)) || while read -r line _; do case $line in processor) maxProc+=1;; esac done < /proc/cpuinfo declare -r ansiMark maxProc shopt -s extglob declare -ai start elap results order declare -a succeeded failed messages ssmesgs wait4oneTask() { local -i epid _SC _num local -a _errLnCl printf 'Running task: %d\r' ${#running[@]} wait -np epid results[epid]=$? local _i _line _scNum _level _msg _level _cnum _lnum elap[epid]=" ${EPOCHREALTIME/.} - ${start[epid]} " if ((results[epid])); then failed[epid]="${files[epid]}" else succeeded[epid]="${files[epid]}" fi while IFS=: read -ru "${shC_Fds[epid]}" -t.02 _ _lnum _cnum _level _line; do _msg=${_line%\[*} _scNum=${_line#"$_msg"\[} _scNum=${_scNum%\]} _msg="${_msg% }" _level=${_level# } _num="10#${_scNum#SC}" _SC[_num]+=1 _errLnCl[_lnum]="$_cnum:$_num ${_errLnCl[_lnum]}" [[ -v ssmesgs[_num] ]] || printf -v ssmesgs[_num] '%-7s: %s' "${_level//?([:)(])}" "$_msg" done printf -v _scNum '[%d]=%%q ' "${!_errLnCl[@]}" # shellcheck disable=SC2059 # Wanted feature: use variables in the printf format printf -v messages[epid] "${_scNum% }" "${_errLnCl[@]/# }" printf -v _scNum '[%d]+=%%d ' "${!_SC[@]}" # shellcheck disable=SC2059 # Wanted feature: use variables in the printf format printf -v scList[epid] "${_scNum% }" "${_SC[@]}" _i=${shC_Fds[epid]} exec {_i}<&- unset "shC_Fds[epid]" unset "running[$epid]" while [[ -v elap[${order[0]}] ]]; do _i=${order[0]} printf " - %(%a %d %T)T.%06.0f %-36s %4d %12d\n" "${start[_i]:0:-6}" \ "${start[_i]: -6}" "${files[_i]#$rootDir}" \ "${results[_i]}" "${elap[_i]}" order=("${order[@]:1}") done } [[ $1 ]] && rootDir="$1" mapfile -d '' -t fList < <(find "${findArgs[@]}" -print0) printf 'Found: %d files to check with max %d instances.\n' \ ${#fList[@]} $maxProc printf -v tmpLoc /run/user/%d/parShell-%02X%02X%02X $UID ${RANDOM}{,,} printf " %-22s %-36s %4s %12s\n" Started File Rslt 'microseconds' for file in "${fList[@]/%$'\11'*}"; do [[ -r $file ]] || continue exec {shC_Fd}>"${tmpLoc}_${file//\//_}" shellcheck -f gcc "$file" >&$shC_Fd 2>&1 & lpid=$! exec {shC_Fd}<"${tmpLoc}_${file//\//_}" rm "${tmpLoc}_${file//\//_}" shC_Fds[lpid]=$shC_Fd files[lpid]="${file}" start[lpid]=${EPOCHREALTIME/.} running[lpid]='' order+=("$lpid") ((${#running[@]}>=maxProc)) && wait4oneTask done while ((${#running[@]})); do wait4oneTask done printf 'Succeeded: %d.\n%s\nFailed: %d.\n' \ ${#succeeded[@]} "${succeeded[*]}" ${#failed[@]} if ((${#failed[@]})); then declare -ai allSc=() for pid in "${!failed[@]}"; do # shellcheck disable=SC2154 # scList is defined by ``printf -v'' declare -ai tSc="(${scList[pid]})" printf -v scLine '%%dx SC%04d, ' "${!tSc[@]}" # shellcheck disable=SC2059 # Wanted feature: use variables in printf printf -v scLine "$scLine" "${tSc[@]}" printf ' - %s\n' "${failed[pid]#$rootDir}" printf ' Shellcheck msgs: %s.\n' "${scLine%, }" declare -a errLnCl="(${messages[pid]})" mapfile -t wholefile <"${failed[pid]}" for lne in "${!errLnCl[@]}"; do string="${wholefile[lne-1]}" declare -i lcol=-1 declare -ai scByLne=() for colSc in ${errLnCl[lne]}; do IFS=: read -r col scnum <<<"$colSc" [[ $lcol == "$col" ]] || printf -v string '%s\e[%sm%s\e[0m%s' "${string::col-1}" \ "$ansiMark" "${string:col-1:1}" "${string:col}" lcol="$col" scByLne[scnum]+=1 done printf -v scStr '%%dxSC%d, ' "${!scByLne[@]}" # shellcheck disable=SC2059 # Wanted feature again. printf -v scStr "$scStr" "${scByLne[@]}" printf '%8d: %s.\n\t%s\n' "$lne" "${scStr%, }" "${string}" done declare -ai allSc+="(${scList[pid]})" done printf 'Overall stat: %d errors founds but %d differents errors.\n' \ $((${allSc[*]/#/+})) ${#allSc[*]} for i in "${!allSc[@]}"; do printf ' - SC%04d %4d %s\n' "${i}" ${allSc[i]} "${ssmesgs[i]}" done fi