|
| 1 | +#!/usr/bin/env bash |
| 2 | + |
| 3 | +RED="\e[31m" |
| 4 | +GREEN="\e[32m" |
| 5 | +RST_COLOR="\e[0m" |
| 6 | + |
| 7 | +if ! command -v shellcheck &>/dev/null; then |
| 8 | + echo -e "${RED}[ERROR]${RST_COLOR} shellcheck is not installed." |
| 9 | + echo "Run 'sudo ./scripts/install-build-tools.sh' to install shellcheck." |
| 10 | + exit 1 |
| 11 | +fi |
| 12 | + |
| 13 | +# Usage: ./scripts/shellcheck.sh [-e|--exclude-code=CODE] [-S|--severity=LEVEL] [-v|--view] |
| 14 | +IFS='' read -rd '' usage <<'EOF' |
| 15 | +Usage: %s [options] |
| 16 | +
|
| 17 | +Options: |
| 18 | + -h, --help print this help and exit |
| 19 | + -e, --exclude-code exclude specific error code, can be repeated |
| 20 | + -S, --severity=LEVEL set severity level (info, warning, error) |
| 21 | + -v, --view view shellcheck report |
| 22 | +
|
| 23 | + example: ./scripts/shellcheck.sh -e SC1091 -e SC1090 -S warning -v |
| 24 | +EOF |
| 25 | + |
| 26 | +echo; echo "Command line arguments: $0 $*"; echo |
| 27 | + |
| 28 | +SEVERITY= |
| 29 | +EXCLUDE_CODES= |
| 30 | +VIEW="False" |
| 31 | + |
| 32 | +_help= |
| 33 | +_err=0 |
| 34 | +while [[ $# -gt 0 ]]; do |
| 35 | + optarg= |
| 36 | + shft_cnt=1 |
| 37 | + if [[ "$1" = '--' ]]; then |
| 38 | + shift 1 |
| 39 | + break |
| 40 | + elif [[ "$1" =~ [=] ]]; then |
| 41 | + optarg="${1#*=}" |
| 42 | + elif [[ "$1" =~ ^-- && $# -gt 1 && ! "$2" =~ ^- ]]; then |
| 43 | + optarg="$2" |
| 44 | + shft_cnt=2 |
| 45 | + elif [[ "$1" =~ ^-[^-] && $# -gt 1 && ! "$2" =~ ^- ]]; then |
| 46 | + optarg="$2" |
| 47 | + shft_cnt=2 |
| 48 | + elif [[ "$1" =~ ^-[^-] ]]; then |
| 49 | + optarg="${1/??/}" |
| 50 | + fi |
| 51 | + |
| 52 | + case "$1" in |
| 53 | + -S*|--severity*) |
| 54 | + # don't let the user enter -S LEVEL more than once |
| 55 | + # SEV=$(echo "${optarg}" | tr -d '[:space:]') |
| 56 | + if [[ -n "$SEVERITY" ]]; then |
| 57 | + printf "${RED}[Error]${RST_COLOR} Severity level already set to: %s\n" "${SEVERITY}" |
| 58 | + _help=1; _err=1 |
| 59 | + # valid if -S has any of 'info', 'warning', 'error' |
| 60 | + elif [[ "${optarg}" == "info" || "${optarg}" == "warning" || "${optarg}" == "error" ]]; then |
| 61 | + SEVERITY="${optarg}" |
| 62 | + else |
| 63 | + # continue and disregard invalid severity level |
| 64 | + printf "${RED}[Error]${RST_COLOR} severity level: %s\n" "${optarg}" |
| 65 | + _err=1 |
| 66 | + fi |
| 67 | + shift "$shft_cnt" |
| 68 | + ;; |
| 69 | + -e*|--exclude-code*) |
| 70 | + # strip whitespace from optarg |
| 71 | + CODE=$(echo "${optarg}" | tr -d '[:space:]') |
| 72 | + # valid if matching format SC1000-SC9999 |
| 73 | + if [[ "${optarg}" =~ ^SC[0-9]{4}$ ]]; then |
| 74 | + # if empty then populate with just error code, otherwise add pipe before new code for grep later |
| 75 | + if [[ -z "${EXCLUDE_CODES}" ]]; then |
| 76 | + EXCLUDE_CODES+="${CODE}" |
| 77 | + else |
| 78 | + EXCLUDE_CODES+="|${CODE}" |
| 79 | + fi |
| 80 | + else |
| 81 | + # continue just don't save invalid error code |
| 82 | + printf "${RED}[Error]${RST_COLOR} Invalid error code entered: %s\n" "${optarg}" |
| 83 | + _err=1 |
| 84 | + fi |
| 85 | + shift "$shft_cnt" |
| 86 | + ;; |
| 87 | + -v|--view) |
| 88 | + VIEW="True" |
| 89 | + shift "$shft_cnt" |
| 90 | + ;; |
| 91 | + -h|--help) |
| 92 | + _help=1 |
| 93 | + ;; |
| 94 | + *) |
| 95 | + printf "${RED}[Error]${RST_COLOR} Unrecognized option: %s\n" "$1" |
| 96 | + _err=1 |
| 97 | + shift "$shft_cnt" |
| 98 | + ;; |
| 99 | + esac |
| 100 | + |
| 101 | + # exit on help message |
| 102 | + if [[ $_help -eq 1 ]]; then |
| 103 | + printf "%s\n" "$usage" |
| 104 | + exit 0 |
| 105 | + fi |
| 106 | + # continue on invalid arg, let user know but don't exit |
| 107 | + if [[ "$_err" -eq 1 ]]; then |
| 108 | + printf "${RED}[Error]${RST_COLOR} Invalid argument: %s\n" "${optarg}" |
| 109 | + printf "%s\n" "$usage" |
| 110 | + fi |
| 111 | +done |
| 112 | + |
| 113 | +# if severity not set, set it to error as default |
| 114 | +if [[ -z "$SEVERITY" ]]; then |
| 115 | + SEVERITY="error" |
| 116 | +fi |
| 117 | + |
| 118 | +ROOT="$(cd "$(dirname "$0")"/.. && pwd)" |
| 119 | +SHELLCHECK_REPORT="${ROOT}/shellcheck-report.txt" |
| 120 | + |
| 121 | +NUM_CORES=1 |
| 122 | +if [[ "$OSTYPE" == "linux-gnu"* ]]; then |
| 123 | + NUM_CORES=$(grep -c ^processor /proc/cpuinfo) |
| 124 | +elif [[ "$OSTYPE" == "darwin"* ]]; then |
| 125 | + NUM_CORES=$(sysctl -n hw.ncpu) |
| 126 | +fi |
| 127 | + |
| 128 | +# run shellcheck in parallel on all tracked shell scripts |
| 129 | +# |
| 130 | +# checking status of this run will give failure if info/warning/error is found by default |
| 131 | +# |
| 132 | +# determine status by parsing shellcheck report to see if any messages |
| 133 | +# of the severity level or more strict are present to determine failure (true errors) |
| 134 | + |
| 135 | +# check if git is installed |
| 136 | +if command -v git &>/dev/null; then |
| 137 | + echo "Using git ls-files to find shell scripts..."; echo |
| 138 | + git ls-files '*.sh' | xargs -n 1 -P "$NUM_CORES" shellcheck > "$SHELLCHECK_REPORT" |
| 139 | +else |
| 140 | + echo "git is not installed. Using find to compile list of shell scripts..."; echo |
| 141 | + if [[ -z "$EXCLUDE_CODES" ]]; then |
| 142 | + find "$ROOT" -name '*.sh' -print0 | xargs -0 -n 1 -P "$NUM_CORES" shellcheck > "$SHELLCHECK_REPORT" |
| 143 | + fi |
| 144 | +fi |
| 145 | + |
| 146 | +# if shell check report exists to determine if shellcheck run was successful |
| 147 | +if [[ -z "$SHELLCHECK_REPORT" ]]; then |
| 148 | + echo "${RED}[FAIL]${RST_COLOR}Shellcheck report ${SHELLCHECK_REPORT} not found. Exiting..." |
| 149 | + exit 1 |
| 150 | +else |
| 151 | + if [[ ! -s "$SHELLCHECK_REPORT" ]]; then |
| 152 | + echo "Shellcheck report is empty: ${SHELLCHECK_REPORT}" |
| 153 | + echo "Either there are no info/warning/error messages for all shell scripts" |
| 154 | + echo "in the codebase or shellcheck failed to run successfully. Exiting..." |
| 155 | + exit 0 |
| 156 | + fi |
| 157 | +fi |
| 158 | + |
| 159 | +# view non-empty shellcheck report, includes info, warnings, errors |
| 160 | +if [[ "$VIEW" == "True" ]]; then |
| 161 | + echo "Shellcheck report: ${SHELLCHECK_REPORT}" |
| 162 | + cat "$SHELLCHECK_REPORT" |
| 163 | +fi |
| 164 | + |
| 165 | +# detect if fatal errors are in shellcheck report |
| 166 | +echo "Checking for errors in shellcheck report with severity level ${SEVERITY}" |
| 167 | +READABLE_EXCLUDE_CODES=("${EXCLUDE_CODES//|/, }") |
| 168 | +echo "Excluding error codes: ${READABLE_EXCLUDE_CODES[*]}"; echo |
| 169 | + |
| 170 | +# if any messages of severity level or more strict are present, use for grepping report |
| 171 | +case "$SEVERITY" in |
| 172 | + "info") REGEX_SEVERITY="info|warning|error" ;; |
| 173 | + "warning") REGEX_SEVERITY="warning|error" ;; |
| 174 | + *) REGEX_SEVERITY="error" ;; |
| 175 | +esac |
| 176 | + |
| 177 | +# just grep report for severity level if no exclude codes, otherwise pipe and exclude codes from matches |
| 178 | +SEARCH_CMD() { |
| 179 | + if [[ "${#EXCLUDE_CODES[@]}" -eq 0 ]]; then |
| 180 | + grep -E "\(${REGEX_SEVERITY}\):" "$SHELLCHECK_REPORT" |
| 181 | + else |
| 182 | + grep -E "\(${REGEX_SEVERITY}\):" "$SHELLCHECK_REPORT" | grep -v -E "\"${EXCLUDE_CODES}"\" |
| 183 | + fi |
| 184 | +} |
| 185 | + |
| 186 | +# if grep yielded no output then no violations of severity level or higher found (success) |
| 187 | +SEARCH_RESULTS="$(SEARCH_CMD)" |
| 188 | +if [[ -z "$SEARCH_RESULTS" ]]; then |
| 189 | + echo -e "${GREEN}[PASS]${RST_COLOR} Shellcheck did not detect violations scanning with severity level '${SEVERITY}'" |
| 190 | + echo; echo -e "${GREEN}Shellcheck passed.${RST_COLOR} See report: ${SHELLCHECK_REPORT}"; echo |
| 191 | + exit 0 |
| 192 | +else |
| 193 | + COUNT=$(echo "$SEARCH_RESULTS" | wc -l | tr -d '[:space:]') |
| 194 | + echo -e "${RED}[FAIL]${RST_COLOR} Shellcheck found ${RED}${COUNT}${RST_COLOR}"\ |
| 195 | + "unexcused violations scanning with severity level '${SEVERITY}'"; echo |
| 196 | + echo -e "${RED}Shellcheck failed.${RST_COLOR} See report: ${SHELLCHECK_REPORT}"; echo |
| 197 | + exit 1 |
| 198 | +fi |
0 commit comments