feat(tui): 清单编辑器循环(第一轮第五块·装配完成)

装配:选行 → 选新方向 → 逐行校验 → 校验后原子保存 + 时间戳备份。输入走 -Driver,所以门禁里能用 -InputScript @('Enter','Down','Enter') 走完整流程,断言看返回值与**文件内容**,不需要捕获控制台输出。

四条"不猜":取消时什么都不写也不产生备份;方向菜单**预选当前值**(不预选的话"连按两次回车"会把字段改成菜单第一项 —— 不是没改,是改错了);方向没变直接返回"没有变化";保存前逐行校验(每行都要能被 ConvertFrom-BackupListLine 解析)—— 这正是 TUI 编辑器比手改安全的地方。

这个函数我试了两次才落地,两次都栽在同一件事上:**用脚本做多行字符串替换时,换行被折成空格**,于是注释与赋值落进同一行、被整段注释掉(症状是"对象没有属性"/"流程莫名走取消分支")。所以这条现在当硬约束执行:多行代码一律整份写文件,不做插入+删除式的拼接。

判据 12 条,覆盖保存 / 取消 / 无变化三条路径,并逐条断言注释行与未编辑行一字不动、备份里存原文、取消时不新增备份。

验收:test.ps1 9/9 全绿(5.1 与 7)。
This commit is contained in:
Shuery committed 2026-09-27 16:40:38 +08:00
1 parent e1fe99671b
commit 5df40d3341
4 files changed
+147

No files matched your search

