Skip to content

Commit 302c7fd

Browse files
authored
feat: Support Environment Variables for Hook Commands (#27)
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.
1 parent 03770b1 commit 302c7fd

10 files changed

+165
-34
lines changed

README.md

+31-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Pre-Commit-GoLang [![MIT license](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/tekwizely/pre-commit-golang/blob/master/LICENSE)
22

3-
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.
3+
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.
44

55
Requires the [Pre-Commit.com](https://pre-commit.com) Hook Management Framework.
66

@@ -26,6 +26,11 @@ You can copy/paste the following snippet into your `.pre-commit-config.yaml` fil
2626
# that Pre-Commit passes into the hook.
2727
# For repo-based hooks, '--' is not needed.
2828
#
29+
# NOTE: You can pass environment variables to hooks using args with the
30+
# following format:
31+
#
32+
# --hook:env:NAME=VALUE
33+
#
2934
# Consider adding aliases to longer-named hooks for easier CLI usage.
3035
# ==========================================================================
3136
- 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
117122
# Invoking Custom Go Tools
118123
# - Configured *entirely* through the `args` attribute, ie:
119124
# args: [ go, test, ./... ]
125+
# - Use arg `--hook:error-on-output` to indicate that any output from the tool
126+
# should be treated as an error.
120127
# - Use the `name` attribute to provide better messaging when the hook runs
121-
# - Use the `alias` attribute to be able invoke your hook via `pre-commit run`
128+
# - Use the `alias` attribute to be able to invoke your hook via `pre-commit run`
122129
#
123130
- id: my-cmd
124131
- id: my-cmd-mod
@@ -205,6 +212,21 @@ See each hook's description below for some popular options that you might want t
205212
206213
Additionally, you can view each tool's individual home page or help settings to learn about all the available options.
207214
215+
#### Passing Environment Variables To Hooks
216+
You can pass environment variables to hooks to customize tool behavior.
217+
218+
**NOTE:** The Pre-Commit framework does not directly support the ability to pass environment variables to hooks.
219+
220+
This feature is enabled via support for a specially-formatted argument:
221+
222+
* `--hook:env:NAME=VALUE`
223+
224+
The hook script will detect this argument and set the variable `NAME` to the value `VALUE` before invoking the configured tool.
225+
226+
You can pass multiple `--hook:env:` arguments.
227+
228+
The arguments can appear anywhere in the `args:` list.
229+
208230
#### Always Run
209231
By default, hooks ONLY run when matching file types (usually `*.go`) are staged.
210232
@@ -214,10 +236,10 @@ When configured to `"always_run"`, a hook is executed as if EVERY matching file
214236
215237
pre-commit supports the ability to assign both an `alias` and a `name` to a configured hook:
216238
217-
| config | description
218-
|--------|------------
219-
| alias | (optional) allows the hook to be referenced using an additional id when using `pre-commit run <hookid>`
220-
| name | (optional) override the name of the hook - shown during hook execution
239+
| config | description |
240+
|--------|---------------------------------------------------------------------------------------------------------|
241+
| alias | (optional) allows the hook to be referenced using an additional id when using `pre-commit run <hookid>` |
242+
| name | (optional) override the name of the hook - shown during hook execution |
221243
222244
These are beneficial for a couple of reasons:
223245
@@ -735,7 +757,7 @@ The alias will enable you to invoke the hook manually from the command-line when
735757
736758
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.
737759
738-
The my-cmd hooks accept an `--error-on-output` argument to indicate this behavior.
760+
The my-cmd hooks accept a `--hook:error-on-output` argument to indicate this behavior.
739761
740762
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:
741763
@@ -748,10 +770,10 @@ _.pre-commit-config.yaml_
748770
- id: my-cmd
749771
name: go-fmt
750772
alias: go-fmt
751-
args: [ --error-on-output, gofmt, -l, -d ]
773+
args: [ gofmt, -l, -d, --hook:error-on-output]
752774
```
753775
754-
**NOTE:** When used, the `--error-on-output` option **must** be the first argument.
776+
**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.
755777
756778
----------
757779
## License

lib/cmd-files.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# shellcheck shell=bash
22

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

56
prepare_file_hook_cmd "$@"
67

78
error_code=0
89
for file in "${FILES[@]}"; do
910
if [ "${error_on_output:-}" -eq 1 ]; then
10-
output=$("${cmd[@]}" "${OPTIONS[@]}" "${file}" 2>&1)
11+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "${file}" 2>&1)
1112
if [ -n "${output}" ]; then
1213
printf "%s\n" "${output}"
1314
error_code=1
1415
fi
15-
elif ! "${cmd[@]}" "${OPTIONS[@]}" "${file}"; then
16+
elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "${file}"; then
1617
error_code=1
1718
fi
1819
done

lib/cmd-mod.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# shellcheck shell=bash
22

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

56
prepare_file_hook_cmd "$@"
@@ -14,12 +15,12 @@ error_code=0
1415
for sub in $(find_module_roots "${FILES[@]}" | sort -u); do
1516
pushd "${sub}" > /dev/null || exit 1
1617
if [ "${error_on_output:-}" -eq 1 ]; then
17-
output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1)
18+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1)
1819
if [ -n "${output}" ]; then
1920
printf "%s\n" "${output}"
2021
error_code=1
2122
fi
22-
elif ! "${cmd[@]}" "${OPTIONS[@]}"; then
23+
elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"; then
2324
error_code=1
2425
fi
2526
popd > /dev/null || exit 1

lib/cmd-pkg.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# shellcheck shell=bash
22

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

56
prepare_file_hook_cmd "$@"
@@ -8,12 +9,12 @@ export GO111MODULE=off
89
error_code=0
910
for sub in $(printf "%q\n" "${FILES[@]}" | xargs -n1 dirname | sort -u); do
1011
if [ "${error_on_output:-}" -eq 1 ]; then
11-
output=$("${cmd[@]}" "${OPTIONS[@]}" "./${sub}" 2>&1)
12+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "./${sub}" 2>&1)
1213
if [ -n "${output}" ]; then
1314
printf "%s\n" "${output}"
1415
error_code=1
1516
fi
16-
elif ! "${cmd[@]}" "${OPTIONS[@]}" "./${sub}"; then
17+
elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" "./${sub}"; then
1718
error_code=1
1819
fi
1920
done

lib/cmd-repo-mod.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# shellcheck shell=bash
22

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

56
prepare_repo_hook_cmd "$@"
@@ -14,12 +15,12 @@ error_code=0
1415
for sub in $(find . -name go.mod -not -path '*/vendor/*' -exec dirname "{}" ';' | sort -u); do
1516
pushd "${sub}" > /dev/null || exit 1
1617
if [ "${error_on_output:-}" -eq 1 ]; then
17-
output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1)
18+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1)
1819
if [ -n "${output}" ]; then
1920
printf "%s\n" "${output}"
2021
error_code=1
2122
fi
22-
elif ! "${cmd[@]}" "${OPTIONS[@]}"; then
23+
elif ! /usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"; then
2324
error_code=1
2425
fi
2526
popd > /dev/null || exit 1

lib/cmd-repo-pkg.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# shellcheck shell=bash
22

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

56
prepare_repo_hook_cmd "$@"
@@ -9,11 +10,11 @@ if [ "${use_dot_dot_dot:-}" -eq 1 ]; then
910
fi
1011
export GO111MODULE=off
1112
if [ "${error_on_output:-}" -eq 1 ]; then
12-
output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1)
13+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1)
1314
if [ -n "${output}" ]; then
1415
printf "%s\n" "${output}"
1516
exit 1
1617
fi
1718
else
18-
"${cmd[@]}" "${OPTIONS[@]}"
19+
/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"
1920
fi

lib/cmd-repo.bash

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# shellcheck shell=bash
22

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

56
prepare_repo_hook_cmd "$@"
@@ -10,11 +11,11 @@ if [[ ${#target[@]} -gt 0 ]]; then
1011
OPTIONS+=("${target[@]}")
1112
fi
1213
if [ "${error_on_output:-}" -eq 1 ]; then
13-
output=$("${cmd[@]}" "${OPTIONS[@]}" 2>&1)
14+
output=$(/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}" 2>&1)
1415
if [ -n "${output}" ]; then
1516
printf "%s\n" "${output}"
1617
exit 1
1718
fi
1819
else
19-
"${cmd[@]}" "${OPTIONS[@]}"
20+
/usr/bin/env "${ENV_VARS[@]}" "${cmd[@]}" "${OPTIONS[@]}"
2021
fi

lib/common.bash

+82-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
: "${ignore_file_pattern_array:=}"
66

77
##
8-
# prepare_repo_hook_cmd
8+
# prepare_file_hook_cmd
99
#
1010
function prepare_file_hook_cmd {
1111
verify_hook_cmd
@@ -38,10 +38,46 @@ function verify_hook_cmd {
3838
##
3939
# parse_file_hook_args
4040
# Creates global vars:
41-
# OPTIONS: List of options to passed to comand
42-
# FILES : List of files to process, filtered against ignore_file_pattern_array
41+
# ENV_VARS: List of variables to assign+export before invoking command
42+
# OPTIONS : List of options to pass to command
43+
# FILES : List of files to process, filtered against ignore_file_pattern_array
44+
#
45+
# NOTE: We consume the first (optional) '--' we encounter.
46+
# If you want to pass '--' to the command, you'll need to use 2 of them
47+
# in hook args, i.e. "args: [..., '--', '--']"
4348
#
4449
function parse_file_hook_args {
50+
# Look for '--hook:*' options up to the first (optional) '--'
51+
# Anything else (including '--' and after) gets saved and passed to next step
52+
# Positional order of saved arguments is preserved
53+
#
54+
local ENV_REGEX='^[a-zA-Z_][a-zA-Z0-9_]*=.*$'
55+
ENV_VARS=()
56+
local __ARGS=()
57+
while [ $# -gt 0 ] && [ "$1" != "--" ]; do
58+
case "$1" in
59+
--hook:env:*)
60+
local env_var="${1#--hook:env:}"
61+
if [[ "${env_var}" =~ ${ENV_REGEX} ]]; then
62+
ENV_VARS+=("${env_var}")
63+
else
64+
printf "ERROR: Invalid hook:env variable: '%s'\n" "${env_var}" >&2
65+
exit 1
66+
fi
67+
shift
68+
;;
69+
--hook:*)
70+
printf "ERROR: Unknown hook option: '%s'\n" "${1}" >&2
71+
exit 1
72+
;;
73+
*) # preserve positional arguments
74+
__ARGS+=("$1")
75+
shift
76+
;;
77+
esac
78+
done
79+
set -- "${__ARGS[@]}" "${@}"
80+
unset __ARGS
4581
OPTIONS=()
4682
# If arg doesn't pass [ -f ] check, then it is assumed to be an option
4783
#
@@ -60,6 +96,7 @@ function parse_file_hook_args {
6096
done
6197

6298
# If '--' next, then files = options
99+
# NOTE: We consume the '--' here
63100
#
64101
if [ "$1" == "--" ]; then
65102
shift
@@ -90,14 +127,52 @@ function parse_file_hook_args {
90127

91128
##
92129
# parse_repo_hook_args
93-
# Build options list, ignoring '--', and anything after
130+
# Creates global vars:
131+
# ENV_VARS: List of variables to assign+export before invoking command
132+
# OPTIONS : List of options to pass to command
133+
#
134+
# NOTE: For consistency with file hooks,
135+
# we consume the first (optional) '--' we encounter.
136+
# If you want to pass '--' to the command, you'll need to use 2 of them
137+
# in hook args, i.e. "args: [..., '--', '--']"
94138
#
95139
function parse_repo_hook_args {
140+
# Look for '--hook:*' options up to the first (optional) '--'
141+
# Consumes the first '--', treating anything after as OPTIONS
142+
# Positional order of OPTIONS is preserved
143+
#
144+
local ENV_REGEX='^[a-zA-Z_][a-zA-Z0-9_]*=.*$'
145+
ENV_VARS=()
96146
OPTIONS=()
97-
while [ $# -gt 0 ] && [ "$1" != "--" ]; do
98-
OPTIONS+=("$1")
99-
shift
147+
while [ $# -gt 0 ]; do
148+
case "$1" in
149+
--hook:env:*)
150+
local env_var="${1#--hook:env:}"
151+
if [[ "${env_var}" =~ ${ENV_REGEX} ]]; then
152+
ENV_VARS+=("${env_var}")
153+
else
154+
printf "ERROR: Invalid hook:env variable: '%s'\n" "${env_var}" >&2
155+
exit 1
156+
fi
157+
shift
158+
;;
159+
--hook:*)
160+
printf "ERROR: Unknown hook option: '%s'\n" "${1}" >&2
161+
exit 1
162+
;;
163+
--) # consume '--' and stop loop
164+
shift
165+
break
166+
;;
167+
*) # preserve positional arguments
168+
OPTIONS+=("$1")
169+
shift
170+
;;
171+
esac
100172
done
173+
# Any remaining items also considered OPTIONS
174+
#
175+
OPTIONS+=("$@")
101176
}
102177

103178
##

lib/prepare-my-cmd.bash

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,36 @@
11
# shellcheck shell=bash
22
use_dot_dot_dot=0
3-
while (($#)); do
3+
# Check for error-on-output
4+
# '--error-on-output' can *only* appear at the FRONT
5+
# !! NOTE: This is DEPRECATED and will be removed in a future version !!
6+
#
7+
if [[ "${1:-}" == "--error-on-output" ]]; then
8+
error_on_output=1
9+
shift
10+
fi
11+
# '--hook:error-on-output' can appear anywhere before (the optional) '--'
12+
# Anything else (including '--' and after) gets saved and passed to next step
13+
# Positional order of saved arguments is preserved
14+
#
15+
_ARGS=()
16+
while [ $# -gt 0 ] && [ "$1" != "--" ]; do
417
case "$1" in
5-
--error-on-output)
18+
--hook:error-on-output)
619
error_on_output=1
20+
# We continue (vs break) in order to consume multiple occurrences
21+
# of the arg. VERY unlikely but let's be safe.
22+
#
723
shift
824
;;
9-
*)
10-
break
25+
*) # preserve positional arguments
26+
__ARGS+=("$1")
27+
shift
1128
;;
1229
esac
1330
done
31+
set -- "${__ARGS[@]}" "${@}"
32+
unset __ARGS
33+
1434
cmd=()
1535
if [ -n "${1:-}" ]; then
1636
cmd+=("${1}")

0 commit comments

Comments
 (0)