diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b87a406..28cfaf1c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,3 +40,11 @@ repos: hooks: - id: check-hooks-apply - id: check-useless-excludes + # Requires to be run under `bash`. + # Requires installation of additional tools. Tools are available on Windows. + # shfmt: https://github.com/mvdan/sh/releases +- repo: https://github.com/syntaqx/git-hooks + rev: v0.0.16 + hooks: + - id: shfmt + args: [-i 4, -w, -s] diff --git a/bash_script.sh b/bash_script.sh new file mode 100644 index 00000000..021e8957 --- /dev/null +++ b/bash_script.sh @@ -0,0 +1,2 @@ +#!/bin/bash +set -Eeuo pipefail diff --git a/pre_commit/ShelPathConv.py b/pre_commit/ShelPathConv.py new file mode 100644 index 00000000..c2c6f855 --- /dev/null +++ b/pre_commit/ShelPathConv.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""".""" +import glob +import os +import pathlib +import re +import subprocess +import sys + +GLOB_PATTERN = re.compile(r'([^:/\\])(?=[/\\]|$)') + + +def GetRealCasePath(path): + """Convert a case preserving path to a case sensitive compatible path.""" + drive, tail = os.path.splitdrive(path) + return next( + iter(glob.glob(''.join((drive, re.sub(GLOB_PATTERN, r'[\1]', tail))))), + path, + ) + + +def ConvertPath(path, prefix, drive_letter_case): + """Convert a Windows path to a POSIX path.""" + if os.path.exists(path): + path = GetRealCasePath(path) + drive, tail = os.path.splitdrive(path) + if drive and not os.path.isabs(path): + drive, tail = os.path.splitdrive(os.path.abspath(path)) + if drive and drive[1:2] == r':': + path = ( + prefix / + drive_letter_case(drive[0]) / + pathlib.PureWindowsPath(tail[1:]).as_posix() + ).as_posix() + else: + path = pathlib.PureWindowsPath(path).as_posix() + return path + + +def ConvertArgsWin32(*args): + """Convert all path like arguments from Windows to POSIX.""" + path = os.path.abspath(os.environ.get('SystemRoot')) + if not path or path[1:2] != r':': + return args + + try: + nix_path = subprocess.run( + [args[0], '-c', 'pwd'], + check=True, + cwd=path, + stdout=subprocess.PIPE, + universal_newlines=True, + ).stdout.strip() + except Exception: + return args + + path = pathlib.PureWindowsPath( + os.path.splitdrive(GetRealCasePath(path))[1], + ).as_posix() + prefix, nix_path, tail = ( + pathlib.PurePosixPath(nix_path).as_posix().partition(path) + ) + if not prefix or nix_path != path or tail: + return args + + drive_letter_case = ( + (lambda s: s.upper()) + if prefix[-1:].isupper() + else (lambda s: s.lower()) + if prefix[-1:].islower() + else (lambda s: s) + ) + prefix = pathlib.PurePosixPath(prefix[0:-1]) + + return [args[0]] + [ + ConvertPath(arg, prefix, drive_letter_case) for arg in args[1:] + ] + + +ConvertArgs = ( + ConvertArgsWin32 if sys.platform == 'win32' else (lambda *args: args) +) + +if __name__ == '__main__': + print(*ConvertArgs(*sys.argv[1:])) diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py index ab2c9eec..46c13ded 100644 --- a/pre_commit/parse_shebang.py +++ b/pre_commit/parse_shebang.py @@ -4,6 +4,9 @@ from __future__ import unicode_literals import os.path from identify.identify import parse_shebang_from_file +from identify.identify import tags_from_path + +from pre_commit import ShelPathConv class ExecutableNotFoundError(OSError): @@ -71,10 +74,12 @@ def normalize_cmd(cmd): # Use PATH to determine the executable exe = normexe(cmd[0]) + convert = 'shell' in tags_from_path(exe) + # Figure out the shebang from the resulting command cmd = parse_filename(exe) + (exe,) + cmd[1:] # This could have given us back another bare executable exe = normexe(cmd[0]) - - return (exe,) + cmd[1:] + cmd = (exe,) + cmd[1:] + return ShelPathConv.ConvertArgs(*cmd) if convert else cmd