diff --git a/README.md b/README.md index 04d8061..50586c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pre-Commit-GoLang [![MIT license](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/tekwizely/pre-commit-golang/blob/master/LICENSE) -A set of git pre-commit hooks for Golang with support for multi-module monorepos, the ability to pass arguments to all hooks, and the ability to invoke custom go tools. +A set of git pre-commit hooks for Golang with support for multi-module monorepos, the ability to pass arguments and environment variables to all hooks, and the ability to invoke custom go tools. Requires the [Pre-Commit.com](https://pre-commit.com) Hook Management Framework. @@ -26,6 +26,11 @@ You can copy/paste the following snippet into your `.pre-commit-config.yaml` fil # that Pre-Commit passes into the hook. # For repo-based hooks, '--' is not needed. # + # NOTE: You can pass environment variables to hooks using args with the + # following format: + # + # --hook:env:NAME=VALUE + # # Consider adding aliases to longer-named hooks for easier CLI usage. # ========================================================================== - repo: https://github.com/tekwizely/pre-commit-golang @@ -117,8 +122,10 @@ You can copy/paste the following snippet into your `.pre-commit-config.yaml` fil # Invoking Custom Go Tools # - Configured *entirely* through the `args` attribute, ie: # args: [ go, test, ./... ] + # - Use arg `--hook:error-on-output` to indicate that any output from the tool + # should be treated as an error. # - Use the `name` attribute to provide better messaging when the hook runs - # - Use the `alias` attribute to be able invoke your hook via `pre-commit run` + # - Use the `alias` attribute to be able to invoke your hook via `pre-commit run` # - id: my-cmd - id: my-cmd-mod @@ -205,6 +212,21 @@ See each hook's description below for some popular options that you might want t Additionally, you can view each tool's individual home page or help settings to learn about all the available options. +#### Passing Environment Variables To Hooks +You can pass environment variables to hooks to customize tool behavior. + +**NOTE:** The Pre-Commit framework does not directly support the ability to pass environment variables to hooks. + +This feature is enabled via support for a specially-formatted argument: + +* `--hook:env:NAME=VALUE` + +The hook script will detect this argument and set the variable `NAME` to the value `VALUE` before invoking the configured tool. + +You can pass multiple `--hook:env:` arguments. + +The arguments can appear anywhere in the `args:` list. + #### Always Run By default, hooks ONLY run when matching file types (usually `*.go`) are staged. @@ -214,10 +236,10 @@ When configured to `"always_run"`, a hook is executed as if EVERY matching file pre-commit supports the ability to assign both an `alias` and a `name` to a configured hook: -| config | description -|--------|------------ -| alias | (optional) allows the hook to be referenced using an additional id when using `pre-commit run ` -| name | (optional) override the name of the hook - shown during hook execution +| config | description | +|--------|---------------------------------------------------------------------------------------------------------| +| alias | (optional) allows the hook to be referenced using an additional id when using `pre-commit run ` | +| name | (optional) override the name of the hook - shown during hook execution | These are beneficial for a couple of reasons: @@ -735,7 +757,7 @@ The alias will enable you to invoke the hook manually from the command-line when Some tools, like `gofmt`, `goimports`, and `goreturns`, don't generate error codes, but instead expect the presence of any output to indicate warning/error conditions. -The my-cmd hooks accept an `--error-on-output` argument to indicate this behavior. +The my-cmd hooks accept a `--hook:error-on-output` argument to indicate this behavior. Here's an example of what it would look like to use the my-cmd hooks to invoke `gofmt` if it wasn't already included: @@ -748,10 +770,10 @@ _.pre-commit-config.yaml_ - id: my-cmd name: go-fmt alias: go-fmt - args: [ --error-on-output, gofmt, -l, -d ] + args: [ gofmt, -l, -d, --hook:error-on-output] ``` -**NOTE:** When used, the `--error-on-output` option **must** be the first argument. +**NOTE:** The plain `--error-on-output` option is now deprecated, but still supported, as long as it's the **very first** entry in the `args:` list. ---------- ## License diff --git a/lib/cmd-files.bash b/lib/cmd-files.bash index 400bf76..8ca2049 100644 --- a/lib/cmd-files.bash +++ b/lib/cmd-files.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_file_hook_cmd "$@" @@ -7,12 +8,12 @@ prepare_file_hook_cmd "$@" error_code=0 for file in "${FILES[@]}"; do if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" "${file}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "${file}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" error_code=1 fi - elif ! "${cmd[@]}" "${OPTIONS[@]}" "${file}"; then + elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "${file}"; then error_code=1 fi done diff --git a/lib/cmd-mod.bash b/lib/cmd-mod.bash index 5e822e4..4879221 100644 --- a/lib/cmd-mod.bash +++ b/lib/cmd-mod.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_file_hook_cmd "$@" @@ -14,12 +15,12 @@ error_code=0 for sub in $(find_module_roots "${FILES[@]}" | sort -u); do pushd "${sub}" > /dev/null || exit 1 if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" error_code=1 fi - elif ! "${cmd[@]}" "${OPTIONS[@]}"; then + elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"; then error_code=1 fi popd > /dev/null || exit 1 diff --git a/lib/cmd-pkg.bash b/lib/cmd-pkg.bash index bf820c9..c5608a6 100644 --- a/lib/cmd-pkg.bash +++ b/lib/cmd-pkg.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_file_hook_cmd "$@" @@ -8,12 +9,12 @@ export GO111MODULE=off error_code=0 for sub in $(printf "%q\n" "${FILES[@]}" | xargs -n1 dirname | sort -u); do if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" "./${sub}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "./${sub}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" error_code=1 fi - elif ! "${cmd[@]}" "${OPTIONS[@]}" "./${sub}"; then + elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "./${sub}"; then error_code=1 fi done diff --git a/lib/cmd-repo-mod.bash b/lib/cmd-repo-mod.bash index 25c91d2..c44b9d5 100644 --- a/lib/cmd-repo-mod.bash +++ b/lib/cmd-repo-mod.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_repo_hook_cmd "$@" @@ -14,12 +15,12 @@ error_code=0 for sub in $(find . -name go.mod -not -path '*/vendor/*' -exec dirname "{}" ';' | sort -u); do pushd "${sub}" > /dev/null || exit 1 if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" error_code=1 fi - elif ! "${cmd[@]}" "${OPTIONS[@]}"; then + elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"; then error_code=1 fi popd > /dev/null || exit 1 diff --git a/lib/cmd-repo-pkg.bash b/lib/cmd-repo-pkg.bash index 8558f65..3f368f0 100644 --- a/lib/cmd-repo-pkg.bash +++ b/lib/cmd-repo-pkg.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_repo_hook_cmd "$@" @@ -9,11 +10,11 @@ if [ "${use_dot_dot_dot:-}" -eq 1 ]; then fi export GO111MODULE=off if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" exit 1 fi else - "${cmd[@]}" "${OPTIONS[@]}" + /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" fi diff --git a/lib/cmd-repo.bash b/lib/cmd-repo.bash index cfa6ecb..81e1550 100644 --- a/lib/cmd-repo.bash +++ b/lib/cmd-repo.bash @@ -1,5 +1,6 @@ # shellcheck shell=bash +# shellcheck source=./common.bash . "$(dirname "${0}")/lib/common.bash" prepare_repo_hook_cmd "$@" @@ -10,11 +11,11 @@ if [[ ${#target[@]} -gt 0 ]]; then OPTIONS+=("${target[@]}") fi if [ "${error_on_output:-}" -eq 1 ]; then - output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1) + output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1) if [ -n "${output}" ]; then printf "%s\n" "${output}" exit 1 fi else - "${cmd[@]}" "${OPTIONS[@]}" + /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" fi diff --git a/lib/common.bash b/lib/common.bash index 1e50b4c..9d9da65 100644 --- a/lib/common.bash +++ b/lib/common.bash @@ -5,7 +5,7 @@ : "${ignore_file_pattern_array:=}" ## -# prepare_repo_hook_cmd +# prepare_file_hook_cmd # function prepare_file_hook_cmd { verify_hook_cmd @@ -38,10 +38,46 @@ function verify_hook_cmd { ## # parse_file_hook_args # Creates global vars: -# OPTIONS: List of options to passed to comand -# FILES : List of files to process, filtered against ignore_file_pattern_array +# ENV_VARS: List of variables to assign+export before invoking command +# OPTIONS : List of options to pass to command +# FILES : List of files to process, filtered against ignore_file_pattern_array +# +# NOTE: We consume the first (optional) '--' we encounter. +# If you want to pass '--' to the command, you'll need to use 2 of them +# in hook args, i.e. "args: [..., '--', '--']" # function parse_file_hook_args { + # Look for '--hook:*' options up to the first (optional) '--' + # Anything else (including '--' and after) gets saved and passed to next step + # Positional order of saved arguments is preserved + # + local ENV_REGEX='^[a-zA-Z_][a-zA-Z0-9_]*=.*$' + ENV_VARS=() + local __ARGS=() + while [ $# -gt 0 ] && [ "$1" != "--" ]; do + case "$1" in + --hook:env:*) + local env_var="${1#--hook:env:}" + if [[ "${env_var}" =~ ${ENV_REGEX} ]]; then + ENV_VARS+=("${env_var}") + else + printf "ERROR: Invalid hook:env variable: '%s'\n" "${env_var}" >&2 + exit 1 + fi + shift + ;; + --hook:*) + printf "ERROR: Unknown hook option: '%s'\n" "${1}" >&2 + exit 1 + ;; + *) # preserve positional arguments + __ARGS+=("$1") + shift + ;; + esac + done + set -- "${__ARGS[@]}" "${@}" + unset __ARGS OPTIONS=() # If arg doesn't pass [ -f ] check, then it is assumed to be an option # @@ -60,6 +96,7 @@ function parse_file_hook_args { done # If '--' next, then files = options + # NOTE: We consume the '--' here # if [ "$1" == "--" ]; then shift @@ -90,14 +127,52 @@ function parse_file_hook_args { ## # parse_repo_hook_args -# Build options list, ignoring '--', and anything after +# Creates global vars: +# ENV_VARS: List of variables to assign+export before invoking command +# OPTIONS : List of options to pass to command +# +# NOTE: For consistency with file hooks, +# we consume the first (optional) '--' we encounter. +# If you want to pass '--' to the command, you'll need to use 2 of them +# in hook args, i.e. "args: [..., '--', '--']" # function parse_repo_hook_args { + # Look for '--hook:*' options up to the first (optional) '--' + # Consumes the first '--', treating anything after as OPTIONS + # Positional order of OPTIONS is preserved + # + local ENV_REGEX='^[a-zA-Z_][a-zA-Z0-9_]*=.*$' + ENV_VARS=() OPTIONS=() - while [ $# -gt 0 ] && [ "$1" != "--" ]; do - OPTIONS+=("$1") - shift + while [ $# -gt 0 ]; do + case "$1" in + --hook:env:*) + local env_var="${1#--hook:env:}" + if [[ "${env_var}" =~ ${ENV_REGEX} ]]; then + ENV_VARS+=("${env_var}") + else + printf "ERROR: Invalid hook:env variable: '%s'\n" "${env_var}" >&2 + exit 1 + fi + shift + ;; + --hook:*) + printf "ERROR: Unknown hook option: '%s'\n" "${1}" >&2 + exit 1 + ;; + --) # consume '--' and stop loop + shift + break + ;; + *) # preserve positional arguments + OPTIONS+=("$1") + shift + ;; + esac done + # Any remaining items also considered OPTIONS + # + OPTIONS+=("$@") } ## diff --git a/lib/prepare-my-cmd.bash b/lib/prepare-my-cmd.bash index df89605..31876d9 100644 --- a/lib/prepare-my-cmd.bash +++ b/lib/prepare-my-cmd.bash @@ -1,16 +1,36 @@ # shellcheck shell=bash use_dot_dot_dot=0 -while (($#)); do +# Check for error-on-output +# '--error-on-output' can *only* appear at the FRONT +# !! NOTE: This is DEPRECATED and will be removed in a future version !! +# +if [[ "${1:-}" == "--error-on-output" ]]; then + error_on_output=1 + shift +fi +# '--hook:error-on-output' can appear anywhere before (the optional) '--' +# Anything else (including '--' and after) gets saved and passed to next step +# Positional order of saved arguments is preserved +# +_ARGS=() +while [ $# -gt 0 ] && [ "$1" != "--" ]; do case "$1" in - --error-on-output) + --hook:error-on-output) error_on_output=1 + # We continue (vs break) in order to consume multiple occurrences + # of the arg. VERY unlikely but let's be safe. + # shift ;; - *) - break + *) # preserve positional arguments + __ARGS+=("$1") + shift ;; esac done +set -- "${__ARGS[@]}" "${@}" +unset __ARGS + cmd=() if [ -n "${1:-}" ]; then cmd+=("${1}") diff --git a/sample-config.yaml b/sample-config.yaml index b61f1cb..5eb1877 100644 --- a/sample-config.yaml +++ b/sample-config.yaml @@ -31,7 +31,7 @@ repos: # and are NOT provided the list of staged files, # # My-Cmd-* Hooks - # Allow you to invoke custom tools in varous contexts. + # Allow you to invoke custom tools in various contexts. # Can be useful if your favorite tool(s) are not built-in (yet) # # Hook Suffixes @@ -68,6 +68,12 @@ repos: # the modified-file list that Pre-Commit passes into the hook. # NOTE: For repo-based hooks, '--' is not needed. # + # Passing Environment Variables to Hooks: + # You can pass environment variables to hooks using args with the + # following format: + # + # --hook:env:NAME=VALUE + # # Always Run: # By default, hooks ONLY run when matching file types are staged. # When configured to "always_run", a hook is executed as if EVERY matching @@ -165,8 +171,10 @@ repos: # Invoking Custom Go Tools # - Configured *entirely* through the `args` attribute, ie: # args: [ go, test, ./... ] + # - Use arg `--hook:error-on-output` to indicate that any output from the tool + # should be treated as an error. # - Use the `name` attribute to provide better messaging when the hook runs - # - Use the `alias` attribute to be able invoke your hook via `pre-commit run` + # - Use the `alias` attribute to be able to invoke your hook via `pre-commit run` # - id: my-cmd - id: my-cmd-mod