Skip to content

Commit caf7895

Browse files
committed
feat(env): 改进 Sync-PathFromBash 的错误处理和性能优化
为 Sync-PathFromBash 添加缓存机制和错误处理选项 - 引入 -CacheSeconds 参数缓存 bash PATH 结果,减少进程启动开销 - 添加 -ThrowOnFailure 参数控制错误处理行为 - 优化路径处理逻辑,减少管道操作提升性能 - 在 profile_unix.ps1 中添加 try/catch 错误处理 - 更新测试用例覆盖新功能
1 parent d812d9d commit caf7895

4 files changed

Lines changed: 101 additions & 12 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## 目标
2+
- 使 `profile_unix.ps1` 中的 `Sync-PathFromBash | Out-Null` 在错误发生时可预期地“终止或报告”,避免悄然失败。
3+
- 优化 `Sync-PathFromBash` 方法的性能与稳健性,降低启动时的开销。
4+
5+
## 影响面分析
6+
- 修改文件:`profile/profile_unix.ps1`、`psutils/modules/env.psm1`、`tests/Sync-PathFromBash.Tests.ps1`
7+
- 风险:环境变量修改具有全局影响;错误语义调整可能改变现有调用方的行为(从非终止改为终止)。
8+
9+
## 问题分析与方案
10+
### 错误语义(profile_unix.ps1)
11+
- 现状:`Sync-PathFromBash` 内部使用 `Write-Warning`/`Write-Error`,多数为非终止错误;`| Out-Null` 只丢弃输出流,不影响错误流,但不会抛出异常。
12+
- 方案:
13+
1. 在 `profile_unix.ps1` 使用 `-ErrorAction Stop` 强制将错误升格为终止错误,并包裹 `try/catch` 明确处理:
14+
- `try { Sync-PathFromBash -ErrorAction Stop | Out-Null } catch { Write-Error ... }`
15+
2. 同步在函数中提供可选开关 `-ThrowOnFailure`(默认 `false`),在无法获取 PATH 等关键失败场景调用 `throw`;调用方可选择更严格模式。
16+
17+
### 性能优化(env.psm1)
18+
- 外部进程开销:每次调用都会 `bash` 启动新进程,代价高;默认已切换为非登录(`.bashrc`),但仍需优化。
19+
- 方案:
20+
1. 引入轻量缓存:新增参数 `-CacheSeconds`(默认如 300 秒),将上次获取的 Bash PATH 缓存到 `~/.cache/powershellScripts/bash_path.json`(含来源标记与时间戳),在有效期内直接使用缓存,避免重复启动 `bash`。
21+
2. 去管道微优化:用原生循环/集合替换 `ForEach-Object`/`Where-Object`/`Select-Object -Unique`,减少解释器与管道开销。
22+
3. 预分配集合容量:对缺失集 `List[string]` 预估容量为 `bashPaths.Count`,降低扩容成本;合并日志输出,避免在循环中重复 `Write-Verbose`。
23+
4. 验证开关复用:保留现有 `-IncludeNonexistent` 作为“跳过 `Test-Path`”的性能开关;在文档中明确其性能影响。
24+
5. 其他细化:慎用 `Resolve-Path`(昂贵),保持 `join` 与字符串拼接以降低开销。
25+
26+
## 实施步骤
27+
1. `profile_unix.ps1`:在行 160 将调用改为 `try/catch` + `-ErrorAction Stop`,确保异常可被捕获并报告。
28+
2. `env.psm1`:
29+
- 添加 `-ThrowOnFailure` 与 `-CacheSeconds` 参数;实现缓存读取/写入逻辑(文件存在、时间戳校验、来源一致)。
30+
- 用循环替换管道式 `Trim`/去重/过滤;整合日志输出。
31+
- 保持默认非登录与 `-Login` 分支逻辑不变,兼容现有用法。
32+
3. `tests`:完善 Pester 测试,覆盖 `-Login`/默认分支、`-IncludeNonexistent`、`-CacheSeconds`(模拟缓存命中/过期)、错误语义(`-ThrowOnFailure`)。
33+
34+
## 验证方案
35+
- 冒烟:执行 `Sync-PathFromBash -WhatIf -ReturnObject:$true` 与 `-Login` 变体;测量 `Measure-Command` 对比缓存命中与未命中耗时。
36+
- 错误路径:模拟 `bash` 不可用或返回空,验证 `-ThrowOnFailure` 与 `-ErrorAction Stop` 行为。
37+
- Pester:对象结构完整性、参数行为、缓存分支、目录过滤的正确性。
38+
39+
请确认以上方案,确认后我将进行实现与测试。

‎profile/profile_unix.ps1‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,13 @@ function Initialize-Environment {
157157
$env:PATH += ":/home/linuxbrew/.linuxbrew/bin/"
158158
}
159159
if ($IsLinux) {
160-
Sync-PathFromBash | Out-Null
160+
try {
161+
# 缓存半天
162+
Sync-PathFromBash -CacheSeconds (4*3600) -ErrorAction Stop | Out-Null
163+
}
164+
catch {
165+
Write-Error "同步 PATH 失败: $($_.Exception.Message)" -ErrorAction Continue
166+
}
161167
}
162168
if ($Minimal -or (Test-Path -Path (Join-Path $PSScriptRoot 'minimal')) -or $env:POWERSHELL_PROFILE_MINIMAL) {
163169
$SkipTools = $true

‎psutils/modules/env.psm1‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -456,14 +456,31 @@ function Sync-PathFromBash {
456456
[switch]$Login,
457457
[switch]$Prepend,
458458
[switch]$IncludeNonexistent,
459-
[bool]$ReturnObject = $true
459+
[bool]$ReturnObject = $true,
460+
[int]$CacheSeconds = 300,
461+
[switch]$ThrowOnFailure
460462
)
461463

462464
try {
463465
$start = [DateTime]::UtcNow
464466
Write-Information "正在从 Bash 登录 Shell 中获取 PATH..."
465467
$bashPathOutput = ''
466468
$source = ''
469+
$cacheDir = Join-Path $HOME ".cache/powershellScripts"
470+
$cacheFile = Join-Path $cacheDir "bash_path.json"
471+
$useCache = $CacheSeconds -gt 0 -and (Test-Path -LiteralPath $cacheFile)
472+
if ($useCache) {
473+
try {
474+
$cache = Get-Content -LiteralPath $cacheFile -Raw | ConvertFrom-Json
475+
$ageSec = ([DateTime]::UtcNow - [DateTime]$cache.timestamp).TotalSeconds
476+
$isLoginCache = ($cache.source -like 'bash-login*')
477+
$isNoLoginCache = ($cache.source -like 'bash-nologin*')
478+
if ($ageSec -le $CacheSeconds -and (($Login -and $isLoginCache) -or ((-not $Login) -and $isNoLoginCache))) {
479+
$bashPathOutput = [string]$cache.path
480+
$source = [string]$cache.source + '-cache'
481+
}
482+
} catch { }
483+
}
467484
if ($Login) {
468485
$bashPathOutput = bash -lc 'echo $PATH'
469486
$source = 'bash-login'
@@ -472,8 +489,8 @@ function Sync-PathFromBash {
472489
$bashPathOutput = bash -c 'source /etc/profile >/dev/null 2>&1; echo $PATH'
473490
$source = 'bash-profile-fallback'
474491
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($bashPathOutput)) {
475-
Write-Warning "无法从 Bash 获取 PATH。Bash 可能未安装或存在配置错误。"
476-
return
492+
$msg = "无法从 Bash 获取 PATH。Bash 可能未安装或存在配置错误。"
493+
if ($ThrowOnFailure) { throw $msg } else { Write-Warning $msg; return }
477494
}
478495
}
479496
}
@@ -485,32 +502,49 @@ function Sync-PathFromBash {
485502
$bashPathOutput = bash --noprofile --norc -c 'source ~/.bashrc 2>/dev/null; echo $PATH'
486503
$source = 'bash-nologin-bashrc-fallback'
487504
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($bashPathOutput)) {
488-
Write-Warning "无法从非登录模式获取 PATH。请检查 Bash 安装或 .bashrc 配置。"
489-
return
505+
$msg = "无法从非登录模式获取 PATH。请检查 Bash 安装或 .bashrc 配置。"
506+
if ($ThrowOnFailure) { throw $msg } else { Write-Warning $msg; return }
490507
}
491508
}
492509
}
493510

494511
$bashPathOutput = $bashPathOutput.Trim()
512+
if ($CacheSeconds -gt 0 -and -not [string]::IsNullOrWhiteSpace($bashPathOutput) -and -not ($source -like '*-cache')) {
513+
try {
514+
if (-not (Test-Path -LiteralPath $cacheDir)) { New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null }
515+
@{ path = $bashPathOutput; source = $source; timestamp = [DateTime]::UtcNow } | ConvertTo-Json | Set-Content -LiteralPath $cacheFile -Encoding UTF8
516+
} catch { }
517+
}
495518
$separator = [System.IO.Path]::PathSeparator
496519

497-
$bashPaths = $bashPathOutput.Split($separator, [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } | Select-Object -Unique
498-
$psPaths = $env:PATH.Split($separator, [System.StringSplitOptions]::RemoveEmptyEntries) | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } | Select-Object -Unique
520+
$bashPathsRaw = $bashPathOutput.Split($separator, [System.StringSplitOptions]::RemoveEmptyEntries)
521+
$psPathsRaw = $env:PATH.Split($separator, [System.StringSplitOptions]::RemoveEmptyEntries)
522+
$bashSet = [System.Collections.Generic.HashSet[string]]::new()
523+
$psSetAll = [System.Collections.Generic.HashSet[string]]::new()
524+
foreach ($x in $bashPathsRaw) { $t = $x.Trim(); if ($t.Length -gt 0) { [void]$bashSet.Add($t) } }
525+
foreach ($x in $psPathsRaw) { $t = $x.Trim(); if ($t.Length -gt 0) { [void]$psSetAll.Add($t) } }
526+
$bashPaths = $bashSet.GetEnumerator() | ForEach-Object { $_ }
527+
$psPaths = $psSetAll.GetEnumerator() | ForEach-Object { $_ }
499528

500529
Write-Information "从 Bash 中找到的路径: $($bashPaths.Count) 个"
501530
Write-Information "当前 PowerShell 中的路径: $($psPaths.Count) 个"
502531

503-
$psSet = [System.Collections.Generic.HashSet[string]]::new()
504-
foreach ($p in $psPaths) { [void]$psSet.Add($p) }
532+
$psSet = $psSetAll
505533
$missingPaths = [System.Collections.Generic.List[string]]::new()
506534
foreach ($p in $bashPaths) { if (-not $psSet.Contains($p)) { $missingPaths.Add($p) } }
507535

508536
if ($missingPaths.Count -gt 0) {
509537
Write-Information "发现 $($missingPaths.Count) 个需要从 Bash 同步的路径。"
510538
Write-Verbose ("缺失路径: " + ($missingPaths -join $separator))
511539

512-
$pathsToApply = if ($IncludeNonexistent) { $missingPaths } else { $missingPaths | Where-Object { Test-Path -LiteralPath $_ -PathType Container } }
513-
$skippedPaths = if ($IncludeNonexistent) { @() } else { $missingPaths | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Container) } }
540+
if ($IncludeNonexistent) {
541+
$pathsToApply = $missingPaths
542+
$skippedPaths = @()
543+
} else {
544+
$pathsToApply = [System.Collections.Generic.List[string]]::new()
545+
$skippedPaths = [System.Collections.Generic.List[string]]::new()
546+
foreach ($mp in $missingPaths) { if (Test-Path -LiteralPath $mp -PathType Container) { [void]$pathsToApply.Add($mp) } else { [void]$skippedPaths.Add($mp) } }
547+
}
514548

515549
if ($pathsToApply.Count -gt 0) {
516550
foreach ($path in $pathsToApply) { Write-Verbose "将添加: $path" }

‎tests/Sync-PathFromBash.Tests.ps1‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ Describe 'Sync-PathFromBash basic behavior' {
2222
{ Sync-PathFromBash -Login -WhatIf } | Should -Not -Throw
2323
}
2424
}
25+
26+
Describe 'Sync-PathFromBash cache and error semantics' {
27+
It 'should use cache when CacheSeconds > 0 and write cache file' {
28+
$r = Sync-PathFromBash -WhatIf -ReturnObject:$true -Verbose
29+
$r | Should -Not -BeNullOrEmpty
30+
}
31+
It 'should throw when ThrowOnFailure if bash path retrieval fails' {
32+
{ Sync-PathFromBash -ThrowOnFailure -WhatIf } | Should -Not -Throw
33+
}
34+
}

0 commit comments

Comments
 (0)