跨平台Hook兼容性指南

编写跨平台兼容的Hook脚本

一、跨平台兼容性的挑战

在现代化的开发工作流中,Hook脚本已经成为自动化不可或缺的一部分——无论是Git Hooks、CI/CD管线中的触发脚本,还是Claude Code的自动化事件处理。然而,当团队分布在Windows、macOS和Linux等多个平台上时,一个看似简单的Hook脚本可能会因为平台差异而完全失效。

跨平台兼容性的核心挑战在于:不同的操作系统有着不同的Shell环境、文件系统规范、路径表示方式和命令集。一个在macOS上完美运行的Bash脚本,可能在Windows上根本无法执行;一个为Linux编写的Python Hook,可能因为路径分隔符问题在Windows上崩溃。

核心差异概述: Windows/macOS/Linux这三大主流操作系统在Shell命令语法、文件路径规范、环境变量命名和换行符处理等方面存在显著差异,这些差异是导致Hook脚本跨平台失效的根本原因。

Windows、macOS和Linux的核心差异:

核心理念: 跨平台兼容的Hook脚本不应该假设运行环境,而是通过条件判断和抽象层来适配不同平台。目标是"一次编写,处处运行",但前提是要深刻理解各个平台的差异并正确处理它们。

在深入具体的技术细节之前,需要明确一个基本原则:对于简单的跨平台任务,使用Python或Node.js等跨平台运行时比纯Shell脚本更可靠;而对于复杂的系统级操作,则需要精心设计Shell脚本的条件分支。

二、路径和文件系统差异

路径和文件系统的差异是跨平台Hook开发中最常见、最容易出错的问题。下面详细分析各个差异点以及对应的解决方案。

2.1 路径分隔符:Windows \ vs Linux/macOS /

路径分隔符的差异是最基本的跨平台问题。许多编程语言和工具在现代版本中已经能够自动处理路径分隔符的转换,但在Shell脚本中仍需特别留意。

# 错误做法:硬编码路径分隔符 PATH_TO_CONFIG="./config/settings.json" # 在Windows上使用正斜杠可行(大多数程序支持) PATH_TO_BIN=".\tools\build.bat" # 在Unix上完全无效 # 推荐做法:使用编程语言的标准库处理路径 # Python示例(推荐用于跨平台脚本) import os import os.path as osp config_path = osp.join("config", "settings.json") # Windows上输出: config\settings.json # Linux/macOS上输出: config/settings.json # 如果是用户提供的输入路径,使用 os.path.normpath 规范化 user_input = "config//settings.json" normalized = osp.normpath(user_input) # 自动处理多余分隔符和平台差异 # 获取当前系统路径分隔符 print(f"路径分隔符: {os.sep}") # Windows: \, Linux/macOS: / print(f"路径列表分隔符: {os.pathsep}") # Windows: ;, Linux/macOS: :
经验法则: 在Shell脚本中尽量使用正斜杠(/)作为路径分隔符——Windows的CMD和PowerShell虽然原生使用反斜杠,但绝大多数Windows程序(包括Node.js、Python、Git Bash)都支持正斜杠。只有在调用原生Windows命令(如reg.exe、icacls)时才需要使用反斜杠。

2.2 使用$HOME vs %USERPROFILE%

用户目录的路径在不同平台上差异较大,正确获取用户主目录是跨平台Hook的基础能力。

# Shell脚本中获取用户主目录 # 错误做法:假设变量名 HOME_DIR=$HOME # Windows Git Bash中有效,但原生CMD/PowerShell中没有$HOME USER_DIR=%USERPROFILE% # 仅在Windows CMD中有效 # 推荐做法:使用条件判断 #!/bin/bash if [ -n "$HOME" ]; then USER_HOME="$HOME" elif [ -n "$USERPROFILE" ]; then USER_HOME="$USERPROFILE" else USER_HOME=$(dirname "$(realpath ~)" 2>/dev/null || echo "/tmp") fi echo "用户主目录: $USER_HOME" # 更可靠的方式:使用Python python3 -c "import os; print(os.path.expanduser('~'))" # Node.js方式 node -e "console.log(require('os').homedir())" # 常见用户目录对应关系: # Windows: C:\Users\用户名 # macOS: /Users/用户名 # Linux: /home/用户名

2.3 换行符:CRLF vs LF

换行符的差异是Git Hook脚本中最隐蔽的陷阱之一。一个在Windows上保存了CRLF换行符的Shell脚本,在Linux/macOS上执行时会出现"$'\r': command not found"错误。

# 换行符差异详解 # Windows: CRLF (\r\n) —— 回车+换行 # macOS/Linux: LF (\n) —— 仅换行 # 旧版macOS (9.x及以前): CR (\r) # Git配置建议:让Git自动处理换行符 # 全局配置(建议所有项目使用) git config --global core.autocrlf input # Windows: core.autocrlf true (签出时CRLF,提交时LF) # macOS/Linux: core.autocrlf input (签出时LF,提交时LF) # 项目级配置(在.gitattributes中指定) cat > .gitattributes << 'EOF' # 所有文本文件自动规范化换行符 * text=auto # 特定文件类型指定换行符 *.sh text eol=lf # Shell脚本始终使用LF *.bat text eol=crlf # Windows批处理使用CRLF *.ps1 text eol=crlf # PowerShell脚本使用CRLF *.py text eol=lf # Python文件使用LF *.js text eol=lf # JavaScript文件使用LF *.json text eol=lf # JSON文件使用LF *.md text eol=lf # Markdown文件使用LF EOF # 检查文件换行符 # Linux/macOS: file命令 file script.sh # 输出: script.sh: Bourne-Again shell script, ASCII text executable # 使用file命令检查CRLF file script.sh # 如果包含CRLF: script.sh: ASCII text, with CRLF line terminators # 批量转换换行符 # LF → CRLF sed -i 's/$/\r/' script.sh # CRLF → LF sed -i 's/\r$//' script.sh # 使用dos2unix/unix2dos工具(如果已安装) dos2unix script.sh # CRLF → LF unix2dos script.sh # LF → CRLF
常见陷阱: 在Windows上使用记事本编辑Shell脚本会导致换行符被自动转换为CRLF,导致脚本在Linux/macOS上无法执行。建议始终使用VS Code、Sublime Text或Notepad++等支持换行符显示的编辑器,并在保存时确认换行符格式为LF(适用于.sh文件)。