+1
View File
@@ -19,6 +19,7 @@
CompatiblePSEditions = @('Desktop', 'Core') CompatiblePSEditions = @('Desktop', 'Core')
FunctionsToExport = @( FunctionsToExport = @(
'Invoke-BakNRetBackupListEditor',
'Set-BakNRetBackupListDirection', 'Set-BakNRetBackupListDirection',
'Save-BakNRetConfigFile', 'Save-BakNRetConfigFile',
'Get-BakNRetBackupListRow', 'Get-BakNRetBackupListRow',
+3
View File
@@ -164,7 +164,10 @@
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListDirection.ps1') . (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListDirection.ps1')
. (Join-Path $PSScriptRoot 'Public\Invoke-BakNRetBackupListEditor.ps1')
Export-ModuleMember -Function @( Export-ModuleMember -Function @(
'Invoke-BakNRetBackupListEditor',
'Set-BakNRetBackupListDirection', 'Set-BakNRetBackupListDirection',
'Save-BakNRetConfigFile', 'Save-BakNRetConfigFile',
'Get-BakNRetBackupListRow', 'Get-BakNRetBackupListRow',
@@ -0,0 +1,105 @@
function Invoke-BakNRetBackupListEditor {
<#
.SYNOPSIS
清单的行编辑器:选一行 → 选新方向 → 逐行校验 → 校验后原子保存(并留时间戳备份)。
.DESCRIPTION
把前面几块装配起来:行表(Get-BakNRetBackupListRow)、单行替换(Set-BakNRetBackupListRow)、
改方向(Set-BakNRetBackupListDirection)、校验后原子保存(Save-BakNRetConfigFile)。
因为输入走 -Driver,整个流程在门禁里可以被 `-InputScript @('Enter','Down','Enter')` 驱动,
断言看的是**返回值**与**文件内容**,不需要捕获控制台输出 —— 与菜单循环同一个套路。
四条"不猜":
* 选行或选方向时按 Esc → 什么都不写,也不产生备份;
* **方向菜单预选当前值** —— 不预选的话,"连按两次回车"会把字段改成菜单的第一项,
那不是"没改",是改错了(实测踩到);
* 方向没变 → 直接返回"没有要保存的内容",不做无意义的写盘与备份;
* 保存前**逐行校验**(每行都要能被 ConvertFrom-BackupListLine 解析)——这正是 TUI 编辑器
比手改安全的地方:手改没有校验。
#>
param(
[Parameter(Mandatory = $true)][string]$Path,
[Parameter(Mandatory = $true)]$Driver,
[switch]$WhatIf
)
if (-not (Test-Path -LiteralPath $Path)) {
return [pscustomobject]@{ Saved = $false; Changed = $false; Error = "找不到清单文件:$Path"; BackupPath = $null }
}
$text = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
$rows = @(Get-BakNRetBackupListRow -Text $text)
$entryRows = @($rows | Where-Object { $_.Record })
if ($entryRows.Count -eq 0) {
return [pscustomobject]@{ Saved = $false; Changed = $false; Error = '清单里没有可编辑的条目。'; BackupPath = $null }
}
$items = @(foreach ($row in $entryRows) {
[pscustomobject]@{
Key = [string]$row.Line
Text = ('第 {0,3} 行 {1,-7} {2}' -f $row.Line, $row.Record.Direction, $row.Record.Path)
}
})
$picked = Invoke-BakNRetMenu -Title ('清单:{0}' -f $Path) -Items $items -Driver $Driver -MultiSelect $false -Hint '↑↓ 选行 回车 编辑 Esc 取消'
if ($picked.Action -ne 'confirm') {
return [pscustomobject]@{ Saved = $false; Changed = $false; Error = ''; BackupPath = $null }
}
$lineNumber = [int]$picked.Chosen.Key
$target = $rows | Where-Object { $_.Line -eq $lineNumber } | Select-Object -First 1
$directionItems = @(
[pscustomobject]@{ Key = 'both'; Text = 'both  两个方向都处理(不带标记)' },
[pscustomobject]@{ Key = 'backup'; Text = 'backup 只备份(行首 +)' },
[pscustomobject]@{ Key = 'restore'; Text = 'restore 只恢复(行首 -)' }
)
# 预选当前取值:菜单停在字段现在的值上,用户要改才按方向键。
$initialIndex = 0
for ($scan = 0; $scan -lt $directionItems.Count; $scan++) {
if ($directionItems[$scan].Key -eq [string]$target.Record.Direction) {
$initialIndex = $scan
break
}
}
$chosen = Invoke-BakNRetMenu -Title '改成哪个方向?' -Items $directionItems -Driver $Driver -MultiSelect $false -InitialIndex $initialIndex -Hint '↑↓ 选方向 回车 应用 Esc 取消'
if ($chosen.Action -ne 'confirm') {
return [pscustomobject]@{ Saved = $false; Changed = $false; Error = ''; BackupPath = $null }
}
$newLine = Set-BakNRetBackupListDirection -Raw $target.Raw -Direction $chosen.Chosen.Key
if ($newLine -eq $target.Raw) {
return [pscustomobject]@{ Saved = $false; Changed = $false; Error = '方向没有变化,没有要保存的内容。'; BackupPath = $null }
}
$newText = Set-BakNRetBackupListRow -Text $text -Line $lineNumber -NewText $newLine
$validate = {
param($candidate)
$bad = @()
$number = 0
foreach ($one in ($candidate -split "`r?`n")) {
$number++
$trimmed = $one.Trim()
if (-not $trimmed -or $trimmed.StartsWith('#')) { continue }
if (-not (ConvertFrom-BackupListLine -Line $one)) {
$bad += ('第 {0} 行解析不了:{1}' -f $number, $trimmed)
}
}
return $bad
}
$saved = Save-BakNRetConfigFile -Path $Path -Text $newText -Validate $validate `
-BackupDirectory (Join-Path (Split-Path -Parent $Path) 'logs\config-backups') -WhatIf:$WhatIf
return [pscustomobject]@{
Saved = $saved.Saved
Changed = $true
Error = $saved.Error
BackupPath = $saved.BackupPath
}
}
+38
View File
@@ -1647,6 +1647,44 @@ Test-Case '改方向:只动行首标记,其余逐字节保持,且保留用
Assert-True $threw '注释行改方向必须抛错' Assert-True $threw '注释行改方向必须抛错'
} }
Test-Case '清单编辑器:脚本驱动走完"选行→改方向→保存",只动一行且留下备份' {
$dir = Join-Path $env:TEMP ("bnr-listed-" + [guid]::NewGuid().ToString('N').Substring(0, 8))
New-Item -ItemType Directory -Path $dir -Force | Out-Null
$file = Join-Path $dir 'BackupList.txt'
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
$original = "# 注释一`nEdge`n+WindowsTerminal :encrypt`n"
[System.IO.File]::WriteAllText($file, $original, $utf8Bom)
$backupDir = Join-Path $dir 'logs\config-backups'
try {
# 1) 选第一行(回车)、方向菜单预选当前值 both、下移到 backup、回车应用
$r = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Enter', 'Down', 'Enter'))
Assert-Equal $true $r.Saved '应当保存'
$after = @((Get-Content -LiteralPath $file -Raw -Encoding UTF8) -split "`r?`n")
Assert-Equal '# 注释一' $after[0] '注释行一字不动'
Assert-Equal '+Edge' $after[1] '第一行改成了 +Edge'
Assert-Equal '+WindowsTerminal :encrypt' $after[2] '第三行一字不动'
Assert-True (Test-Path $r.BackupPath) '应当留下时间戳备份'
Assert-Equal $original ([System.IO.File]::ReadAllText($r.BackupPath)) '备份里是原文'
# 2) 取消(Esc):什么都不写,也不产生新备份
$backupsBefore = @(Get-ChildItem $backupDir -File | Measure-Object).Count
$contentBefore = [System.IO.File]::ReadAllText($file)
$c = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Esc'))
Assert-Equal $false $c.Saved '取消不该保存'
Assert-Equal $false $c.Changed '取消不该标记为已改'
Assert-Equal $contentBefore ([System.IO.File]::ReadAllText($file)) '取消后文件内容不变'
Assert-Equal $backupsBefore @(Get-ChildItem $backupDir -File | Measure-Object).Count '取消不该新增备份'
# 3) 方向没变:菜单预选当前值 backup,直接回车 → 说明"没有变化",不白写一次盘
$same = Invoke-BakNRetBackupListEditor -Path $file -Driver (New-BakNRetInputDriver -Script @('Enter', 'Enter'))
Assert-Equal $false $same.Saved '方向没变不该保存'
Assert-True ($same.Error -like '*没有变化*') '应当说明原因'
Assert-Equal $contentBefore ([System.IO.File]::ReadAllText($file)) '文件内容仍然不变'
} finally {
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ============================================================================ # ============================================================================
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue