Skip to content

Commit

Permalink
feat: Support Environment Variables for Hook Commands (#27)
Browse files Browse the repository at this point in the history
Adds ability to configure environment variables to be set when invoking hook commands.
Variables are configured via hook "args" using the following argument pattern:

    --hook:env:NAME=VALUE

feat: Add support for "--hook:error-on-output" in my-cmd-* hooks.
* Can exist anywhere in the arg list before a '--' argument
* re: Support for plain '--error-on-output' at first element
  * Still works (currently)
  * Is now deprecated and will be removed in a future version

chore: Use "/usr/bin/env" to  invoke commands
* No-longer invokes the commands directly
* Makes it trivial to pass environment variables to commands
* Should not cause issues as /usr/bin/env was already vital

chore:  Add "shellcheck source" declarations on dynamically sourced files
* Only files that were already modified for this PR were addressed

Breaking Change: For compatibility with file-based hooks, Repo-based hooks
no-longer ignore '--', or proceeding arguments, in the argument list.
However, to further match file-based logic, the first '--' will be consumed,
treating anything after it as OPTIONS to be passed to the hook command.
  • Loading branch information
TekWizely committed Oct 31, 2022
1 parent 03770b1 commit 302c7fd
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 34 deletions.
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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 <hookid>`
| 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 <hookid>` |
| name | (optional) override the name of the hook - shown during hook execution |
These are beneficial for a couple of reasons:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cmd-files.bash
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

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
Expand Down
5 changes: 3 additions & 2 deletions lib/cmd-mod.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

prepare_file_hook_cmd "$@"
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cmd-pkg.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

prepare_file_hook_cmd "$@"
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cmd-repo-mod.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

prepare_repo_hook_cmd "$@"
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions lib/cmd-repo-pkg.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

prepare_repo_hook_cmd "$@"
Expand All @@ -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
5 changes: 3 additions & 2 deletions lib/cmd-repo.bash
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# shellcheck shell=bash

# shellcheck source=./common.bash
. "$(dirname "${0}")/lib/common.bash"

prepare_repo_hook_cmd "$@"
Expand All @@ -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
89 changes: 82 additions & 7 deletions lib/common.bash
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
: "${ignore_file_pattern_array:=}"

##
# prepare_repo_hook_cmd
# prepare_file_hook_cmd
#
function prepare_file_hook_cmd {
verify_hook_cmd
Expand Down Expand Up @@ -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
#
Expand All @@ -60,6 +96,7 @@ function parse_file_hook_args {
done

# If '--' next, then files = options
# NOTE: We consume the '--' here
#
if [ "$1" == "--" ]; then
shift
Expand Down Expand Up @@ -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+=("$@")
}

##
Expand Down
28 changes: 24 additions & 4 deletions lib/prepare-my-cmd.bash
Original file line number Diff line number Diff line change
@@ -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}")
Expand Down
Loading

0 comments on commit 302c7fd

Please sign in to comment.