diff --git a/BakNRet/BakNRet.psd1 b/BakNRet/BakNRet.psd1 index 51799a8..bd4ddb3 100644 --- a/BakNRet/BakNRet.psd1 +++ b/BakNRet/BakNRet.psd1 @@ -19,6 +19,8 @@ CompatiblePSEditions = @('Desktop', 'Core') FunctionsToExport = @( + 'Get-BakNRetBackupListRow', + 'Set-BakNRetBackupListRow', 'Invoke-BakNRetMenu', 'Update-BakNRetMenuState', 'Format-BakNRetPaddedText', diff --git a/BakNRet/BakNRet.psm1 b/BakNRet/BakNRet.psm1 index 86846aa..ca0944a 100644 --- a/BakNRet/BakNRet.psm1 +++ b/BakNRet/BakNRet.psm1 @@ -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', diff --git a/BakNRet/Public/Get-BakNRetBackupListRow.ps1 b/BakNRet/Public/Get-BakNRetBackupListRow.ps1 new file mode 100644 index 0000000..ca0eab7 --- /dev/null +++ b/BakNRet/Public/Get-BakNRetBackupListRow.ps1 @@ -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) +} diff --git a/BakNRet/Public/Set-BakNRetBackupListRow.ps1 b/BakNRet/Public/Set-BakNRetBackupListRow.ps1 new file mode 100644 index 0000000..b3cd1da --- /dev/null +++ b/BakNRet/Public/Set-BakNRetBackupListRow.ps1 @@ -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") +} diff --git a/tests/Run-Tests.ps1 b/tests/Run-Tests.ps1 index a52ef03..6cffc0e 100644 --- a/tests/Run-Tests.ps1 +++ b/tests/Run-Tests.ps1 @@ -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