Skip to content

Commit 682c6b7

Browse files
committed
feat: 增加同步时自动清理失效旧 shim 的功能
- 新增 `Get-ManagedBinShimMetadata` 函数,用于读取 shim 文件头部的源脚本元数据 - 新增 `Remove-StaleManagedBinScripts` 函数,在同步时自动清理源脚本已删除或已重命名导致的旧 shim - 更新脚本描述文档,明确说明同步时的自动清理行为 - 添加单元测试,验证重命名源脚本时能正确清理旧 shim 并保留其他无关 shim
1 parent ffe9b5a commit 682c6b7

2 files changed

Lines changed: 155 additions & 19 deletions

File tree

Manage-BinScripts.ps1

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,49 @@
22

33
<#
44
.SYNOPSIS
5-
管理bin目录脚本映射的自动化工具
5+
管理 `bin` 目录脚本 shim 的自动化工具。
66
77
.DESCRIPTION
8-
该脚本用于自动生成bin目录下的脚本映射,将项目中的PowerShell脚本
9-
复制到bin目录(作为Shim),保持原始文件名。
10-
11-
功能特点:
12-
1. 性能优化:自动忽略 node_modules, .git 等目录
13-
2. 灵活配置:支持 glob 模式过滤
14-
3. 相对路径:保持脚本内部 $PSScriptRoot 正确性
15-
4. 冲突检测:支持自动重命名解决冲突
8+
该脚本用于扫描项目中的 PowerShell 与 Python 脚本,并在 `bin` 目录生成可直接执行的 shim。
9+
10+
主要能力:
11+
1. 自动扫描脚本,并跳过 `.git`、`node_modules`、`bin` 等无关目录。
12+
2. 支持使用 `Patterns` 按相对路径筛选要生成 shim 的脚本。
13+
3. 支持多种重名处理策略,避免生成到 `bin` 时发生文件名冲突。
14+
4. 生成的 shim 保留相对路径语义,确保源脚本中的 `$PSScriptRoot` 或调用路径正常工作。
15+
5. 在 `sync` 时自动清理失效的旧 shim:如果源脚本已不存在,或同一源脚本因改名生成了新的目标文件名,会移除旧的自动生成 shim。
1616
1717
.PARAMETER Action
18-
要执行的操作:'sync'(同步脚本到bin目录)或'clean'(清理bin目录)
18+
要执行的操作:
19+
- `sync`:扫描项目脚本并同步生成 `bin` 下的 shim。
20+
- `clean`:清理 `bin` 目录下的 `.ps1` 文件。
1921
2022
.PARAMETER Force
21-
强制覆盖bin目录中已存在的文件
23+
强制覆盖 `bin` 目录中已存在的目标文件;未指定时,已存在文件会被跳过。
2224
2325
.PARAMETER Patterns
24-
可选的 Glob 模式列表,用于筛选要安装的脚本。
25-
例如: 'scripts/pwsh/*.ps1', 'ai/**/*.ps1'
26-
如果未指定,默认扫描除忽略目录外的所有 .ps1 文件。
26+
可选的 Glob 模式列表,用于筛选需要生成 shim 的脚本。
27+
例如:`scripts/pwsh/*.ps1`、`ai/**/*.ps1`
28+
29+
如果未指定,则使用脚本内置的默认模式。
2730
2831
.PARAMETER DuplicateStrategy
2932
重名处理策略:
30-
- PrefixParent (默认): 使用父目录名作为前缀 (e.g. parent_script.ps1)
31-
- Overwrite: 覆盖同名文件 (旧行为)
32-
- Skip: 跳过重名文件
33+
- `PrefixParent`(默认):使用父目录或更深层路径作为前缀生成唯一文件名,例如 `devops_Clean-DockerImages.ps1`
34+
- `Overwrite`:后扫描到的脚本覆盖同名目标文件
35+
- `Skip`:遇到重名时仅保留第一个脚本
3336
3437
.EXAMPLE
3538
.\Manage-BinScripts.ps1 -Action sync
36-
同步所有查找的到的 PowerShell 脚本到 bin 目录,自动处理重名
39+
使用默认模式同步生成 `bin` shim,并自动清理失效的旧 shim。
40+
41+
.EXAMPLE
42+
.\Manage-BinScripts.ps1 -Action sync -Patterns 'scripts/pwsh/devops/*.ps1' -Force
43+
仅同步 `scripts/pwsh/devops` 下的脚本,并强制覆盖已存在的目标文件。
3744
3845
.EXAMPLE
3946
.\Manage-BinScripts.ps1 -Action sync -DuplicateStrategy Overwrite
40-
同步脚本,遇到重名直接覆盖(最后扫描到的生效)
47+
同步脚本,并在目标文件重名时使用覆盖策略。
4148
#>
4249

4350
[CmdletBinding(SupportsShouldProcess = $true)]
@@ -194,6 +201,91 @@ function Resolve-ScriptTargetNames {
194201
return $mapping
195202
}
196203

204+
function Get-ManagedBinShimMetadata {
205+
param([string]$Path)
206+
207+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
208+
return $null
209+
}
210+
211+
$headerLines = Get-Content -LiteralPath $Path -TotalCount 8 -ErrorAction SilentlyContinue
212+
if (-not $headerLines) {
213+
return $null
214+
}
215+
216+
$isManagedShim = $headerLines | Where-Object { $_ -eq '# Auto-generated shim by Manage-BinScripts.ps1' } | Select-Object -First 1
217+
if (-not $isManagedShim) {
218+
return $null
219+
}
220+
221+
$sourceMatch = $headerLines | ForEach-Object {
222+
if ($_ -match '^# Source:\s*(.+)$') {
223+
$Matches[1].Trim()
224+
}
225+
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -First 1
226+
227+
if (-not $sourceMatch) {
228+
return $null
229+
}
230+
231+
$item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
232+
if (-not $item) {
233+
return $null
234+
}
235+
236+
return [PSCustomObject]@{
237+
Name = $item.Name
238+
FullName = $item.FullName
239+
SourcePath = $sourceMatch
240+
}
241+
}
242+
243+
function Remove-StaleManagedBinScripts {
244+
[CmdletBinding(SupportsShouldProcess = $true)]
245+
param([hashtable]$CurrentMapping)
246+
247+
if (-not (Test-Path $BinDir)) {
248+
return 0
249+
}
250+
251+
$removedCount = 0
252+
$binScripts = Get-ChildItem -Path $BinDir -Filter '*.ps1' -File -ErrorAction SilentlyContinue
253+
254+
foreach ($binScript in $binScripts) {
255+
$metadata = Get-ManagedBinShimMetadata -Path $binScript.FullName
256+
if ($null -eq $metadata) {
257+
continue
258+
}
259+
260+
$shouldRemove = $false
261+
$removeReason = $null
262+
263+
if ($CurrentMapping.ContainsKey($metadata.SourcePath)) {
264+
$expectedName = $CurrentMapping[$metadata.SourcePath]
265+
if ($metadata.Name -cne $expectedName) {
266+
$shouldRemove = $true
267+
$removeReason = "renamed to $expectedName"
268+
}
269+
}
270+
elseif (-not (Test-Path -LiteralPath $metadata.SourcePath -PathType Leaf)) {
271+
$shouldRemove = $true
272+
$removeReason = 'source script no longer exists'
273+
}
274+
275+
if (-not $shouldRemove) {
276+
continue
277+
}
278+
279+
if ($PSCmdlet.ShouldProcess($metadata.Name, "Remove stale shim ($removeReason)")) {
280+
Remove-Item -LiteralPath $metadata.FullName -Force
281+
Write-Host "清理旧 Shim: $($metadata.Name)" -ForegroundColor DarkYellow
282+
$removedCount++
283+
}
284+
}
285+
286+
return $removedCount
287+
}
288+
197289
function Sync-BinScripts {
198290
[CmdletBinding(SupportsShouldProcess = $true)]
199291
param([switch]$Force)
@@ -397,10 +489,13 @@ function Sync-BinScripts {
397489
}
398490
}
399491
}
492+
493+
$removedCount = Remove-StaleManagedBinScripts -CurrentMapping $scriptMapping
400494

401495
Write-Host "`n处理完成!" -ForegroundColor Green
402496
Write-Host " 新增/更新: $syncedCount" -ForegroundColor White
403497
Write-Host " 跳过(已存在): $skippedCount" -ForegroundColor White
498+
Write-Host " 清理旧文件: $removedCount" -ForegroundColor White
404499
}
405500

406501
function Clean-BinScripts {
@@ -435,3 +530,4 @@ switch ($Action) {
435530
Clean-BinScripts
436531
}
437532
}
533+

tests/Manage-BinScripts.Tests.ps1

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Set-StrictMode -Version Latest
2+
3+
Describe 'Manage-BinScripts' {
4+
BeforeEach {
5+
$script:ProjectRoot = Split-Path -Parent $PSScriptRoot
6+
$script:TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("Manage-BinScripts.Tests.{0}" -f [System.Guid]::NewGuid())
7+
$script:ManageScript = Join-Path $script:TempRoot 'Manage-BinScripts.ps1'
8+
9+
New-Item -ItemType Directory -Path $script:TempRoot -Force | Out-Null
10+
Copy-Item -Path (Join-Path $script:ProjectRoot 'Manage-BinScripts.ps1') -Destination $script:ManageScript -Force
11+
12+
New-Item -ItemType Directory -Path (Join-Path $script:TempRoot 'scripts/pwsh/a') -Force | Out-Null
13+
New-Item -ItemType Directory -Path (Join-Path $script:TempRoot 'scripts/pwsh/b') -Force | Out-Null
14+
15+
Set-Content -Path (Join-Path $script:TempRoot 'scripts/pwsh/a/A.ps1') -Value "Write-Output 'A'" -Encoding utf8NoBOM
16+
Set-Content -Path (Join-Path $script:TempRoot 'scripts/pwsh/b/B.ps1') -Value "Write-Output 'B'" -Encoding utf8NoBOM
17+
}
18+
19+
AfterEach {
20+
if ($script:TempRoot -and (Test-Path -LiteralPath $script:TempRoot)) {
21+
Remove-Item -LiteralPath $script:TempRoot -Recurse -Force
22+
}
23+
}
24+
25+
It 'removes stale renamed shims and preserves unrelated shims during partial sync' {
26+
& $script:ManageScript -Action sync -Force
27+
28+
$binDir = Join-Path $script:TempRoot 'bin'
29+
Test-Path -LiteralPath (Join-Path $binDir 'A.ps1') | Should -BeTrue
30+
Test-Path -LiteralPath (Join-Path $binDir 'B.ps1') | Should -BeTrue
31+
32+
Rename-Item -LiteralPath (Join-Path $script:TempRoot 'scripts/pwsh/a/A.ps1') -NewName 'A2.ps1'
33+
34+
& $script:ManageScript -Action sync -Force -Patterns @('scripts/pwsh/a/*.ps1')
35+
36+
Test-Path -LiteralPath (Join-Path $binDir 'A.ps1') | Should -BeFalse
37+
Test-Path -LiteralPath (Join-Path $binDir 'A2.ps1') | Should -BeTrue
38+
Test-Path -LiteralPath (Join-Path $binDir 'B.ps1') | Should -BeTrue
39+
}
40+
}

0 commit comments

Comments
 (0)