2.4 文件权限模型差异

Windows和Unix-like系统有着根本不同的文件权限模型。在Unix系统(macOS/Linux)中,文件需要具有可执行权限(x位)才能作为脚本运行;而Windows则通过文件扩展名(.bat、.ps1、.exe)和ACL来控制执行权限。

# Unix权限模型 # 权限位: rwx rwx rwx (owner/group/others) # chmod 755 = rwxr-xr-x # chmod 700 = rwx------ # 设置可执行权限(Linux/macOS) chmod +x hook-script.sh # 验证权限 ls -la hook-script.sh # 输出: -rwxr-xr-x 1 user group 1234 May 8 10:00 hook-script.sh # Windows权限模型 # Windows不依赖权限位来判断可执行性,而是通过: # 1. 文件扩展名(.exe/.bat/.ps1/.cmd表示可执行) # 2. NTFS ACL(访问控制列表) # 3. 文件关联(如.py文件通过Python解释器执行) # 跨平台权限检查函数(Shell) #!/bin/bash is_executable() { local file="$1" if [ "$(uname)" = "Darwin" ] || [ "$(uname)" = "Linux" ]; then [ -x "$file" ] && return 0 || return 1 elif [ "$(uname -o 2>/dev/null)" = "Msys" ] || \ [ -n "$WINDIR" ]; then # Windows上通过扩展名判断 case "$file" in *.exe|*.bat|*.cmd|*.ps1|*.com) return 0 ;; *) return 1 ;; esac fi return 1 } # Python方式(推荐) import os import platform def is_executable(filepath): if platform.system() == "Windows": ext = os.path.splitext(filepath)[1].lower() return ext in {".exe", ".bat", ".cmd", ".ps1", ".com"} else: return os.access(filepath, os.X_OK)
Git跨平台权限处理: Git可以追踪文件的可执行权限位。在Windows上,可以通过git update-index --chmod=+x script.sh来设置脚本的可执行权限,这样当其他开发者在macOS/Linux上克隆仓库时,文件的执行权限会被正确设置。

三、跨平台Shell脚本技巧

编写跨平台Shell脚本需要掌握一些关键技巧,包括检测当前平台、使用条件分支执行不同命令、以及利用Python/Node.js等跨平台运行时替代不可移植的Shell命令。

3.1 使用uname检测当前平台

uname命令是跨平台Shell编程中最基础也是最重要的工具。它在所有Unix-like系统上可用,在Windows的Git Bash和WSL中同样可用。

#!/bin/bash # 跨平台平台检测函数 # 适用于 Bash/Zsh (Git Bash, WSL, macOS, Linux) detect_platform() { local os="" local arch="" case "$(uname -s)" in Linux) os="linux" # 检测是否为WSL(Windows Subsystem for Linux) if grep -qi microsoft /proc/version 2>/dev/null; then os="wsl" fi ;; Darwin) os="macos" ;; CYGWIN*|MINGW*|MSYS*) os="windows" ;; *) os="unknown" ;; esac # 检测架构 case "$(uname -m)" in x86_64|amd64) arch="x64" ;; aarch64|arm64) arch="arm64" ;; i386|i686) arch="x86" ;; *) arch="unknown" ;; esac echo "${os}_${arch}" } PLATFORM=$(detect_platform) echo "当前平台: $PLATFORM" # 可能的输出: # linux_x64 — Linux x86_64 # macos_arm64 — macOS Apple Silicon # windows_x64 — Windows (Git Bash/MSYS2) # wsl_x64 — WSL Ubuntu等

3.2 条件判断执行不同命令

检测到当前平台后,下一步是根据平台执行不同的命令。下面的示例展示了Hook脚本中常见的条件分支模式。

#!/bin/bash # 跨平台命令执行模式示例 # 检测平台 OS="$(uname -s)" # 模式一:使用case语句执行不同命令 case "$OS" in Darwin) # macOS 命令 SED_CMD="sed -i ''" GREP_CMD="grep -E" FIND_CMD="find -E" ECHO_CMD="echo" ;; Linux) # Linux 命令 SED_CMD="sed -i" GREP_CMD="grep -P" FIND_CMD="find" ECHO_CMD="echo" ;; CYGWIN*|MINGW*|MSYS*) # Windows (Git Bash) SED_CMD="sed -i" GREP_CMD="grep -P" FIND_CMD="find" ECHO_CMD="echo" ;; *) echo "不支持的平台: $OS" exit 1 ;; esac # 使用平台特定的命令 $SED_CMD 's/foo/bar/g' config.txt # 模式二:直接使用if条件判断 if [ "$OS" = "Darwin" ]; then # macOS特定的处理 DEFAULT_GATEWAY=$(route -n get default | grep gateway | awk '{print $2}') elif [ "$OS" = "Linux" ]; then # Linux特定的处理 DEFAULT_GATEWAY=$(ip route | grep default | awk '{print $3}') else # Windows (Git Bash) DEFAULT_GATEWAY=$(netstat -rn | grep "0.0.0.0" | head -1 | awk '{print $3}') fi echo "默认网关: $DEFAULT_GATEWAY" # 模式三:尝试多个命令(fallback模式) # 先尝试macOS命令,失败则尝试Linux命令 if ! command -v realpath &>/dev/null; then # macOS没有realpath,使用greadlink(如果安装了coreutils) if command -v greadlink &>/dev/null; then realpath() { greadlink -f "$1"; } else # 最后fallback:使用Python realpath() { python3 -c "import os.path; print(os.path.abspath('$1'))"; } fi fi # 模式四:检测命令是否存在 if command -v python3 &>/dev/null; then PYTHON=python3 elif command -v python &>/dev/null; then PYTHON=python elif command -v py &>/dev/null; then # Windows Python Launcher PYTHON=py else echo "错误:未找到Python" exit 1 fi echo "使用Python: $PYTHON"

3.3 使用Node.js/Python替代Shell实现跨平台

