1#!/bin/bash 2set -e 3 4# This script reformats source files using various formatters and linters. 5# 6# Files are changed in-place, so make sure you don't have anything open in an 7# editor, and you may want to commit before formatting in case of awryness. 8# 9# This must be run on a clean repository to succeed 10# 11function display_help() 12{ 13 echo "usage: format-code.sh [-h | --help] [--no-diff]" 14 echo " [<path>]" 15 echo 16 echo "Format and lint a repository." 17 echo 18 echo "Arguments:" 19 echo " --no-diff Don't show final diff output" 20 echo " path Path to git repository (default to pwd)" 21} 22 23eval set -- "$(getopt -o 'h' --long 'help,no-diff' -n 'format-code.sh' -- "$@")" 24while true; do 25 case "$1" in 26 '-h'|'--help') 27 display_help && exit 0 28 ;; 29 30 '--no-diff') 31 OPTION_NO_DIFF=1 32 shift 33 ;; 34 35 '--') 36 shift 37 break 38 ;; 39 40 *) 41 echo "unknown option: $1" 42 display_help && exit 1 43 ;; 44 esac 45done 46 47# Detect tty and set nicer colors. 48if [ -t 1 ]; then 49 BLUE="\e[34m" 50 GREEN="\e[32m" 51 NORMAL="\e[0m" 52 RED="\e[31m" 53 YELLOW="\e[33m" 54else # non-tty, no escapes. 55 BLUE="" 56 GREEN="" 57 NORMAL="" 58 RED="" 59 YELLOW="" 60fi 61 62# Allow called scripts to know which clang format we are using 63export CLANG_FORMAT="clang-format" 64 65# Path to default config files for linters. 66CONFIG_PATH="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/config" 67 68# Find repository root for `pwd` or $1. 69if [ -z "$1" ]; then 70 DIR="$(git rev-parse --show-toplevel || pwd)" 71else 72 DIR="$(git -C "$1" rev-parse --show-toplevel)" 73fi 74if [ ! -e "$DIR/.git" ]; then 75 echo -e "${RED}Error:${NORMAL} Directory ($DIR) does not appear to be a git repository" 76 exit 1 77fi 78 79cd "${DIR}" 80echo -e " ${BLUE}Formatting code under${NORMAL} $DIR" 81 82LINTERS_ALL=( \ 83 commit_gitlint \ 84 commit_spelling \ 85 clang_format \ 86 eslint \ 87 pycodestyle \ 88 shellcheck \ 89 ) 90declare -A LINTER_REQUIRE=() 91declare -A LINTER_CONFIG=() 92declare -A LINTER_IGNORE=() 93LINTERS_ENABLED=() 94declare -A LINTER_TYPES=() 95 96LINTER_REQUIRE+=([commit_spelling]="codespell") 97LINTER_TYPES+=([commit_spelling]="commit") 98function do_commit_spelling() { 99 # Run the codespell with openbmc spcific spellings on the patchset 100 echo -n "openbmc-dictionary - misspelling count >> " 101 sed "s/Signed-off-by.*//" "$@" | \ 102 codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count - 103 104 # Run the codespell with generic dictionary on the patchset 105 echo -n "generic-dictionary - misspelling count >> " 106 sed "s/Signed-off-by.*//" "$@" | \ 107 codespell --builtin clear,rare,en-GB_to_en-US -d --count - 108} 109 110LINTER_REQUIRE+=([commit_gitlint]="gitlint") 111LINTER_TYPES+=([commit_gitlint]="commit") 112function do_commit_gitlint() { 113 gitlint --extra-path "${CONFIG_PATH}/gitlint/" \ 114 --config "${CONFIG_PATH}/.gitlint" 115} 116 117LINTER_REQUIRE+=([eslint]="eslint;.eslintrc.json;${CONFIG_PATH}/eslint-global-config.json") 118LINTER_IGNORE+=([eslint]=".eslintignore") 119LINTER_TYPES+=([eslint]="json") 120function do_eslint() { 121 eslint --no-eslintrc -c "${LINTER_CONFIG[eslint]}" \ 122 --ext .json --format=stylish \ 123 --resolve-plugins-relative-to /usr/local/lib/node_modules \ 124 --no-error-on-unmatched-pattern "$@" 125} 126 127LINTER_REQUIRE+=([pycodestyle]="pycodestyle;setup.cfg") 128LINTER_TYPES+=([pycodestyle]="python") 129function do_pycodestyle() { 130 pycodestyle --show-source "$@" 131} 132 133LINTER_REQUIRE+=([shellcheck]="shellcheck;.shellcheck") 134LINTER_TYPES+=([shellcheck]="bash;sh") 135function do_shellcheck() { 136 shellcheck --color=never -x "$@" 137} 138 139LINTER_REQUIRE+=([clang_format]="clang-format;.clang-format") 140LINTER_IGNORE+=([clang_format]=".clang-ignore;.clang-format-ignore") 141LINTER_TYPES+=([clang_format]="c;cpp") 142do_clang_format() { 143 "${CLANG_FORMAT}" -i "$@" 144} 145 146function get_file_type() 147{ 148 case "$(basename "$1")" in 149 # First to early detect template files. 150 *.in | *.meson) echo "meson-template" && return ;; 151 *.mako | *.mako.*) echo "mako" && return ;; 152 153 *.ac) echo "autoconf" && return ;; 154 *.[ch]) echo "c" && return ;; 155 *.[ch]pp) echo "cpp" && return ;; 156 *.json) echo "json" && return ;; 157 *.md) echo "markdown" && return ;; 158 *.py) echo "python" && return ;; 159 *.yaml | *.yml) echo "yaml" && return ;; 160 161 # Special files. 162 .git/COMMIT_EDITMSG) echo "commit" && return ;; 163 meson.build) echo "meson" && return ;; 164 esac 165 166 case "$(file "$1")" in 167 *Bourne-Again\ shell*) echo "bash" && return ;; 168 *C++\ source*) echo "cpp" && return ;; 169 *C\ source*) echo "c" && return ;; 170 *JSON\ data*) echo "json" && return ;; 171 *POSIX\ shell*) echo "sh" && return ;; 172 *Python\ script*) echo "python" && return ;; 173 *zsh\ shell*) echo "zsh" && return ;; 174 esac 175 176 echo "unknown" 177} 178 179function check_linter() 180{ 181 TITLE="$1" 182 IFS=";" read -r -a ARGS <<< "$2" 183 184 EXE="${ARGS[0]}" 185 if [ ! -x "${EXE}" ]; then 186 if ! which "${EXE}" > /dev/null 2>&1 ; then 187 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find ${EXE}" 188 return 189 fi 190 fi 191 192 CONFIG="${ARGS[1]}" 193 FALLBACK="${ARGS[2]}" 194 195 if [ -n "${CONFIG}" ]; then 196 if [ -e "${CONFIG}" ]; then 197 LINTER_CONFIG+=( [${TITLE}]="${CONFIG}" ) 198 elif [ -n "${FALLBACK}" ] && [ -e "${FALLBACK}" ]; then 199 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find ${CONFIG}; using ${FALLBACK}" 200 LINTER_CONFIG+=( [${TITLE}]="${FALLBACK}" ) 201 else 202 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find config ${CONFIG}" 203 return 204 fi 205 fi 206 207 LINTERS_ENABLED+=( "${TITLE}" ) 208} 209 210# Check for a global .linter-ignore file. 211GLOBAL_IGNORE=("cat") 212if [ -e ".linter-ignore" ]; then 213 GLOBAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter" ".linter-ignore") 214fi 215 216# Find all the files in the git repository and organize by type. 217declare -A FILES=() 218if [ -e .git/COMMIT_EDITMSG ]; then 219 FILES+=([commit]=".git/COMMIT_EDITMSG") 220fi 221while read -r file; do 222 ftype="$(get_file_type "$file")" 223 FILES+=([$ftype]="$(echo -ne "$file;${FILES[$ftype]:-}")") 224done < <(git ls-files | "${GLOBAL_IGNORE[@]}") 225 226# For each linter, check if there are an applicable files and if it can 227# be enabled. 228for op in "${LINTERS_ALL[@]}"; do 229 for ftype in ${LINTER_TYPES[$op]//;/ }; do 230 if [[ -v FILES["$ftype"] ]]; then 231 check_linter "$op" "${LINTER_REQUIRE[${op}]}" 232 break 233 fi 234 done 235done 236 237# Call each linter. 238for op in "${LINTERS_ENABLED[@]}"; do 239 240 # Determine the linter-specific ignore file(s). 241 LOCAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter") 242 if [[ -v LINTER_IGNORE["$op"] ]]; then 243 for ignorefile in ${LINTER_IGNORE["$op"]//;/ } ; do 244 if [ -e "$ignorefile" ]; then 245 LOCAL_IGNORE+=("$ignorefile") 246 fi 247 done 248 fi 249 if [ 1 -eq ${#LOCAL_IGNORE[@]} ]; then 250 LOCAL_IGNORE=("cat") 251 fi 252 253 # Find all the files for this linter, filtering out the ignores. 254 LINTER_FILES=() 255 while read -r file ; do 256 if [ -e "$file" ]; then 257 LINTER_FILES+=("$file") 258 fi 259 done < <(for ftype in ${LINTER_TYPES[$op]//;/ }; do 260 # shellcheck disable=SC2001 261 echo "${FILES["$ftype"]:-}" | sed "s/;/\\n/g" 262 done | "${LOCAL_IGNORE[@]}") 263 264 # Call the linter now with all the files. 265 echo -e " ${BLUE}Running $op${NORMAL}" 266 "do_$op" "${LINTER_FILES[@]}" 267done 268 269# Check for differences. 270if [ -z "$OPTION_NO_DIFF" ]; then 271 echo -e " ${BLUE}Result differences...${NORMAL}" 272 if ! git --no-pager diff --exit-code ; then 273 echo -e "Format: ${RED}FAILED${NORMAL}" 274 exit 1 275 else 276 echo -e "Format: ${GREEN}PASSED${NORMAL}" 277 fi 278fi 279 280# Sometimes your situation is terrible enough that you need the flexibility. 281# For example, phosphor-mboxd. 282for formatter in "format-code.sh" "format-code"; do 283 if [[ -x "${formatter}" ]]; then 284 echo -e " ${BLUE}Calling secondary formatter:${NORMAL} ${formatter}" 285 "./${formatter}" 286 if [ -z "$OPTION_NO_DIFF" ]; then 287 git --no-pager diff --exit-code 288 fi 289 fi 290done 291