Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support arbitrary shells for hook scripts #971

Closed
jawnsy opened this issue May 18, 2021 · 20 comments
Closed

Support arbitrary shells for hook scripts #971

jawnsy opened this issue May 18, 2021 · 20 comments
Labels

Comments

@jawnsy
Copy link

jawnsy commented May 18, 2021

Husky seems to run all hook scripts using sh, regardless of the shebang present in the hook. For example, with a pre-commit hook containing a line like:

source "$(dirname "$0")/_/husky.sh"

we will get an error, because source is a Bash-specific command:

$ git commit
.husky/pre-commit: 4: source: not found

This occurs regardless of whether the shebang line is set as #!/bin/sh or #!/usr/bin/env bash, and seems to be because Husky invokes the currently-running hook script using sh in order to capture the exit value:

export readonly husky_skip_init=1
sh -e "$0" "$@"
exitCode="$?"
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi

A workaround for this is to use a Bourne-shell compatible pre-commit script, which then invokes bash explicitly with the target hook.

However, it would be nice if Husky respected the shebang of the hook script directly when executing it. This seems to be possible by changing husky.sh to use env to execute the command instead, for example:

  export readonly husky_skip_init=1
  env "$0" "$@"
  exitCode="$?"

However, this only works if the hook is also marked executable. Maybe we can use $SHELL instead to detect the currently-running shell, and exec the script again using that?

@bertho-zero
Copy link

It would be perfect with https://github.com/google/zx !

@jawnsy
Copy link
Author

jawnsy commented May 22, 2021

I don't know if this works generally for other shells, but I found that we can add a section at the top of the pre-commit script to detect if we're not running under Bash, and exec under bash in that case. For example, here's my pre-commit script:

#!/usr/bin/env bash

# Bash sets the BASH environment variable, so if it is not set, then we
# are running in a different shell, so manually run ourselves in BASH.
if [ -z "${BASH:-}" ]; then
  exec bash "$0" "$@"
fi

source "$(dirname "$0")/_/husky.sh"

set -euo pipefail
yarn lint-staged

This seems to work alright on Ubuntu (which uses dash as /bin/sh). Maybe for now it's enough to document this as a way to achieve this, for shells that are supersets of sh (i.e. as long as the shell can run normal sh scripts, then this should work). I think what happens is this:

  1. When the script first runs, it is invoked according to the shebang, running as sh
  2. We detect that we're not running under bash, and self-exec the script under bash
  3. Once we're running the script in bash, we source the husky.sh script
  4. The husky.sh script executes our current script in a new shell, using sh, and sets husky_skip_init in the environment
  5. The script detects that we're not running under bash, and self-execs again
  6. This time, the source of husky.sh does nothing, because we're already running under bash
  7. We then proceed to run the remainder of the script

I think we can remove some of the nested execs if we use the sh-compatible . "$(dirname "$0")/_/husky.sh" and move it to the top too, but I personally find the source keyword to be clearer

@nfantone
Copy link

nfantone commented Jun 17, 2021

Also related: since husky runs every script with sh -e, testing exit codes from other tools becomes impossible. The whole script will be interrupted as soon as any command returns a non-zero status. For example, the following will result in husky - prepare-commit-msg hook exited with code 1 (error) if no rebase is in progress:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Avoid applying hook if `git rebase` is in progress
git rev-parse -q --verify REBASE_HEAD

if [ $? -eq 0 ]; then
# ...

@stale
Copy link

stale bot commented Aug 16, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Aug 16, 2021
@timdp
Copy link

timdp commented Aug 16, 2021

Please remove the wontfix label as this still applies. It's really confusing to have husky add create a shell script with a shebang and then ignore it when it's run. Thanks!

@stale
Copy link

stale bot commented Oct 15, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Oct 15, 2021
@bertho-zero
Copy link

Don't close, bot

@stale stale bot removed the wontfix label Oct 16, 2021
@stale
Copy link

stale bot commented Dec 15, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Dec 15, 2021
@hustcer
Copy link

