#!/usr/bin/env bash set -eo pipefail ####################################################################### # Init arguments parser # Arguments: # script_dir - absolute path to hook dir location ####################################################################### function common::initialize { local -r script_dir=$1 # source getopt function # shellcheck source=../lib_getopt . "$script_dir/../lib_getopt" } ####################################################################### # Parse args and filenames passed to script and populate respective # global variables with appropriate values # Globals (init and populate): # ARGS (array) arguments that configure wrapped tool behavior # HOOK_CONFIG (array) arguments that configure hook behavior # FILES (array) filenames to check # Arguments: # $@ (array) all specified in `hooks.[].args` in # `.pre-commit-config.yaml` and filenames. ####################################################################### function common::parse_cmdline { # common global arrays. # Populated via `common::parse_cmdline` and can be used inside hooks' functions declare -g -a ARGS=() HOOK_CONFIG=() FILES=() ENV_PASSTHRU=() local argv argv=$(getopt -o a:,h: --long args:,hook-config:,env-passthrough: -- "$@") || return eval "set -- $argv" for argv; do case $argv in -a | --args) shift ARGS+=("$1") shift ;; -h | --hook-config) shift HOOK_CONFIG+=("$1;") shift ;; --env-passthrough) shift ENV_PASSTHRU+=("$1") shift ;; --) shift # shellcheck disable=SC2034 # Variable is used FILES=("$@") break ;; esac done } # Iterates through each value in ${ENV_PASSTHRU} and replaces the # placeholder value __ENV___ with the value of the environment # variable # # Example usage: # - FOO=one # - __ENV_FOO__ becomes "one" function common::passthrough_env_vars { local var for var in "${ENV_PASSTHRU[@]}"; do # shellcheck disable=SC2178 ARGS=${ARGS[*]/__ENV_${var}__/${!var}} done } ####################################################################### # This is a workaround to improve performance when all files are passed # See: https://github.com/antonbabenko/pre-commit-terraform/issues/309 # Arguments: # hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file # files (array) filenames to check # Outputs: # Return 0 if `-a|--all` arg was passed to `pre-commit` ####################################################################### function common::is_hook_run_on_whole_repo { local -r hook_id="$1" shift 1 local -a -r files=("$@") # get directory containing `.pre-commit-hooks.yaml` file local -r root_config_dir="$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")" # get included and excluded files from .pre-commit-hooks.yaml file local -r hook_config_block=$(sed -n "/^- id: $hook_id$/,/^$/p" "$root_config_dir/.pre-commit-hooks.yaml") local -r included_files=$(awk '$1 == "files:" {print $2; exit}' <<< "$hook_config_block") local -r excluded_files=$(awk '$1 == "exclude:" {print $2; exit}' <<< "$hook_config_block") # sorted string with the files passed to the hook by pre-commit local -r files_to_check=$(printf '%s\n' "${files[@]}" | sort | tr '\n' ' ') # git ls-files sorted string local all_files_that_can_be_checked if [ -z "$excluded_files" ]; then all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | tr '\n' ' ') else all_files_that_can_be_checked=$(git ls-files | sort | grep -e "$included_files" | grep -v -e "$excluded_files" | tr '\n' ' ') fi if [ "$files_to_check" == "$all_files_that_can_be_checked" ]; then return 0 else return 1 fi } ####################################################################### # Hook execution boilerplate logic which is common to hooks, that run # on per dir basis. # 1. Because hook runs on whole dir, reduce file paths to uniq dir paths # 2. Run for each dir `per_dir_hook_unique_part`, on all paths # 2.1. If at least 1 check failed - change exit code to non-zero # 3. Complete hook execution and return exit code # Arguments: # args (string with array) arguments that configure wrapped tool behavior # hook_id (string) hook ID, see `- id` for details in .pre-commit-hooks.yaml file # files (array) filenames to check ####################################################################### function common::per_dir_hook { local -r args="$1" local -r hook_id="$2" shift 2 local -a -r files=("$@") # check is (optional) function defined if [ "$(type -t run_hook_on_whole_repo)" == function ] && # check is hook run via `pre-commit run --all` common::is_hook_run_on_whole_repo "$hook_id" "${files[@]}"; then run_hook_on_whole_repo "$args" exit 0 fi # consume modified files passed from pre-commit so that # hook runs against only those relevant directories local index=0 for file_with_path in "${files[@]}"; do file_with_path="${file_with_path// /__REPLACED__SPACE__}" dir_paths[index]=$(dirname "$file_with_path") ((index += 1)) done # preserve errexit status shopt -qo errexit && ERREXIT_IS_SET=true # allow hook to continue if exit_code is greater than 0 set +e local final_exit_code=0 # run hook for each path for dir_path in $(echo "${dir_paths[*]}" | tr ' ' '\n' | sort -u); do dir_path="${dir_path//__REPLACED__SPACE__/ }" pushd "$dir_path" > /dev/null || continue per_dir_hook_unique_part "$args" "$dir_path" local exit_code=$? if [ $exit_code -ne 0 ]; then final_exit_code=$exit_code fi popd > /dev/null done # restore errexit if it was set before the "for" loop [[ $ERREXIT_IS_SET ]] && set -e # return the hook final exit_code exit $final_exit_code } ####################################################################### # Colorize provided string and print it out to stdout # Environment variables: # PRE_COMMIT_COLOR (string) If set to `never` - do not colorize output # Arguments: # COLOR (string) Color name that will be used to colorize # TEXT (string) # Outputs: # Print out provided text to stdout ####################################################################### function common::colorify { # shellcheck disable=SC2034 local -r red="\e[0m\e[31m" # shellcheck disable=SC2034 local -r green="\e[0m\e[32m" # shellcheck disable=SC2034 local -r yellow="\e[0m\e[33m" # Color reset local -r RESET="\e[0m" # Params start # local COLOR="${!1}" local -r TEXT=$2 # Params end # if [ "$PRE_COMMIT_COLOR" = "never" ]; then COLOR=$RESET fi echo -e "${COLOR}${TEXT}${RESET}" }