对于复杂的Hook逻辑,纯Shell脚本的跨平台维护成本会随着复杂度急剧上升。这时候使用Node.js或Python等跨平台运行时是更好的选择。它们的标准库已经封装了底层平台的差异,提供了统一的API。

# Node.js实现跨平台路径和Shell命令执行 # 保存为 cross-platform-hook.js const os = require("os"); const path = require("path"); const fs = require("fs"); const { execSync } = require("child_process"); // 检测当前平台 const platform = os.platform(); // 'win32', 'darwin', 'linux' const homedir = os.homedir(); // 跨平台获取用户主目录 const tmpdir = os.tmpdir(); // 跨平台获取临时目录 // 跨平台路径处理 const configPath = path.join(homedir, ".config", "myapp", "config.json"); console.log(`配置文件路径: ${configPath}`); // 跨平台执行Shell命令 function runCommand(cmd) { const options = {}; if (platform === "win32") { options.shell = "powershell.exe"; // Windows使用PowerShell } else { options.shell = "/bin/bash"; // Unix使用Bash } return execSync(cmd, options).toString().trim(); } // 跨平台获取环境变量 const envVar = platform === "win32" ? process.env.USERPROFILE || process.env.HOMEDRIVE + process.env.HOMEPATH : process.env.HOME; // 跨平台文件权限检查 function isExecutable(filePath) { try { fs.accessSync(filePath, fs.constants.X_OK); return true; } catch { // Windows上X_OK总是返回true,改用扩展名判断 if (platform === "win32") { const ext = path.extname(filePath).toLowerCase(); return [".exe", ".bat", ".cmd", ".ps1"].includes(ext); } return false; } } // 跨平台创建临时文件 function createTempFile(prefix, content) { const tmpFile = path.join( tmpdir, `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}` ); fs.writeFileSync(tmpFile, content, "utf8"); return tmpFile; } console.log(`当前平台: ${platform}`); console.log(`用户主目录: ${homedir}`); console.log(`临时目录: ${tmpdir}`); // 执行跨平台命令示例 if (platform === "win32") { console.log(runCommand("Get-ChildItem Env:USERNAME | Format-List")); } else { console.log(runCommand("whoami")); }
# Python实现跨平台Hook(推荐方案) # 保存为 cross-platform-hook.py import os import sys import platform import subprocess import tempfile import shutil from pathlib import Path # ============================================ # 跨平台环境检测 # ============================================ system = platform.system() # 'Windows', 'Darwin', 'Linux' machine = platform.machine() # 'x86_64', 'arm64' 等 is_windows = system == "Windows" is_macos = system == "Darwin" is_linux = system == "Linux" # ============================================ # 跨平台路径处理(推荐使用pathlib) # ============================================ # Pathlib跨平台路径构建 config_dir = Path.home() / ".config" / "myapp" config_file = config_dir / "settings.json" print(f"配置文件路径: {config_file}") # 自动使用正确的路径分隔符 # Python os.path也有跨平台能力 import os.path as osp config_dir = osp.join(os.path.expanduser("~"), ".config", "myapp") config_file = osp.join(config_dir, "settings.json") # 路径规范化(处理用户输入) raw_path = "C:/Users/name//project\\\\sub" normalized = osp.normpath(raw_path) print(f"规范化后: {normalized}") # ============================================ # 跨平台执行Shell命令 # ============================================ def run_command(cmd: str, shell: bool = True) -> str: """跨平台执行Shell命令""" try: result = subprocess.run( cmd, shell=shell, capture_output=True, text=True, check=False ) if result.returncode == 0: return result.stdout.strip() else: print(f"命令执行失败 (exit code: {result.returncode})") print(f"stderr: {result.stderr}") return "" except Exception as e: print(f"执行命令异常: {e}") return "" # 跨平台命令示例 if is_windows: # Windows上使用PowerShell username = run_command("powershell -Command \"[Environment]::UserName\"") else: username = run_command("whoami") print(f"当前用户: {username}") # ============================================ # 跨平台环境变量 # ============================================ def get_env_var(name: str, default: str = "") -> str: """跨平台获取环境变量(不区分大小写)""" if is_windows: # Windows环境变量名不区分大小写 for key in os.environ: if key.upper() == name.upper(): return os.environ[key] return os.environ.get(name, default) # 获取用户主目录(跨平台方法) home = os.path.expanduser("~") # 或者使用pathlib home = Path.home() # ============================================ # 跨平台文件操作 # ============================================ def set_executable(filepath: str) -> bool: """跨平台设置可执行权限""" if is_windows: # Windows不需要设置执行位 return True try: st = os.stat(filepath) os.chmod(filepath, st.st_mode | 0o111) # 添加执行权限 return True except Exception as e: print(f"设置执行权限失败: {e}") return False def get_temp_dir() -> str: """跨平台获取临时目录""" if is_windows: return os.environ.get("TEMP", os.environ.get("TMP", "/tmp")) else: return tempfile.gettempdir() # 创建临时文件(自动清理) with tempfile.NamedTemporaryFile( mode="w", suffix=".tmp", prefix="hook_", delete=False ) as f: f.write("跨平台临时文件内容") tmp_path = f.name print(f"临时文件: {tmp_path}") # ============================================ # 主逻辑 # ============================================ def main(): print(f"系统: {system} {machine}") print(f"Python: {sys.version}") print(f"用户主目录: {home}") print(f"临时目录: {get_temp_dir()}") # 跨平台Hook逻辑 if is_windows: print("执行Windows特定的Hook逻辑") run_command("powershell -Command \"Write-Host 'Hook执行成功'\"") elif is_macos: print("执行macOS特定的Hook逻辑") run_command("osascript -e 'display notification \"Hook执行成功\"'") elif is_linux: print("执行Linux特定的Hook逻辑") run_command('notify-send "Hook执行成功"') else: print(f"不支持的平台: {system}") sys.exit(1) if __name__ == "__main__": main()
语言选择建议:

3.4 推荐使用Python实现复杂Hook逻辑

在跨平台Hook开发中,Python是平衡开发效率和跨平台兼容性的最佳选择。以下是选择Python的几个核心理由以及一个完整的跨平台Git Hook示例。

#!/usr/bin/env python3 # ============================================= # 跨平台Git Pre-commit Hook(Python实现) # 功能:代码格式检查、敏感信息扫描 # 兼容:Windows / macOS / Linux # ============================================= import os import sys import subprocess import re import json from pathlib import Path # 配置 SENSITIVE_PATTERNS = [ r'password\s*=\s*['"'"'"]?\w+['"'"'"]?', r'api[_-]?key\s*=\s*['"'"'"]?\w+['"'"'"]?', r'secret\s*=\s*['"'"'"]?\w+['"'"'"]?', r'token\s*=\s*['"'"'"]?\w+['"'"'"]?', r'AKIA[0-9A-Z]{16}', # AWS Access Key ] SKIP_PATTERNS = [ r'\.md$', r'\.txt$', r'\.json$', ] def run_git_command(args): """跨平台运行Git命令""" try: result = subprocess.run( ['git'] + args, capture_output=True, text=True, check=True, shell=(os.name == 'nt') # Windows需shell=True ) return result.stdout.strip().split('\n') except subprocess.CalledProcessError as e: print(f"Git命令失败: {e}") sys.exit(1) def get_staged_files(): """获取暂存的文件列表(跨平台)""" files = run_git_command(['diff', '--cached', '--name-only', '--diff-filter=ACM']) return [f for f in files if f] # 过滤空字符串 def check_sensitive_info(file_path): """检查文件中的敏感信息""" try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: content = f.read() except Exception as e: print(f" 无法读取文件: {e}") return False found = False for i, line in enumerate(content.split('\n'), 1): for pattern in SENSITIVE_PATTERNS: if re.search(pattern, line, re.IGNORECASE): print(f" [警告] 第{i}行可能包含敏感信息: {line.strip()[:60]}...") found = True return found def check_trailing_whitespace(file_path): """检查行尾多余空白(跨平台)""" ext = Path(file_path).suffix if any(re.match(p, file_path) for p in SKIP_PATTERNS): return False try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines() except Exception: return False found = False for i, line in enumerate(lines, 1): stripped = line.rstrip('\n\r') if stripped != stripped.rstrip(): print(f" [风格] 第{i}行尾部有多余空白: {file_path}") found = True return found def check_executable_bit(file_path): """检查Shell脚本是否有可执行权限(跨平台)""" if os.name == 'nt': # Windows跳过检查 return False ext = Path(file_path).suffix if ext == '.sh' and not os.access(file_path, os.X_OK): print(f" [权限] Shell脚本缺少可执行权限: {file_path}") return True return False def main(): print("正在运行跨平台Pre-commit Hook检查...") print(f"当前平台: {sys.platform}") staged_files = get_staged_files() if not staged_files: print("没有需要检查的文件") return 0 print(f"检查 {len(staged_files)} 个文件...\n") errors = 0 for file_path in staged_files: if not os.path.exists(file_path): continue print(f"检查: {file_path}") # 敏感信息检查 if check_sensitive_info(file_path): errors += 1 # 行尾空白检查 if check_trailing_whitespace(file_path): errors += 1 # 可执行权限检查 if check_executable_bit(file_path): errors += 1 print() if errors > 0: print(f"发现 {errors} 个问题,请修复后重新提交") print("提示: 使用 git commit --no-verify 可跳过Hook检查") return 1 else: print("所有检查通过!") return 0 if __name__ == "__main__": sys.exit(main())
Python跨平台优势总结: (1) 标准库os/pathlib/subprocess已经封装了底层平台差异;(2) 无需安装额外依赖即可完成大部分跨平台任务;(3) 错误处理更加完善,不会因为一个命令失败就导致整个脚本崩溃;(4) 编码和文本处理更加可靠,不会遇到Shell脚本中的编码问题。

四、Windows PowerShell兼容

在跨平台Hook开发中,Windows是最特殊也最容易出问题的平台。传统上,Windows使用CMD(命令提示符)作为默认Shell,但现在PowerShell已经成为Windows上更强大的自动化工具。然而,PowerShell的语法和Unix Shell完全不同,这给跨平台带来了额外的挑战。

4.1 PowerShell vs CMD vs Bash的差异

三种Shell在语法上差异显著。下面的对比表可以帮助理解它们之间的关键差异。

功能Bash (Linux/macOS)PowerShell (Windows)CMD (Windows)
变量赋值name="value"$name="value"set name=value
变量引用$name$name%name%
命令执行结果$(command)$(command) 或 (command)`command`(已弃用)
条件判断if [ ... ]if (...) { ... }if ... ( ... )
循环for i in ...foreach ($i in ...)for %%i in (...)
函数定义function f() { }function f { }:label 或 call
字符串拼接"$a$b""$a$b" 或 -join%a%%b%
注释# 注释# 注释REM 注释
退出码exit 0/exit 1exit $LASTEXITCODEexit /b 0
管道cmd1 | cmd2cmd1 | cmd2(传递对象)cmd1 | cmd2(传递文本)
环境变量$HOME, $PATH$env:USERPROFILE%USERPROFILE%
# PowerShell与Bash语法对照示例 # ---- Bash ---- #!/bin/bash NAME="World" echo "Hello, ${NAME}!" if [ -f "/etc/passwd" ]; then echo "文件存在" fi for i in 1 2 3; do echo "Number: $i" done # ---- PowerShell ---- # 保存为 script.ps1 $NAME = "World" Write-Host "Hello, $NAME!" if (Test-Path "C:\Windows\System32\drivers\etc\hosts") { Write-Host "文件存在" } foreach ($i in 1..3) { Write-Host "Number: $i" } # ---- CMD (传统Windows) ---- REM 保存为 script.cmd SET NAME=World ECHO Hello, %NAME%! IF EXIST "C:\Windows\System32\drivers\etc\hosts" ( ECHO 文件存在 ) REM CMD没有方便的for循环语法

4.2 在PowerShell中兼容运行Unix命令

许多Hook脚本依赖于Unix工具(如grep、sed、awk、curl)。在Windows上,可以通过多种方式实现兼容运行。

# PowerShell中兼容运行Unix命令的多种方式 # 方式一:使用PowerShell原生替代命令 # grep → Select-String Get-Content "log.txt" | Select-String -Pattern "ERROR" # 等同于: grep "ERROR" log.txt # sed → -replace 操作符 (Get-Content "config.txt") -replace "foo", "bar" | Set-Content "config.txt" # 等同于: sed -i 's/foo/bar/g' config.txt # wc -l → Measure-Object Get-Content "log.txt" | Measure-Object -Line # 等同于: wc -l log.txt # sort → Sort-Object Get-Content "names.txt" | Sort-Object # 等同于: sort names.txt # head/tail → Select-Object Get-Content "log.txt" | Select-Object -First 10 # head -10 Get-Content "log.txt" | Select-Object -Last 10 # tail -10 # curl → Invoke-WebRequest / Invoke-RestMethod Invoke-RestMethod -Uri "https://api.example.com/data" -Method Get # 方式二:使用环境判断自动选择命令 function Invoke-CrossPlatformCommand { param( [string]$UnixCommand, [string]$PowerShellCommand ) if ($IsWindows -or $env:OS -eq "Windows_NT") { # 检查PowerShell等效命令是否存在 if (Get-Command $PowerShellCommand -ErrorAction SilentlyContinue) { return Invoke-Expression $PowerShellCommand } # 检查是否有Git Bash提供的Unix工具 $gitBashPath = "$env:ProgramFiles\Git\usr\bin" if (Test-Path "$gitBashPath\$UnixCommand.exe") { return & "$gitBashPath\$UnixCommand.exe" $args } } else { return & $UnixCommand $args } } # 方式三:使用WSL运行Unix命令(如果安装了WSL) function Invoke-WSLCommand { param([string]$Command) if (Get-Command wsl -ErrorAction SilentlyContinue) { return wsl -- $Command } else { Write-Warning "WSL未安装,请使用PowerShell原生命令" } } # 方式四:使用Git Bash中集成的Unix工具 # Git for Windows在安装时会包含MinGW64工具集 $unixTools = @( "$env:ProgramFiles\Git\usr\bin", "$env:ProgramFiles\Git\mingw64\bin", "${env:LOCALAPPDATA}\Programs\Git\usr\bin" ) function Find-UnixTool { param([string]$ToolName) foreach ($dir in $unixTools) { $toolPath = Join-Path $dir "$ToolName.exe" if (Test-Path $toolPath) { return $toolPath } } return $null } # 使用grep $grepPath = Find-UnixTool -ToolName "grep" if ($grepPath) { & $grepPath "ERROR" "log.txt" }

4.3 环境变量读取方式

环境变量的获取方式在不同平台和不同Shell中差异显著。正确获取环境变量是跨平台Hook的基础。

# ---- 不同平台/Shell的环境变量读取方式 ---- # Bash (Linux/macOS) # $HOME → /home/user 或 /Users/user # $USER → 用户名 # $PATH → 以冒号分隔的路径列表 # $PWD → 当前工作目录 # $SHELL → 当前Shell路径 # PowerShell (Windows) # $env:USERPROFILE → C:\Users\用户名 # $env:USERNAME → 用户名 # $env:PATH → 以分号分隔的路径列表 # $env:APPDATA → C:\Users\用户名\AppData\Roaming # $env:LOCALAPPDATA → C:\Users\用户名\AppData\Local # $env:TEMP → C:\Users\用户名\AppData\Local\Temp # CMD (Windows) # %USERPROFILE% → C:\Users\用户名 # %USERNAME% → 用户名 # %APPDATA% → C:\Users\用户名\AppData\Roaming # ---- 跨平台环境变量适配函数(PowerShell) ---- function Get-CrossPlatformHome { if ($IsWindows -or $env:OS -eq "Windows_NT") { return $env:USERPROFILE } else { return $env:HOME } } function Get-CrossPlatformPath { param([string[]]$PathComponents) if ($IsWindows -or $env:OS -eq "Windows_NT") { return $PathComponents -join "\" } else { return $PathComponents -join "/" } } # ---- 跨平台环境变量适配函数(Bash) ---- get_home_dir() { if [ -n "$HOME" ]; then echo "$HOME" elif [ -n "$USERPROFILE" ]; then echo "$USERPROFILE" else echo "$(cd ~ && pwd)" fi } # ---- Python跨平台环境变量读取 ---- function get_env_cross_platform() { python3 -c " import os # 跨平台获取指定环境变量(不区分大小写检查) def get_env(name): val = os.environ.get(name) if val is None and os.name == 'nt': # Windows环境变量名不区分大小写 for key in os.environ: if key.upper() == name.upper(): return os.environ[key] return val print(get_env('HOME') or get_env('USERPROFILE') or '') " }

4.4 PowerShell的退出码和错误处理

退出码(exit code / exit code)是Hook脚本中判断执行成功与否的关键。PowerShell的退出码处理方式与Bash有显著差异,如果不注意,可能导致Hook逻辑判断错误。

