-
Notifications
You must be signed in to change notification settings - Fork 203
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ci: add shellcheck gh action; fix fatal shellcheck errors
This commit made with the assistance of github copilot Signed-off-by: Morgan Rockett <morgan.rockett@tufts.edu>
Showing
8 changed files
with
358 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,320 @@ | ||
#!/usr/bin/env bash | ||
|
||
START_TIME=$(date "+%s") | ||
ROOT="$(cd "$(dirname "$0")"/.. && pwd)" | ||
SHELLCHECK_REPORT="${ROOT}/shellcheck-report.txt" | ||
SHELLCHECK_REPORT_ALL_MSGS="${ROOT}/shellcheck-report-all-msgs.txt" | ||
|
||
SEVERITY="error" | ||
EXCLUDE_CODES= | ||
VIEW= | ||
COLOR="auto" | ||
|
||
exit_on_error() { | ||
echo; echo -e "${RED}[ERROR]${RST_COLOR} $1" | ||
echo; echo "Exiting..."; echo | ||
exit 1 | ||
} | ||
|
||
check_shellcheck_install() { | ||
if ! command -v shellcheck &>/dev/null; then | ||
exit_on_error "shellcheck is not installed.\n\n\ | ||
Run '# ./scripts/install-build-tools.sh' to install shellcheck." | ||
fi | ||
} | ||
|
||
exit_bad_arg() { | ||
if [[ -z "$1" ]]; then | ||
# must pass argument to function | ||
exit_on_error "No argument passed to exit_bad_arg function" | ||
fi | ||
show_usage | ||
exit_on_error "Invalid argument: $1" | ||
} | ||
|
||
show_usage() { | ||
cat << EOF | ||
Usage: $0 [options] | ||
Options: | ||
-h, --help print this help and exit | ||
-C, --color colorize the output, default is 'auto' | ||
-e, --exclude-code exclude specific error code, can have multiple, default is empty | ||
-S, --severity=LEVEL set severity level (style, info, warning, error), default is 'error' | ||
-v, --view view shellcheck report, default is False | ||
Usage: $ ./scripts/shellcheck.sh [-C|--color=MODE] [-e|--exclude-code=CODE] [-S|--severity=LEVEL] [-v|--view] | ||
example: $ ./scripts/shellcheck.sh -C auto -S info -e SC2034 -e SC2086 | ||
EOF | ||
} | ||
|
||
parse_cli_args() { | ||
echo | ||
while [[ $# -gt 0 ]]; do | ||
optarg= | ||
shft_cnt=1 | ||
# if -- is passed then stop parsing | ||
if [[ "$1" = '--' ]]; then | ||
break | ||
# --option=value | ||
elif [[ "$1" =~ ^-- && ! "$1" =~ ^--$ ]]; then | ||
optarg="${1#*=}"; shft_cnt=1 | ||
# -o=value | ||
elif [[ "$1" =~ ^-- && $# -gt 1 && ! "$2" =~ ^- ]]; then | ||
optarg="$2"; shft_cnt=2 | ||
# -o value | ||
elif [[ "$1" =~ ^-[^-] && $# -gt 1 && ! "$2" =~ ^- ]]; then | ||
optarg="$2"; shft_cnt=2 | ||
# -o | ||
elif [[ "$1" =~ ^-[^-] ]]; then | ||
optarg="${1/??/}" | ||
fi | ||
|
||
case "$1" in | ||
-S*|--severity*) | ||
case "${optarg}" in | ||
style|info|warning|error) | ||
SEVERITY="${optarg}" ;; | ||
*) | ||
exit_bad_arg "$optarg" ;; | ||
esac | ||
shift "$shft_cnt" ;; | ||
-C*|--color*) | ||
case "${optarg}" in | ||
always|auto|never) | ||
COLOR="${optarg}" ;; | ||
*) | ||
exit_bad_arg "$optarg" ;; | ||
esac | ||
shift "$shft_cnt" ;; | ||
-e*|--exclude-code*) | ||
# valid if matching format SC1000-SC9999 | ||
if [[ "${optarg}" =~ ^SC[0-9]{4}$ ]]; then | ||
# if empty, fill with error code, else append comma and new code | ||
if [[ -z "${EXCLUDE_CODES}" ]]; then | ||
EXCLUDE_CODES+="${optarg}" | ||
else | ||
EXCLUDE_CODES+=",${optarg}" | ||
fi | ||
else | ||
exit_bad_arg "$optarg" | ||
fi | ||
shift "$shft_cnt" ;; | ||
-v|--view) | ||
VIEW="True" | ||
shift "$shft_cnt" ;; | ||
-h|--help) | ||
echo; echo "Command line arguments: $0 $*"; echo | ||
show_usage | ||
exit 0 ;; | ||
*) | ||
exit_bad_arg "$optarg" ;; | ||
esac | ||
done | ||
} | ||
|
||
get_num_cores() { | ||
local CORE_COUNT=1 | ||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then | ||
CORE_COUNT=$(grep -c ^processor /proc/cpuinfo) | ||
elif [[ "$OSTYPE" == "darwin"* ]]; then | ||
CORE_COUNT=$(sysctl -n hw.ncpu) | ||
fi | ||
printf "%d\n" "$CORE_COUNT" | ||
} | ||
|
||
run_shellcheck() { | ||
# check if git is installed | ||
if command -v git &>/dev/null; then | ||
if [[ -z "$EXCLUDE_CODES" ]]; then | ||
git ls-files '*.sh' | xargs -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" > "$SHELLCHECK_REPORT_ALL_MSGS" | ||
git ls-files '*.sh' | xargs -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" -S "$SEVERITY" > "$SHELLCHECK_REPORT" | ||
else | ||
git ls-files '*.sh' | xargs -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" > "$SHELLCHECK_REPORT_ALL_MSGS" | ||
git ls-files '*.sh' | xargs -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" -S "$SEVERITY" --exclude="$EXCLUDE_CODES" > "$SHELLCHECK_REPORT" | ||
fi | ||
else | ||
echo "git is not installed. Using find to compile list of shell scripts..."; echo | ||
if [[ -z "$EXCLUDE_CODES" ]]; then | ||
find "$ROOT" -name '*.sh' -print0 | xargs -0 -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" > "$SHELLCHECK_REPORT_ALL_MSGS" | ||
find "$ROOT" -name '*.sh' -print0 | xargs -0 -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" -S "$SEVERITY" > "$SHELLCHECK_REPORT" | ||
else | ||
find "$ROOT" -name '*.sh' -print0 | xargs -0 -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" > "$SHELLCHECK_REPORT_ALL_MSGS" | ||
find "$ROOT" -name '*.sh' -print0 | xargs -0 -n 1 -P "$NUM_CORES" shellcheck -s bash -C"$COLOR" -S "$SEVERITY" --exclude="$EXCLUDE_CODES" > "$SHELLCHECK_REPORT" | ||
fi | ||
fi | ||
} | ||
|
||
view_report() { | ||
if [[ "$#" -ne 1 ]]; then | ||
exit_on_error "view_report function requires 1 argument" | ||
elif [[ ! -f "$1" ]]; then | ||
exit_on_error "view_report function requires a file as an argument" | ||
fi | ||
SHELL_REPORT="$1" | ||
# view non-empty shellcheck report, includes info, warnings, errors | ||
echo | ||
echo -e "----------------------- ${BLUE}Start of Shellcheck report${RST_COLOR} -----------------------" | ||
echo; cat "$SHELL_REPORT"; echo | ||
echo -e "------------------------ ${BLUE}End of Shellcheck report${RST_COLOR} ------------------------" | ||
echo | ||
} | ||
|
||
check_report() { | ||
SHELL_REPORT="$1" | ||
# check if shellcheck file was created | ||
if [[ ! -f "$SHELL_REPORT" ]]; then | ||
exit_on_error "Shellcheck report was not created: ${SHELL_REPORT}" | ||
# if shellcheck report was generated within the last minute then ok | ||
elif [[ -f "$SHELL_REPORT" ]]; then | ||
FILE_MODIFIED=$(date -r "$SHELL_REPORT" "+%s") | ||
TIME_DIFF=$(( FILE_MODIFIED - START_TIME )) | ||
# in testing takes a few seconds at most | ||
if [[ "$TIME_DIFF" -gt 60 ]]; then | ||
exit_on_error "Shellcheck report was not created within the last minute: ${SHELL_REPORT}" | ||
fi | ||
# check if shellcheck report is empty | ||
elif [[ ! -s "$SHELL_REPORT" ]]; then | ||
echo "Shellcheck report is empty: ${SHELL_REPORT}" | ||
echo "Either there are no info/warning/error messages for all shell scripts" | ||
echo "in the codebase or shellcheck failed to run successfully. Exiting..." | ||
exit 0 | ||
fi | ||
|
||
if [[ "$VIEW" == "True" ]]; then | ||
view_report "$SHELL_REPORT" | ||
fi | ||
} | ||
|
||
determine_pass_fail() { | ||
# detect if fatal errors are in shellcheck report | ||
echo -e "Checking for errors in shellcheck report with strictness level '${CYAN}${SEVERITY}${RST_COLOR}'" | ||
if [[ -n "$EXCLUDE_CODES" ]]; then | ||
echo -e "Excluding error codes: '${YELLOW}${EXCLUDE_CODES}${RST_COLOR}'" | ||
fi | ||
|
||
TOTAL_ERRORS=0 | ||
for SEV_LEVEL in "${REGEX_SEVERITY[@]}"; do | ||
# get count of errors of severity level in main shellcheck report not waived version | ||
ERROR_COUNT=$(grep -cE "\s\(${SEV_LEVEL}\):\s" "$SHELLCHECK_REPORT") | ||
FATAL_ERROR_COUNTS["$SEV_LEVEL"]=$(( FATAL_ERROR_COUNTS["$SEV_LEVEL"] + ERROR_COUNT )) | ||
TOTAL_ERRORS=$(( TOTAL_ERRORS + ERROR_COUNT )) | ||
done | ||
|
||
if [[ "$TOTAL_ERRORS" -gt 0 ]]; then | ||
return 1 | ||
fi | ||
return 0 | ||
} | ||
|
||
calc_waived_error_stats() { | ||
# find count of all violations of each severity level ignoring exclude codes, level | ||
for SEV_LEVEL in error warning info style; do | ||
# get count of errors of severity level | ||
ERROR_COUNT=$(grep -cE "\s\(${SEV_LEVEL}\):\s" "$SHELLCHECK_REPORT_ALL_MSGS") | ||
ALL_ERROR_COUNTS["$SEV_LEVEL"]=$(( ALL_ERROR_COUNTS["$SEV_LEVEL"] + ERROR_COUNT )) | ||
done | ||
|
||
# waive less strict levels all at once | ||
for WAIVE_LEV in "${WAIVE_STRICTER[@]}"; do | ||
WAIVED_ERROR_COUNTS["$WAIVE_LEV"]="${ALL_ERROR_COUNTS["$WAIVE_LEV"]}" | ||
done | ||
|
||
if [[ -n "$EXCLUDE_CODES" ]]; then | ||
# Process each severity level in REGEX_SEVERITY | ||
for SEV in "${REGEX_SEVERITY[@]}"; do | ||
for EX_CODE in ${EXCLUDE_CODES//,/ }; do | ||
# Count occurrences of exclude codes at the current severity level | ||
EX_COUNT=$(grep -cE "\s${EX_CODE}\s\(${SEV}\):\s" "$SHELLCHECK_REPORT_ALL_MSGS") | ||
# Add the count to the waived error counts for the current severity | ||
WAIVED_ERROR_COUNTS["$SEV"]=$(( WAIVED_ERROR_COUNTS["$SEV"] + EX_COUNT )) | ||
done | ||
done | ||
fi | ||
} | ||
|
||
print_summary_table() { | ||
echo; echo "-----------------------------------------------------" | ||
echo -e "${BLUE}Severity Level Counts Summary${RST_COLOR}" | ||
echo "-----------------------------------------------------" | ||
printf "%-10s %-10s %-10s %-10s %-10s\n" "Type" "Error" "Warning" "Info" "Style" | ||
echo "-----------------------------------------------------" | ||
printf "%-10s %-10d %-10d %-10d %-10d\n" "Total" "${ALL_ERROR_COUNTS["error"]}" "${ALL_ERROR_COUNTS["warning"]}" "${ALL_ERROR_COUNTS["info"]}" "${ALL_ERROR_COUNTS["style"]}" | ||
printf "%-10s %-10d %-10d %-10d %-10d\n" "Waived" "${WAIVED_ERROR_COUNTS["error"]}" "${WAIVED_ERROR_COUNTS["warning"]}" "${WAIVED_ERROR_COUNTS["info"]}" "${WAIVED_ERROR_COUNTS["style"]}" | ||
printf "%-10s ${RED}%-10d${RST_COLOR} ${RED}%-10d${RST_COLOR} ${RED}%-10d${RST_COLOR} ${RED}%-10d${RST_COLOR}\n" \ | ||
"Fatal" "${FATAL_ERROR_COUNTS["error"]}" "${FATAL_ERROR_COUNTS["warning"]}" "${FATAL_ERROR_COUNTS["info"]}" "${FATAL_ERROR_COUNTS["style"]}" | ||
echo "-----------------------------------------------------"; echo | ||
} | ||
|
||
print_final_message() { | ||
if [[ "$#" -ne 1 ]] || [[ "$1" -lt 0 && "$0" -gt 1 ]]; then | ||
exit_on_error "This function needs a decimal status code as input" | ||
fi | ||
EXIT_STATUS="$1" | ||
# if no errors found, then shellcheck passed | ||
if [[ "$EXIT_STATUS" -eq 0 ]]; then | ||
echo -e "${GREEN}[PASS]${RST_COLOR} Shellcheck did not detect violations" | ||
echo; echo -e "${GREEN}Shellcheck passed.${RST_COLOR} See report: ${SHELLCHECK_REPORT}"; echo | ||
exit "$EXIT_STATUS" | ||
else | ||
echo -e "${RED}[FAIL]${RST_COLOR} Shellcheck found unexcused violations" | ||
exit_on_error "Shellcheck failed. See report: ${SHELLCHECK_REPORT}" | ||
fi | ||
} | ||
|
||
main() { | ||
check_shellcheck_install | ||
parse_cli_args "$@" | ||
|
||
if [[ "$COLOR" != "never" ]]; then | ||
RED="\e[31m" | ||
GREEN="\e[32m" | ||
YELLOW="\e[33m" | ||
BLUE="\e[34m" | ||
CYAN="\e[36m" | ||
RST_COLOR="\e[0m" | ||
else | ||
RED=""; GREEN=""; YELLOW=""; BLUE=""; CYAN=""; RST_COLOR="" | ||
fi | ||
|
||
NUM_CORES=$(get_num_cores) | ||
|
||
run_shellcheck | ||
check_report "$SHELLCHECK_REPORT" | ||
|
||
declare -A FATAL_ERROR_COUNTS; declare -A ALL_ERROR_COUNTS; declare -A WAIVED_ERROR_COUNTS | ||
|
||
FATAL_ERROR_COUNTS=( ["style"]=0 ["info"]=0 ["warning"]=0 ["error"]=0 ) | ||
ALL_ERROR_COUNTS=( ["style"]=0 ["info"]=0 ["warning"]=0 ["error"]=0 ) | ||
WAIVED_ERROR_COUNTS=( ["style"]=0 ["info"]=0 ["warning"]=0 ["error"]=0 ) | ||
|
||
# if any messages of severity level or more strict are present, use for grepping report | ||
case "$SEVERITY" in | ||
"style") REGEX_SEVERITY=("error" "warning" "info" "style") ;; | ||
"info") REGEX_SEVERITY=("error" "warning" "info") ;; | ||
"warning") REGEX_SEVERITY=("error" "warning") ;; | ||
"error") REGEX_SEVERITY=("error") ;; | ||
esac | ||
|
||
determine_pass_fail | ||
STATUS="$?" | ||
|
||
check_report "$SHELLCHECK_REPORT_ALL_MSGS" | ||
|
||
# waive stricter levels if possible | ||
case "$SEVERITY" in | ||
"style") WAIVE_STRICTER=("") ;; | ||
"info") WAIVE_STRICTER=("style") ;; | ||
"warning") WAIVE_STRICTER=("style" "info") ;; | ||
"error") WAIVE_STRICTER=("style" "info" "warning") ;; | ||
esac | ||
|
||
calc_waived_error_stats | ||
|
||
print_summary_table | ||
print_final_message "$STATUS" | ||
} | ||
|
||
main "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters