feat(tui): 清单行表与单行替换(第一轮第五块·核心)
外科式改写的地基(ADR-0013):Get-BakNRetBackupListRow 把清单文本拆成带**行号**的行表,Get-BakNRetBackupListRow 之外的行一字节不动。行表刻意保留注释与空行 —— 只在表里放"真正的条目"会让行号错位,改一行就会连锁改掉别处。 解析器给的 Record.Raw 就是原始行文本,所以"没被编辑的行写回原样"是天然的:注释、对齐、$( ) 表达式、跨行拼接都不会被碰到。这也是它比"解析成对象再序列化"安全的地方。 行号越界**抛错**而不是静默不动:静默不动会制造"保存成功但其实没改",而用户看到的是一份以为改过、其实没改的配置。 判据是 spec 里那条往返断言的文本层形式:每行原样写回后**文本逐字节相同**;改一行时只有那一行不同、注释行数不变、原文本不被就地改动。 验收:断言 13 条;test.ps1 9/9 全绿(5.1 与 7)。
This commit is contained in:
1 parent
835fb0cf2b
commit
54972ab341
5 files changed
+110
No files matched your search
@@ -19,6 +19,8 @@
|
||||
CompatiblePSEditions = @('Desktop', 'Core')
|
||||
|
||||
FunctionsToExport = @(
|
||||
'Get-BakNRetBackupListRow',
|
||||
'Set-BakNRetBackupListRow',
|
||||
'Invoke-BakNRetMenu',
|
||||
'Update-BakNRetMenuState',
|
||||
'Format-BakNRetPaddedText',
|
||||
|
||||
@@ -157,7 +157,12 @@
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Public\Invoke-BakNRetMenu.ps1')
|
||||
|
||||
. (Join-Path $PSScriptRoot 'Public\Get-BakNRetBackupListRow.ps1')
|
||||
. (Join-Path $PSScriptRoot 'Public\Set-BakNRetBackupListRow.ps1')
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Get-BakNRetBackupListRow',
|
||||
'Set-BakNRetBackupListRow',
|
||||
'Invoke-BakNRetMenu',
|
||||
'Update-BakNRetMenuState',
|
||||
'Format-BakNRetPaddedText',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
function Get-BakNRetBackupListRow {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
把清单文本拆成带**行号**的行表;能解析的行附上解析结果,注释与空行的 Record 是 $null。
|
||||
|
||||
.DESCRIPTION
|
||||
为什么要保留注释行与空行:外科式改写(ADR-0013)要按行号改**一行**,而行号必须与磁盘上的
|
||||
文件一一对应。如果只在表里放"真正的条目",行号就错位了,改一行会连锁改掉别处。
|
||||
|
||||
Raw 是原始行文本(原文一个字节都不动),解析器给的 Record.Raw 也是它 —— 编辑时只要没动这一行,
|
||||
就写回原样,注释、对齐、`$( )` 表达式、跨行拼接都会原封不动地留下来。
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text)
|
||||
|
||||
$lines = $Text -split "`r?`n"
|
||||
$rows = @()
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$raw = $lines[$i]
|
||||
$record = $null
|
||||
# 注释行与空行不解析(解析器对它们返回 $null,这里显式跳过以省掉一次无谓的调用)
|
||||
$trimmed = $raw.Trim()
|
||||
if ($trimmed -and -not $trimmed.StartsWith('#')) {
|
||||
$record = ConvertFrom-BackupListLine -Line $raw
|
||||
}
|
||||
$rows += [pscustomobject]@{
|
||||
Line = $i + 1
|
||||
Raw = $raw
|
||||
Record = $record
|
||||
}
|
||||
}
|
||||
return @($rows)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
function Set-BakNRetBackupListRow {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
把清单文本里**第 N 行**换成新文本,其余每一行逐字节保持;返回新文本(不改磁盘)。
|
||||
|
||||
.DESCRIPTION
|
||||
外科式改写的核心(ADR-0013):只碰被编辑的那一行。注释、对齐、`$( )` 表达式、跨行拼接同属
|
||||
别的行,所以它们不会被碰到 —— 这也是它比"解析成对象再序列化"安全的地方(后者会把注释与
|
||||
表达式一起毁掉)。
|
||||
|
||||
行号越界时**抛错**而不是静默不动:静默不动会让"保存成功但其实没改"这种最坏的情况发生,
|
||||
而用户看到的是一份以为改过、其实没改的配置。
|
||||
|
||||
编码与原子替换不在这里:本函数只处理字符串。写盘由调用方走 Write-BaknretAtomicText
|
||||
(并且必须先校验,见 ADR-0013)。
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text,
|
||||
[Parameter(Mandatory = $true)][int]$Line,
|
||||
[Parameter(Mandatory = $true)][AllowEmptyString()][string]$NewText
|
||||
)
|
||||
|
||||
$lines = @($Text -split "`r?`n")
|
||||
if ($Line -lt 1 -or $Line -gt $lines.Count) {
|
||||
throw ("行号越界:文件共 {0} 行,请求改第 {1} 行。" -f $lines.Count, $Line)
|
||||
}
|
||||
|
||||
$lines[$Line - 1] = $NewText
|
||||
return ($lines -join "`n")
|
||||
}
|
||||
@@ -1556,6 +1556,47 @@ Test-Case '菜单循环:按数量分档验(0 项 / 1 项 / 3 项),单键
|
||||
Assert-Equal 'empty' $empty.Reason '原因标为 empty'
|
||||
}
|
||||
|
||||
Test-Case '清单行表与单行替换:只动那一行,注释与其它行逐字节不变' {
|
||||
$text = @(
|
||||
'# 注释一',
|
||||
'Edge',
|
||||
'',
|
||||
'+WindowsTerminal :encrypt',
|
||||
'# 注释二',
|
||||
'-OldApp :- logs\'
|
||||
) -join "`n"
|
||||
|
||||
# 行表必须包含注释与空行,否则行号会错位(外科式改写按行号动手)
|
||||
$rows = Get-BakNRetBackupListRow -Text $text
|
||||
Assert-Equal 6 $rows.Count '总行数'
|
||||
Assert-Equal 1 $rows[0].Line '首行行号是 1'
|
||||
Assert-True ($null -eq $rows[0].Record) '注释行不解析'
|
||||
Assert-True ($null -eq $rows[2].Record) '空行不解析'
|
||||
Assert-Equal 'Edge' $rows[1].Record.Path '第 2 行是条目'
|
||||
Assert-Equal 'backup' $rows[3].Record.Direction '第 4 行方向是 backup'
|
||||
Assert-Equal 'Edge' $rows[1].Raw 'Raw 是原始行文本'
|
||||
|
||||
# 往返:每一行原样写回 → 文本必须逐字节相同(这是"外科式"的最强形式)
|
||||
$same = $text
|
||||
foreach ($row in $rows) { $same = Set-BakNRetBackupListRow -Text $same -Line $row.Line -NewText $row.Raw }
|
||||
Assert-Equal $text $same '每行原样写回后文本逐字节相同'
|
||||
|
||||
# 改一行:只有那一行不同,注释行数不变,原文本不被就地改动
|
||||
$edited = Set-BakNRetBackupListRow -Text $text -Line 2 -NewText '-Edge'
|
||||
$before = @($text -split "`n")
|
||||
$after = @($edited -split "`n")
|
||||
Assert-Equal $before.Count $after.Count '行数不变'
|
||||
Assert-Equal '-Edge' $after[1] '第 2 行改成了 -Edge'
|
||||
Assert-Equal 2 @(Compare-Object -ReferenceObject $before -DifferenceObject $after).Count '只应当有一行不同'
|
||||
Assert-Equal 2 @($after | Where-Object { $_.Trim().StartsWith('#') }).Count '注释行数不变'
|
||||
Assert-Equal 'Edge' $before[1] '原文本没有被就地改动'
|
||||
|
||||
# 行号越界必须抛错:静默不动会让"保存成功但其实没改"发生
|
||||
$threw = $false
|
||||
try { Set-BakNRetBackupListRow -Text $text -Line 99 -NewText 'x' | Out-Null } catch { $threw = $true }
|
||||
Assert-True $threw '行号越界必须抛错'
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
Remove-Item -LiteralPath $sandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $itemSandbox -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Reference in new issue
Block a user