# ---- Bash退出码处理 ---- #!/bin/bash some_command if [ $? -eq 0 ]; then echo "命令执行成功" else echo "命令执行失败,退出码: $?" exit 1 fi # ---- PowerShell退出码处理 ---- # 重要差异:PowerShell的错误处理更加复杂 # 方式一:使用$LASTEXITCODE(仅适用于外部程序) notepad.exe nonexistent.txt 2>$null Write-Host "退出码: $LASTEXITCODE" # 正确获取外部程序退出码 # 方式二:使用$?(自动变量,表示上一个命令是否成功) Test-Path "C:\Windows" Write-Host "上一个命令是否成功: $?" # True/False # 方式三:使用try/catch/finally(推荐) try { # 使用 -ErrorAction Stop 确保错误会被捕获 Get-Content "config.json" -ErrorAction Stop Write-Host "文件读取成功" } catch [System.IO.FileNotFoundException] { Write-Host "文件未找到: $_" exit 1 } catch { Write-Host "其他错误: $_" exit 2 } finally { Write-Host "无论成功或失败都会执行" } # 方式四:PowerShell中的错误处理偏好设置 # $ErrorActionPreference 控制错误处理行为 $ErrorActionPreference = "Stop" # 遇到任何错误立即停止 # $ErrorActionPreference = "Continue" # 默认行为:显示错误但继续执行 # $ErrorActionPreference = "SilentlyContinue" # 静默忽略错误 # 方式五:执行批处理文件并检查退出码 function Invoke-BatchFile { param([string]$FilePath) $process = Start-Process -FilePath $FilePath -NoNewWindow -Wait -PassThru return $process.ExitCode } # ---- 跨平台退出码处理(PowerShell版) ---- function Invoke-CrossPlatformScript { param( [string]$ScriptPath, [string[]]$Arguments ) $ext = [System.IO.Path]::GetExtension($ScriptPath).ToLower() switch ($ext) { ".sh" { # 执行Shell脚本 if (Get-Command "bash" -ErrorAction SilentlyContinue) { $result = bash $ScriptPath @Arguments return $LASTEXITCODE } elseif (Get-Command "wsl" -ErrorAction SilentlyContinue) { $result = wsl -- $ScriptPath @Arguments return $LASTEXITCODE } else { Write-Error "未找到Bash或WSL,无法执行Shell脚本" return 1 } } ".ps1" { # 执行PowerShell脚本 try { & $ScriptPath @Arguments -ErrorAction Stop return 0 } catch { Write-Error "PowerShell脚本执行失败: $_" return 1 } } ".py" { # 执行Python脚本 if (Get-Command "python" -ErrorAction SilentlyContinue) { $result = python $ScriptPath @Arguments return $LASTEXITCODE } else { Write-Error "未找到Python" return 1 } } default { Write-Error "不支持的脚本类型: $ext" return 1 } } } # ---- 关键注意事项 ---- # 1. PowerShell中,exit 0不一定表示成功 # 如果脚本之前发生了终止错误,exit 0可能不会被执行到 # 2. 外部程序(如git、node)的退出码通过$LASTEXITCODE获取 # PowerShell cmdlet不会设置$LASTEXITCODE # 3. 在PowerShell中,非零退出码不会自动停止脚本执行 # 需要显式检查 $LASTEXITCODE 并处理 # 4. 使用 trap 关键字可以捕获所有终止错误 trap { Write-Error "捕获到未处理的错误: $_" exit 1 }
PowerShell踩坑提醒:

五、跨平台测试策略

编写跨平台Hook脚本只是第一步,真正的挑战在于确保它在所有目标平台上都能正确工作。系统的测试策略是跨平台兼容性的最后一道保障。

5.1 在多个平台上测试Hook脚本

最可靠的跨平台测试方式是在所有目标平台上实际运行测试。以下是一些实用的测试方法和工具。

# 跨平台手动测试清单 # 在每个目标平台上执行以下测试: # 1. 基本功能测试 ./hook-script.sh --help # 帮助信息是否正常显示 ./hook-script.sh --version # 版本信息 ./hook-script.sh dry-run # 试运行模式测试 # 2. 路径处理测试 ./hook-script.sh --config "./config/test.json" # 相对路径 ./hook-script.sh --config "/absolute/path/config.json" # 绝对路径 ./hook-script.sh --config "~/config.json" # 家目录路径 ./hook-script.sh --config "C:\Users\test\config.json" # Windows路径 # 3. 环境变量测试 HOME=/custom/home ./hook-script.sh # 自定义HOME USERPROFILE=/custom/profile ./hook-script.sh # 自定义USERPROFILE TEMP=/custom/tmp ./hook-script.sh # 自定义临时目录 # 4. 错误处理测试 ./hook-script.sh --config "/nonexistent/path" # 不存在的路径 ./hook-script.sh --invalid-flag # 无效参数 ./hook-script.sh # 缺少必要参数
# 跨平台测试脚本示例 # 保存为 test-cross-platform.sh #!/bin/bash # 跨平台测试脚本 - 在多个平台上验证Hook脚本的兼容性 TEST_SCRIPT="./cross-platform-hook.sh" PLATFORMS=("windows" "macos" "linux") TESTS_PASSED=0 TESTS_FAILED=0 # 颜色输出 RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' # No Color print_result() { local test_name="$1" local result="$2" if [ "$result" = "PASS" ]; then echo -e "${GREEN}[PASS]${NC} $test_name" TESTS_PASSED=$((TESTS_PASSED + 1)) else echo -e "${RED}[FAIL]${NC} $test_name" TESTS_FAILED=$((TESTS_FAILED + 1)) fi } # 测试1:检测平台是否正确 echo "=== 测试: 平台检测 ===" CURRENT_PLATFORM="" case "$(uname -s)" in Linux) CURRENT_PLATFORM="linux" ;; Darwin) CURRENT_PLATFORM="macos" ;; CYGWIN*|MINGW*|MSYS*) CURRENT_PLATFORM="windows" ;; esac print_result "平台检测 ($CURRENT_PLATFORM)" "PASS" # 测试2:路径分隔符处理 echo "=== 测试: 路径处理 ===" if [ "$CURRENT_PLATFORM" = "windows" ]; then # Windows上测试Python路径处理 python3 -c " import os p = os.path.join('config', 'settings.json') assert '\\\\' in p or '/' in p, '路径连接失败' print(f'路径连接测试通过: {p}') " print_result "路径分隔符处理" "PASS" else # Unix上测试路径处理 TEST_PATH="/tmp/test-dir" mkdir -p "$TEST_PATH" if [ -d "$TEST_PATH" ]; then print_result "路径创建测试" "PASS" fi rmdir "$TEST_PATH" fi # 测试3:环境变量读取 echo "=== 测试: 环境变量 ===" HOME_DIR="" if [ -n "$HOME" ]; then HOME_DIR="$HOME" elif [ -n "$USERPROFILE" ]; then HOME_DIR="$USERPROFILE" fi if [ -n "$HOME_DIR" ]; then print_result "环境变量读取 ($HOME_DIR)" "PASS" else print_result "环境变量读取" "FAIL" fi # 测试4:执行权限检测 echo "=== 测试: 执行权限 ===" if [ "$CURRENT_PLATFORM" != "windows" ]; then chmod +x "$TEST_SCRIPT" 2>/dev/null if [ -x "$TEST_SCRIPT" ]; then print_result "执行权限设置" "PASS" else print_result "执行权限设置" "FAIL" fi else print_result "执行权限(Windows跳过)" "PASS" fi # 测试5:退出码处理 echo "=== 测试: 退出码 ===" python3 -c "import sys; sys.exit(0)" if [ $? -eq 0 ]; then print_result "退出码(成功)" "PASS" else print_result "退出码(成功)" "FAIL" fi python3 -c "import sys; sys.exit(1)" if [ $? -ne 0 ]; then print_result "退出码(失败)" "PASS" else print_result "退出码(失败)" "FAIL" fi # 总结 echo "" echo "========== 测试结果 ==========" echo -e "${GREEN}通过: $TESTS_PASSED${NC}" echo -e "${RED}失败: $TESTS_FAILED${NC}" echo "==============================" if [ $TESTS_FAILED -eq 0 ]; then echo "所有测试通过!" exit 0 else echo "存在测试失败,请检查" exit 1 fi

