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] [--list-tools]" 14 echo " [--disable <tool>] [--enable <tool>] [<path>]" 15 echo 16 echo "Format and lint a repository." 17 echo 18 echo "Arguments:" 19 echo " --list-tools Display available linters and formatters" 20 echo " --no-diff Don't show final diff output" 21 echo " --disable <tool> Disable linter" 22 echo " --enable <tool> Enable only specific linters" 23 echo " --allow-missing Run even if linters are not all present" 24 echo " path Path to git repository (default to pwd)" 25} 26 27LINTERS_ALL=( \ 28 markdownlint \ 29 ) 30LINTERS_DISABLED=() 31LINTERS_ENABLED=() 32declare -A LINTERS_FAILED=() 33 34eval set -- "$(getopt -o 'h' --long 'help,list-tools,no-diff,disable:,enable:,allow-missing' -n 'format-code.sh' -- "$@")" 35while true; do 36 case "$1" in 37 '-h'|'--help') 38 display_help && exit 0 39 ;; 40 41 '--list-tools') 42 echo "Available tools:" 43 for t in "${LINTERS_ALL[@]}"; do 44 echo " $t" 45 done 46 exit 0 47 ;; 48 49 '--no-diff') 50 OPTION_NO_DIFF=1 51 shift 52 ;; 53 54 '--disable') 55 LINTERS_DISABLED+=("$2") 56 shift && shift 57 ;; 58 59 '--enable') 60 LINTERS_ENABLED+=("$2") 61 shift && shift 62 ;; 63 64 '--allow-missing') 65 ALLOW_MISSING=yes 66 shift 67 ;; 68 69 '--') 70 shift 71 break 72 ;; 73 74 *) 75 echo "unknown option: $1" 76 display_help && exit 1 77 ;; 78 esac 79done 80 81# Detect tty and set nicer colors. 82if [ -t 1 ]; then 83 BLUE="\e[34m" 84 GREEN="\e[32m" 85 NORMAL="\e[0m" 86 RED="\e[31m" 87 YELLOW="\e[33m" 88else # non-tty, no escapes. 89 BLUE="" 90 GREEN="" 91 NORMAL="" 92 RED="" 93 YELLOW="" 94fi 95 96# Allow called scripts to know which clang format we are using 97export CLANG_FORMAT="true" 98 99# Path to default config files for linters. 100CONFIG_PATH="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/config" 101 102# Find repository root for `pwd` or $1. 103if [ -z "$1" ]; then 104 DIR="$(git rev-parse --show-toplevel || pwd)" 105else 106 DIR="$(git -C "$1" rev-parse --show-toplevel)" 107fi 108if [ ! -e "$DIR/.git" ]; then 109 echo -e "${RED}Error:${NORMAL} Directory ($DIR) does not appear to be a git repository" 110 exit 1 111fi 112 113cd "${DIR}" 114echo -e " ${BLUE}Formatting code under${NORMAL} $DIR" 115 116# Config hashes: 117# LINTER_REQUIRE - The requirements to run a linter, semi-colon separated. 118# 1. Executable. 119# 2. [optional] Configuration file. 120# 3. [optional] Global fallback configuration file. 121# 122# LINTER_IGNORE - An optional set of semi-colon separated ignore-files 123# specific to the linter. 124# 125# LINTER_TYPES - The file types supported by the linter, semi-colon separated. 126# 127# LINTER_CONFIG - The config (from LINTER_REQUIRE) chosen for the repository. 128# 129declare -A LINTER_REQUIRE=() 130declare -A LINTER_IGNORE=() 131declare -A LINTER_TYPES=() 132declare -A LINTER_CONFIG=() 133 134LINTER_REQUIRE+=([commit_spelling]="codespell") 135LINTER_TYPES+=([commit_spelling]="commit") 136function do_commit_spelling() { 137 # Run the codespell with openbmc spcific spellings on the patchset 138 echo -n "openbmc-dictionary - misspelling count >> " 139 sed "s/Signed-off-by.*//" "$@" | \ 140 codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count - 141 142 # Run the codespell with generic dictionary on the patchset 143 echo -n "generic-dictionary - misspelling count >> " 144 sed "s/Signed-off-by.*//" "$@" | \ 145 codespell --builtin clear,rare,en-GB_to_en-US -d --count - 146} 147 148LINTER_REQUIRE+=([commit_gitlint]="gitlint") 149LINTER_TYPES+=([commit_gitlint]="commit") 150function do_commit_gitlint() { 151 gitlint --extra-path "${CONFIG_PATH}/gitlint/" \ 152 --config "${CONFIG_PATH}/.gitlint" 153} 154 155# We need different function style for bash/zsh vs plain sh, so beautysh is 156# split into two linters. "function foo()" is not traditionally accepted 157# POSIX-shell syntax, so shellcheck barfs on it. 158LINTER_REQUIRE+=([beautysh]="beautysh") 159LINTER_IGNORE+=([beautysh]=".beautysh-ignore") 160LINTER_TYPES+=([beautysh]="bash;zsh") 161function do_beautysh() { 162 beautysh --force-function-style fnpar "$@" 163} 164LINTER_REQUIRE+=([beautysh_sh]="beautysh") 165LINTER_IGNORE+=([beautysh_sh]=".beautysh-ignore") 166LINTER_TYPES+=([beautysh_sh]="sh") 167function do_beautysh_sh() { 168 beautysh --force-function-style paronly "$@" 169} 170 171LINTER_REQUIRE+=([black]="black") 172LINTER_TYPES+=([black]="python") 173function do_black() { 174 black -l 79 "$@" 175} 176 177LINTER_REQUIRE+=([eslint]="eslint;.eslintrc.json;${CONFIG_PATH}/eslint-global-config.json") 178LINTER_IGNORE+=([eslint]=".eslintignore") 179LINTER_TYPES+=([eslint]="json") 180function do_eslint() { 181 eslint --no-eslintrc -c "${LINTER_CONFIG[eslint]}" \ 182 --ext .json --format=stylish \ 183 --resolve-plugins-relative-to /usr/local/lib/node_modules \ 184 --no-error-on-unmatched-pattern "$@" 185} 186 187LINTER_REQUIRE+=([flake8]="flake8") 188LINTER_IGNORE+=([flake8]=".flake8-ignore") 189LINTER_TYPES+=([flake8]="python") 190function do_flake8() { 191 flake8 --show-source --extend-ignore=E203,E501 "$@" 192 # We disable E203 and E501 because 'black' is handling these and they 193 # disagree on best practices. 194} 195 196LINTER_REQUIRE+=([isort]="isort") 197LINTER_TYPES+=([isort]="python") 198function do_isort() { 199 isort --profile black "$@" 200} 201 202LINTER_REQUIRE+=([markdownlint]="markdownlint;.markdownlint.yaml;${CONFIG_PATH}/markdownlint.yaml") 203LINTER_IGNORE+=([markdownlint]=".markdownlint-ignore") 204LINTER_TYPES+=([markdownlint]="markdown") 205function do_markdownlint() { 206 markdownlint --config "${LINTER_CONFIG[markdownlint]}" \ 207 --disable line-length -- "$@" || \ 208 echo -e " ${YELLOW}Failed markdownlint; temporarily ignoring." 209 # We disable line-length because prettier should handle prose wrap for us. 210} 211 212LINTER_REQUIRE+=([prettier]="prettier;.prettierrc.yaml;${CONFIG_PATH}/prettierrc.yaml") 213LINTER_IGNORE+=([prettier]=".prettierignore") 214LINTER_TYPES+=([prettier]="json;markdown;yaml") 215function do_prettier() { 216 prettier --config "${LINTER_CONFIG[prettier]}" --write "$@" 217} 218 219LINTER_REQUIRE+=([shellcheck]="shellcheck") 220LINTER_IGNORE+=([shellcheck]=".shellcheck-ignore") 221LINTER_TYPES+=([shellcheck]="bash;sh") 222function do_shellcheck() { 223 shellcheck --color=never -x "$@" 224} 225 226LINTER_REQUIRE+=([clang_format]="clang-format;.clang-format") 227LINTER_IGNORE+=([clang_format]=".clang-ignore;.clang-format-ignore") 228LINTER_TYPES+=([clang_format]="c;cpp") 229function do_clang_format() { 230 "${CLANG_FORMAT}" -i "$@" 231} 232 233function get_file_type() 234{ 235 case "$(basename "$1")" in 236 # First to early detect template files. 237 *.in | *.meson) echo "meson-template" && return ;; 238 *.mako | *.mako.*) echo "mako" && return ;; 239 240 *.ac) echo "autoconf" && return ;; 241 *.[ch]) echo "c" && return ;; 242 *.[ch]pp) echo "cpp" && return ;; 243 *.json) echo "json" && return ;; 244 *.md) echo "markdown" && return ;; 245 *.py) echo "python" && return ;; 246 *.tcl) echo "tcl" && return ;; 247 *.yaml | *.yml) echo "yaml" && return ;; 248 249 # Special files. 250 .git/COMMIT_EDITMSG) echo "commit" && return ;; 251 meson.build) echo "meson" && return ;; 252 esac 253 254 case "$(file "$1")" in 255 *Bourne-Again\ shell*) echo "bash" && return ;; 256 *C++\ source*) echo "cpp" && return ;; 257 *C\ source*) echo "c" && return ;; 258 *JSON\ data*) echo "json" && return ;; 259 *POSIX\ shell*) echo "sh" && return ;; 260 *Python\ script*) echo "python" && return ;; 261 *python3\ script*) echo "python" && return ;; 262 *zsh\ shell*) echo "zsh" && return ;; 263 esac 264 265 echo "unknown" 266} 267 268LINTERS_AVAILABLE=() 269function check_linter() 270{ 271 TITLE="$1" 272 IFS=";" read -r -a ARGS <<< "$2" 273 274 if [[ "${LINTERS_DISABLED[*]}" =~ $1 ]]; then 275 return 276 fi 277 278 if [ 0 -ne "${#LINTERS_ENABLED[@]}" ]; then 279 if ! [[ "${LINTERS_ENABLED[*]}" =~ $1 ]]; then 280 return 281 fi 282 fi 283 284 EXE="${ARGS[0]}" 285 if [ ! -x "${EXE}" ]; then 286 if ! which "${EXE}" > /dev/null 2>&1 ; then 287 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find ${EXE}" 288 if [ -z "$ALLOW_MISSING" ]; then 289 exit 1 290 fi 291 return 292 fi 293 fi 294 295 CONFIG="${ARGS[1]}" 296 FALLBACK="${ARGS[2]}" 297 298 if [ -n "${CONFIG}" ]; then 299 if [ -e "${CONFIG}" ]; then 300 LINTER_CONFIG+=( [${TITLE}]="${CONFIG}" ) 301 elif [ -n "${FALLBACK}" ] && [ -e "${FALLBACK}" ]; then 302 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find ${CONFIG}; using ${FALLBACK}" 303 LINTER_CONFIG+=( [${TITLE}]="${FALLBACK}" ) 304 else 305 echo -e " ${YELLOW}${TITLE}:${NORMAL} cannot find config ${CONFIG}" 306 return 307 fi 308 fi 309 310 LINTERS_AVAILABLE+=( "${TITLE}" ) 311} 312 313# Check for a global .linter-ignore file. 314GLOBAL_IGNORE=("cat") 315if [ -e ".linter-ignore" ]; then 316 GLOBAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter" ".linter-ignore") 317fi 318 319# Find all the files in the git repository and organize by type. 320declare -A FILES=() 321if [ -e .git/COMMIT_EDITMSG ]; then 322 FILES+=([commit]=".git/COMMIT_EDITMSG") 323fi 324while read -r file; do 325 ftype="$(get_file_type "$file")" 326 FILES+=([$ftype]="$(echo -ne "$file;${FILES[$ftype]:-}")") 327done < <(git ls-files | xargs realpath --relative-base=. | "${GLOBAL_IGNORE[@]}") 328 329# For each linter, check if there are an applicable files and if it can 330# be enabled. 331for op in "${LINTERS_ALL[@]}"; do 332 for ftype in ${LINTER_TYPES[$op]//;/ }; do 333 if [[ -v FILES["$ftype"] ]]; then 334 check_linter "$op" "${LINTER_REQUIRE[${op}]}" 335 break 336 fi 337 done 338done 339 340# Call each linter. 341for op in "${LINTERS_AVAILABLE[@]}"; do 342 343 # Determine the linter-specific ignore file(s). 344 LOCAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter") 345 if [[ -v LINTER_IGNORE["$op"] ]]; then 346 for ignorefile in ${LINTER_IGNORE["$op"]//;/ } ; do 347 if [ -e "$ignorefile" ]; then 348 LOCAL_IGNORE+=("$ignorefile") 349 fi 350 done 351 fi 352 if [ 1 -eq ${#LOCAL_IGNORE[@]} ]; then 353 LOCAL_IGNORE=("cat") 354 fi 355 356 # Find all the files for this linter, filtering out the ignores. 357 LINTER_FILES=() 358 while read -r file ; do 359 if [ -e "$file" ]; then 360 LINTER_FILES+=("$file") 361 fi 362 done < <(for ftype in ${LINTER_TYPES[$op]//;/ }; do 363 # shellcheck disable=SC2001 364 echo "${FILES["$ftype"]:-}" | sed "s/;/\\n/g" 365 done | "${LOCAL_IGNORE[@]}") 366 367 # Call the linter now with all the files. 368 if [ 0 -ne ${#LINTER_FILES[@]} ]; then 369 echo -e " ${BLUE}Running $op${NORMAL}" 370 if ! "do_$op" "${LINTER_FILES[@]}" ; then 371 LINTERS_FAILED+=([$op]=1) 372 fi 373 else 374 echo -e " ${YELLOW}${op}:${NORMAL} all applicable files are on ignore-lists" 375 fi 376done 377 378# Check for failing linters. 379if [ 0 -ne ${#LINTERS_FAILED[@]} ]; then 380 for op in "${!LINTERS_FAILED[@]}"; do 381 echo -e "$op: ${RED}FAILED${NORMAL}" 382 done 383 exit 1 384fi 385 386# Check for differences. 387if [ -z "$OPTION_NO_DIFF" ]; then 388 echo -e " ${BLUE}Result differences...${NORMAL}" 389 if ! git --no-pager diff --exit-code ; then 390 echo -e "Format: ${RED}FAILED${NORMAL}" 391 exit 1 392 else 393 echo -e "Format: ${GREEN}PASSED${NORMAL}" 394 fi 395fi 396 397# Sometimes your situation is terrible enough that you need the flexibility. 398# For example, phosphor-mboxd. 399for formatter in "format-code.sh" "format-code"; do 400 if [[ -x "${formatter}" ]]; then 401 echo -e " ${BLUE}Calling secondary formatter:${NORMAL} ${formatter}" 402 "./${formatter}" 403 if [ -z "$OPTION_NO_DIFF" ]; then 404 git --no-pager diff --exit-code 405 fi 406 fi 407done 408