hustcer commented Dec 15, 2021

need fix

@stale stale bot removed the wontfix label Dec 15, 2021
@stale
Copy link

stale bot commented Feb 13, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@elektronik2k5
Copy link

Here's my best attempt to work around this issue:

  1. Install patch-package and set it up in your package.json's scripts section: "prepare": "patch-package --error-on-fail && husky install",
  2. Create a patches/husky+7.0.4.patch file with these contents:
diff --git a/node_modules/husky/husky.sh b/node_modules/husky/husky.sh
index 6809ccc..4a467b3 100644
--- a/node_modules/husky/husky.sh
+++ b/node_modules/husky/husky.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/usr/bin/env bash
 if [ -z "$husky_skip_init" ]; then
   debug () {
     if [ "$HUSKY_DEBUG" = "1" ]; then
@@ -20,7 +20,7 @@ if [ -z "$husky_skip_init" ]; then
   fi
 
   export readonly husky_skip_init=1
-  sh -e "$0" "$@"
+  bash -e "$0" "$@"
   exitCode="$?"
 
   if [ $exitCode != 0 ]; then
  1. Use the standard #!/usr/bin/env bash shebang in your husky scripts.
  2. ???
  3. PROFIT!!!!

This setup works on Linux, Windows (via git bash) and even Mac with an updated bash installed via brew, which was the challenging part.
Pretty sure it'll work for other shells just as well, if you replace bash with your-favorite-shell.

I'd love to have a non monkey patched way to achieve the same result.

@leppaott
Copy link

+1 for some option to change this behavior. Our pre-commit started as portable script that worked on bash and dash for one Ubuntu user but now started to use Bash specific code so this would be nice without patching or committing the husky code.

@mondeja
Copy link

mondeja commented Mar 14, 2022

Just to give my two cents on this discussión.

Bash is not a POSIX compliant shell. Only the sh binary is the one that can be expected to work across operative systems. So the example of the OP should be rewritten as . "$(dirname "$0")/_/husky.sh". Note the change from source to . (a dot). That will work on Bash and POSIX compliant shells.

@jlarmstrongiv
Copy link

Found this out the hard way! I ended up monkey-patching:

{
  "prepare": "husky install && npx replace-in-file '#!/bin/sh' '#!/usr/bin/env bash' '.husky/_/husky.sh' --quiet && npx replace-in-file 'sh -e' 'bash -e' '.husky/_/husky.sh' --quiet",
}

@stale
Copy link

stale bot commented Jun 1, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Jun 1, 2022
@hustcer
Copy link

hustcer commented Jun 1, 2022

no stale

@ioxua
Copy link

ioxua commented Jun 18, 2023

was this ever fixed? I'm on husky@^8.0.0, the issue remains

@hustcer
Copy link

hustcer commented Jun 19, 2023

For those who want to use arbitrary shells for hooks, I would recommend lefthook

@d-hancharou
Copy link

d-hancharou commented Feb 29, 2024

Found this out the hard way! I ended up monkey-patching:

{
  "prepare": "husky install && npx replace-in-file '#!/bin/sh' '#!/usr/bin/env bash' '.husky/_/husky.sh' --quiet && npx replace-in-file 'sh -e' 'bash -e' '.husky/_/husky.sh' --quiet",
}

Correct fix for latest version (v8.0.0+):

{
  "prepare": "husky install && npx replace-in-file '#!/usr/bin/env sh' '#!/usr/bin/env bash' '.husky/_/husky.sh' --quiet && npx replace-in-file 'sh -e' 'bash -e' '.husky/_/husky.sh' --quiet",
}

@typicode
Copy link
Owner

typicode commented Mar 1, 2024

To run hooks with another shell simply use this:

# In .husky/pre-commit
bash << EOF
# your script
# ...
EOF

Be aware that not everyone has bash (in particular Windows user) so your hook may fail for them.

Repository owner locked as resolved and limited conversation to collaborators Mar 1, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.