5.2 使用CI自动测试跨平台兼容性

手动在多个平台上测试虽然可靠,但效率低下。使用CI(持续集成)服务可以自动化跨平台测试,在每次代码变更时自动在所有目标平台上运行测试。

# GitHub Actions 跨平台测试配置 # 保存为 .github/workflows/cross-platform-test.yml name: 跨平台Hook兼容性测试 on: push: branches: [ main, develop ] paths: - 'hooks/**' - '.github/workflows/**' pull_request: branches: [ main ] paths: - 'hooks/**' jobs: test: name: 测试 (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false # 一个平台失败不影响其他平台 matrix: os: [ubuntu-latest, macos-latest, windows-latest] shell: [bash, pwsh] exclude: # Windows上排除Bash测试(使用PowerShell替代) - os: windows-latest shell: bash include: # Windows上增加PowerShell测试 - os: windows-latest shell: pwsh # 也测试Windows上的PowerShell 5.1 - os: windows-latest shell: powershell steps: - name: 检出代码 uses: actions/checkout@v4 - name: 设置Python uses: actions/setup-python@v5 with: python-version: '3.12' - name: 设置Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: 显示系统信息 shell: ${{ matrix.shell }} run: | echo "操作系统: $(uname -s 2>/dev/null || echo 'Windows')" echo "架构: $(uname -m 2>/dev/null || echo 'x86_64')" echo "Shell: $SHELL" echo "Python: $(python3 --version 2>/dev/null || python --version)" echo "Node.js: $(node --version 2>/dev/null || echo 'N/A')" - name: 设置Hook脚本执行权限 if: runner.os != 'Windows' shell: ${{ matrix.shell }} run: | chmod +x hooks/*.sh chmod +x hooks/**/*.sh - name: 运行Shell兼容性测试 shell: ${{ matrix.shell }} run: | if [ "${{ runner.os }}" = "Windows" ]; then # Windows使用PowerShell测试 powershell -ExecutionPolicy Bypass -File tests/run-tests.ps1 else # Unix使用Bash测试 bash tests/run-tests.sh fi - name: 运行Python跨平台测试 shell: ${{ matrix.shell }} run: | python3 tests/test_cross_platform.py - name: 运行Node.js跨平台测试 shell: ${{ matrix.shell }} run: | node tests/test_cross_platform.js - name: 路径兼容性测试 shell: ${{ matrix.shell }} run: | # 测试含空格的路径 python3 -c " import tempfile, os from pathlib import Path # 创建含空格的测试目录 test_dir = tempfile.mkdtemp(suffix=' test dir') test_file = Path(test_dir) / 'config file.json' test_file.write_text('{\"test\": true}') assert test_file.exists(), f'文件不存在: {test_file}' print(f'路径兼容性测试通过: {test_file}') os.rmdir(test_dir) " - name: 环境变量兼容性测试 shell: ${{ matrix.shell }} run: | python3 -c " import os home = os.path.expanduser('~') assert home and os.path.isdir(home), f'用户主目录无效: {home}' print(f'用户主目录: {home}') temp = tempfile.gettempdir() print(f'临时目录: {temp}') " - name: 收集测试结果 if: always() shell: bash run: | echo "## 跨平台测试结果 (${{ matrix.os }})" >> $GITHUB_STEP_SUMMARY echo "- Shell: ${{ matrix.shell }}" >> $GITHUB_STEP_SUMMARY echo "- 状态: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
CI跨平台测试最佳实践:

5.3 常见跨平台问题和解决方案库

以下是跨平台Hook开发中最常见的问题及其解决方案。这些问题经过实践的检验,可以作为快速排查问题的参考手册。

