#!/bin/bash # This scipt lints files for common errors. # # For go files, it runs gofmt and go vet, and optionally golint and # gocyclo, if they are installed. # # For shell files, it runs shfmt. If you don't have that installed, you can get # it with: # go get -u gopkg.in/mvdan/sh.v1/cmd/shfmt # # With no arguments, it lints the current files staged # for git commit. Or you can pass it explicit filenames # (or directories) and it will lint them. # # To use this script automatically, run: # ln -s ../../bin/lint .git/hooks/pre-commit set -e LINT_IGNORE_FILE=${LINT_IGNORE_FILE:-".lintignore"} IGNORE_LINT_COMMENT= IGNORE_SPELLINGS= PARALLEL= while true; do case "$1" in -nocomment) IGNORE_LINT_COMMENT=1 shift 1 ;; -notestpackage) # NOOP, still accepted for backwards compatibility shift 1 ;; -ignorespelling) IGNORE_SPELLINGS="$2,$IGNORE_SPELLINGS" shift 2 ;; -p) PARALLEL=1 shift 1 ;; *) break ;; esac done spell_check() { local filename="$1" local lint_result=0 # we don't want to spell check tar balls, binaries, Makefile and json files if file "$filename" | grep executable >/dev/null 2>&1; then return $lint_result fi if [[ $filename == *".tar" || $filename == *".gz" || $filename == *".json" || $(basename "$filename") == "Makefile" ]]; then return $lint_result fi # misspell is completely optional. If you don't like it # don't have it installed. if ! type misspell >/dev/null 2>&1; then return $lint_result fi if ! misspell -error -i "$IGNORE_SPELLINGS" "${filename}"; then lint_result=1 fi return $lint_result } lint_go() { local filename="$1" local lint_result=0 if [ -n "$(gofmt -s -l "${filename}")" ]; then lint_result=1 echo "${filename}: run gofmt -s -w ${filename}" fi go tool vet "${filename}" || lint_result=$? # golint is completely optional. If you don't like it # don't have it installed. if type golint >/dev/null 2>&1; then # golint doesn't set an exit code it seems if [ -z "$IGNORE_LINT_COMMENT" ]; then lintoutput=$(golint "${filename}") else lintoutput=$(golint "${filename}" | grep -vE 'comment|dot imports|ALL_CAPS') fi if [ -n "$lintoutput" ]; then lint_result=1 echo "$lintoutput" fi fi # gocyclo is completely optional. If you don't like it # don't have it installed. Also never blocks a commit, # it just warns. if type gocyclo >/dev/null 2>&1; then gocyclo -over 25 "${filename}" | while read -r line; do echo "${filename}": higher than 25 cyclomatic complexity - "${line}" done fi return $lint_result } lint_sh() { local filename="$1" local lint_result=0 if ! diff -u "${filename}" <(shfmt -i 4 "${filename}"); then lint_result=1 echo "${filename}: run shfmt -i 4 -w ${filename}" fi # the shellcheck is completely optional. If you don't like it # don't have it installed. if type shellcheck >/dev/null 2>&1; then shellcheck "${filename}" || lint_result=1 fi return $lint_result } lint_tf() { local filename="$1" local lint_result=0 if ! diff -u <(hclfmt "${filename}") "${filename}"; then lint_result=1 echo "${filename}: run hclfmt -w ${filename}" fi return $lint_result } lint_md() { local filename="$1" local lint_result=0 for i in '=======' '>>>>>>>'; do if grep -q "${i}" "${filename}"; then lint_result=1 echo "${filename}: bad merge/rebase!" fi done return $lint_result } lint_py() { local filename="$1" local lint_result=0 if yapf --diff "${filename}" | grep -qE '^[+-]'; then lint_result=1 echo "${filename}: run yapf --in-place ${filename}" else # Only run flake8 if yapf passes, since they pick up a lot of similar issues flake8 "${filename}" || lint_result=1 fi return $lint_result } lint() { filename="$1" ext="${filename##*\.}" local lint_result=0 # Don't lint deleted files if [ ! -f "$filename" ]; then return fi # Don't lint specific files case "$(basename "${filename}")" in static.go) return ;; coverage.html) return ;; *.pb.go) return ;; esac if [[ "$(file --mime-type "${filename}" | awk '{print $2}')" == "text/x-shellscript" ]]; then ext="sh" fi case "$ext" in go) lint_go "${filename}" || lint_result=1 ;; sh) lint_sh "${filename}" || lint_result=1 ;; tf) lint_tf "${filename}" || lint_result=1 ;; md) lint_md "${filename}" || lint_result=1 ;; py) lint_py "${filename}" || lint_result=1 ;; esac spell_check "${filename}" || lint_result=1 return $lint_result } lint_files() { local lint_result=0 while read -r filename; do lint "${filename}" || lint_result=1 done exit $lint_result } matches_any() { local filename="$1" local patterns="$2" while read -r pattern; do # shellcheck disable=SC2053 # Use the [[ operator without quotes on $pattern # in order to "glob" the provided filename: [[ "$filename" == $pattern ]] && return 0 done <<<"$patterns" return 1 } filter_out() { local patterns_file="$1" if [ -n "$patterns_file" ] && [ -r "$patterns_file" ]; then local patterns patterns=$(sed '/^#.*$/d ; /^\s*$/d' "$patterns_file") # Remove blank lines and comments before we start iterating. [ -n "$DEBUG" ] && echo >&2 "> Filters:" && echo >&2 "$patterns" local filtered_out=() while read -r filename; do matches_any "$filename" "$patterns" && filtered_out+=("$filename") || echo "$filename" done [ -n "$DEBUG" ] && echo >&2 "> Files filtered out (i.e. NOT linted):" && printf >&2 '%s\n' "${filtered_out[@]}" else cat # No patterns provided: simply propagate stdin to stdout. fi } list_files() { if [ $# -gt 0 ]; then find "$@" | grep -vE '(^|/)vendor/' else git ls-files --exclude-standard | grep -vE '(^|/)vendor/' fi } if [ $# = 1 ] && [ -f "$1" ]; then lint "$1" elif [ -n "$PARALLEL" ]; then list_files "$@" | filter_out "$LINT_IGNORE_FILE" | xargs -n1 -P16 "$0" else list_files "$@" | filter_out "$LINT_IGNORE_FILE" | lint_files fi