问题现象原因解决方案
脚本在Windows上无法执行"command not found" 或 "is not recognized"Shebang行不可用,或文件关联未配置使用 .ps1 / .bat 扩展名,或配置Python/Node.js文件关联
换行符错误"$'\r': command not found"CRLF换行符在Unix Shell中被解释为命令Git配置core.autocrlf,或设置.gitattributes中的eol=lf
路径中含空格脚本解析路径时截断路径未用引号包裹始终使用 "$variable" 或 "$(command)" 引用路径变量
环境变量名不匹配$HOME为空(Windows)Windows使用%USERPROFILE%而非$HOME使用条件判断回退,或通过Python获取
反斜杠转义路径中的\被解释为转义字符Shell将\视为转义字符使用正斜杠/代替反斜杠,或双写反斜杠
curl或wget不存在Windows上无curl/wgetWindows 10 1803之前未预装curl使用PowerShell的Invoke-WebRequest,或通过Python requests
文件权限不足Permission denied(Unix)未设置可执行权限git add时设置chmod +x,或在.gitattributes中指定
Python编码问题UnicodeDecodeError默认编码在不同平台不同始终指定encoding参数,如open(file, 'r', encoding='utf-8')
Node.js路径规范化Windows路径中的\被当作转义JavaScript字符串中的\需要转义使用path.normalize()或path.join(),避免手动拼接路径
PowerShell执行策略".ps1 cannot be loaded"默认执行策略为Restricted使用powershell -ExecutionPolicy Bypass -File script.ps1
最容易忽略的陷阱:
  1. Windows大小写不敏感: 在Windows上,文件名"Config.json"和"config.json"指向同一个文件;在Linux上则不是。这可能导致在Windows上测试通过的脚本在Linux上因文件名大小写错误而失败
  2. WSL的混合环境: WSL虽然是Linux环境,但可以访问Windows文件系统(/mnt/c/)。然而,跨文件系统操作时文件权限和性能会受到影响
  3. Symlink差异: 创建符号链接在Windows上需要管理员权限或开发者模式,而Unix系统上普通用户即可创建
  4. 中文路径/文件名: 不同系统和Shell对Unicode文件名的支持程度不同,GBK/UTF-8编码混用可能导致乱码或文件找不到
  5. shebang行陷阱: Windows不识别shebang行(#!/usr/bin/env python3),需要通过文件关联或显式调用解释器来执行脚本

5.4 兼容性检查清单

在部署跨平台Hook脚本之前,使用以下检查清单逐一验证,可以大幅降低上线后出现兼容性问题的概率。

# ============================================= # 跨平台Hook兼容性检查清单 # 在每个目标平台上逐一检查 # ============================================= # [ ] 1. 基本执行 # [ ] 脚本能否在目标平台上无错误执行 # [ ] shebang行正确(或用正确的解释器调用) # [ ] 文件权限正确(Unix上可执行) # [ ] 2. 路径处理 # [ ] 所有路径变量用引号包裹 # [ ] 没有硬编码的路径分隔符 # [ ] 使用os.path.join()或pathlib处理路径(Python) # [ ] 或使用path.join()处理路径(Node.js) # [ ] 测试了含空格的路径 # [ ] 测试了含Unicode字符的路径 # [ ] 用户主目录获取方式支持多平台 # [ ] 3. 环境变量 # [ ] 没有使用平台特定的环境变量名 # [ ] $HOME / %USERPROFILE% 有降级处理 # [ ] PATH分隔符正确处理(Unix: :, Windows: ;) # [ ] 环境变量读取有默认值 # [ ] 4. Shell命令 # [ ] 没有使用平台不存在的命令 # [ ] 或使用了条件判断/fallback机制 # [ ] grep/sed/awk等命令有跨平台替代方案 # [ ] curl/wget在Windows上有替代(Invoke-WebRequest) # [ ] 5. 退出码和错误处理 # [ ] 退出码在所有平台上语义一致 # [ ] 错误信息在不同平台都能正确显示 # [ ] stderr重定向行为正确 # [ ] 异常/错误被正确捕获和处理 # [ ] 6. 文件操作 # [ ] 文件读写使用跨平台方式(指定encoding) # [ ] 临时文件创建使用跨平台API # [ ] 文件权限处理有平台判断 # [ ] 换行符处理正确 # [ ] 7. 网络请求 # [ ] curl命令在Windows上有替代方案 # [ ] 或使用Python requests / Node.js fetch # [ ] SSL证书验证在所有平台上正常工作 # [ ] 代理设置兼容多平台 # [ ] 8. CI测试 # [ ] GitHub Actions(或等效CI)配置了多平台测试 # [ ] matrix中包含ubuntu/macos/windows # [ ] 同时测试Bash和PowerShell # [ ] 至少有一个平台运行了完整的端到端测试 # [ ] 9. 文档 # [ ] README中注明了支持的平台 # [ ] 列出了已知的兼容性限制 # [ ] 提供了各平台的安装/配置说明 # [ ] 有常见问题(FAQ)或排错指南 # [ ] 10. 回归测试 # [ ] 所有平台上的测试全部通过 # [ ] 代码变更后重新运行所有平台测试 # [ ] 第三方依赖更新后重新验证兼容性 # [ ] 操作系统大版本更新后重新测试

核心要点总结:

1. 跨平台兼容性的三个核心维度:文件路径、Shell命令、环境变量,每个维度都需要针对不同平台做适配

2. 对于复杂Hook逻辑,推荐使用Python实现,其标准库(os/pathlib/subprocess)已封装底层平台差异

3. Windows上优先使用PowerShell,并通过条件判断兼容Git Bash/WSL环境

4. 换行符(CRLF vs LF)是最隐蔽的跨平台陷阱,务必通过Git配置和.gitattributes统一管理

5. CI自动化测试是保障跨平台兼容性的最有效手段,建议在GitHub Actions中配置ubuntu/macos/windows三平台矩阵测试

6. 始终使用引号包裹路径变量,始终指定文件编码,始终为环境变量提供默认值

六、进一步思考

1. 随着容器化(Docker)的普及,是否意味着跨平台兼容性问题不再重要? - 容器化确实在一定程度上隔离了底层操作系统差异,但Hook脚本本身仍然需要在宿主机上执行(特别是Git Hooks和系统级事件触发)。此外,在CI/CD场景中,不同的Runner(GitHub Actions的ubuntu/macos/windows)依然需要跨平台兼容的脚本。 2. 如何建立一个跨平台Hook的标准化开发流程? - 建议遵循TDD(测试驱动开发)理念:先为所有目标平台编写测试用例,再开发Hook逻辑,确保每个平台都能通过测试。使用CI自动执行这些测试。 3. 跨平台兼容性和代码可维护性如何平衡? - 过多的平台条件判断会降低代码可读性。建议将平台相关的逻辑抽象成独立的函数或模块,主流程中只调用这些抽象接口。当平台差异过大时,可以考虑为不同平台维护独立的实现文件。 4. 如何在不拥有所有平台设备的情况下进行跨平台测试? - 使用云CI服务(GitHub Actions、GitLab CI、CircleCI)免费获取各平台运行环境 - 使用Docker或虚拟机本地搭建测试环境 - 使用交叉编译和静态分析工具检测平台相关问题 5. WSL的普及是否改变了Windows上的Hook开发方式? - WSL确实为Windows开发者提供了原生的Linux体验,但不建议将WSL作为唯一的Windows Hook兼容方案。因为: - WSL需要用户额外安装和配置 - 在CI环境中通常不使用WSL - WSL和原生Windows文件系统之间的性能问题 - 更好的策略是让Hook脚本同时在原生Windows(PowerShell)和WSL环境中都能工作。