Private/Set-HDTWorkspaceKey.ps1
|
function Set-HDTWorkspaceKey { <# .SYNOPSIS Replaces, inserts or removes one key of a workspace document, building the blocks above it when they are not there, and leaving every other line byte-identical. .DESCRIPTION THE SPLICE EVERY WORKSPACE EDIT COMES DOWN TO: rewrite the key's lines if it is there, insert them if it is not, drop them if the caller passed nothing. Written once, because logLevel, drivers, extraContent and startCommand differ only in what goes in them. IT BUILDS THE BLOCKS THAT ARE MISSING, and that is what makes it different from the rules editor's equivalent. rules.yaml always has a rules: list to splice into; workspace.yaml usually has NO bootImage block at all, because New-HDTWorkspace deliberately writes none - an omitted setting takes the engine's default, and a copied-out default goes stale the day the engine's changes. So the first boot image setting an administrator writes has to create bootImage:, its nested key, and the value, in one insert. A NEW KEY GOES WHERE THE DOCUMENT'S OWN ORDER PUTS IT - identity, then deployRoot, then logLevel, then the blocks; and inside bootImage, the artifact settings, then the lists, then the entry and start commands. That is the order every worked example and every document this toolkit writes uses, and it is the order an administrator scanning a diff expects. The key is inserted after the last key that precedes it in that order and is actually present. A NEW BLOCK AT THE TOP LEVEL GETS A BLANK LINE ABOVE IT, so it is spaced the way the rest of the file is rather than welded to the key above. THE TEXT ARRIVES AT COLUMN ZERO AND IS SHIFTED TO WHERE IT LANDS, which is the same reindenting a pasted task sequence step goes through and for the same reason: the block's internal shape survives, and the key ends up in the one column that makes it a key. REMOVING A KEY THAT WAS THE LAST THING IN ITS BLOCK REMOVES THE BLOCK. A bootImage: with nothing under it parses as a null, and the engine refuses a workspace whose bootImage is not a mapping - so leaving the husk behind would produce a document that cannot be loaded, from a command whose whole job was to take one setting away. .PARAMETER Line The document, already split into lines. .PARAMETER Path The key path, outermost first. .PARAMETER Text The lines the key becomes, written at column zero. Empty removes the key. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.String[] .EXAMPLE Set-HDTWorkspaceKey -Line $line -Path @('logLevel') -Text @('logLevel: Debug') .EXAMPLE Set-HDTWorkspaceKey -Line $line -Path @('bootImage', 'drivers') -Text @() Removes the boot driver group, and the bootImage block with it when that was all it held. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Returns a copy of in-memory lines. Save-HDTWorkspaceDocument is the only command that writes, and it carries ShouldProcess.')] [CmdletBinding()] [OutputType([string[]])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $Line, [Parameter(Mandatory = $true, Position = 1)] [ValidateNotNullOrEmpty()] [string[]] $Path, [Parameter(Mandatory = $true, Position = 2)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $Text ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # The order a workspace document is written in, per container. The root list # matches Assert-HDTWorkspaceDocument's allowed keys, and the bootImage list # matches its own - a key added there and forgotten here is inserted at the # top of its block rather than misplaced, which is untidy and never wrong. $order = @{ '' = @('schemaVersion', 'id', 'name', 'deployRoot', 'logLevel', 'credential', 'bootImage') 'credential' = @('username') 'bootImage' = @('name', 'architecture', 'language', 'scratchSpaceMB', 'optionalComponents', 'extraContent', 'drivers', 'unattend', 'background', 'timeZone', 'rootCertificates', 'clientCertificate', 'entryCommand', 'startCommand', 'skip') } $existing = Get-HDTWorkspaceKey -Line $Line -Path $Path # ===================================================================== # THE KEY IS THERE: replace its lines, or take them out. # ===================================================================== if ($null -ne $existing) { $written = [string[]] @(Set-HDTWorkspaceIndent -Block $Text -Indent ([int] $existing.Indent)) $result = New-Object -TypeName System.Collections.ArrayList for ($i = 0; $i -lt $Line.Count; $i++) { if ($i -ge [int] $existing.Index -and $i -le [int] $existing.End) { if ($i -eq [int] $existing.Index) { foreach ($current in $written) { [void] $result.Add($current) } } continue } [void] $result.Add($Line[$i]) } $final = [string[]] @($result) if (@($Text).Count -eq 0) { $final = [string[]] @(Remove-HDTBlankRun -Line $final -At ([int] $existing.Index)) # THE HUSK GOES WITH IT. A bootImage: left holding nothing is a # document the engine refuses to load. for ($depth = $Path.Count - 1; $depth -ge 1; $depth--) { $parent = Get-HDTWorkspaceKey -Line $final -Path ([string[]] @($Path[0..($depth - 1)])) if ($null -eq $parent) { continue } if ([int] $parent.ChildIndent -ge 0) { break } $keep = New-Object -TypeName System.Collections.ArrayList for ($i = 0; $i -lt $final.Count; $i++) { if ($i -ge [int] $parent.Index -and $i -le [int] $parent.End) { continue } [void] $keep.Add($final[$i]) } $final = [string[]] @(Remove-HDTBlankRun -Line ([string[]] @($keep)) -At ([int] $parent.Index)) } } return [string[]] $final } if (@($Text).Count -eq 0) { # Nothing to remove and nothing to write. return [string[]] @($Line) } # ===================================================================== # THE KEY IS NOT THERE: build it, and whatever has to hold it. # ===================================================================== $parentBlock = $null $have = 0 for ($n = $Path.Count - 1; $n -ge 1; $n--) { $candidate = Get-HDTWorkspaceKey -Line $Line -Path ([string[]] @($Path[0..($n - 1)])) if ($null -ne $candidate) { $parentBlock = $candidate $have = $n break } } # Wrapped from the inside out, so the innermost key keeps the shape the # caller composed and every container above it is two columns shallower. $block = [string[]] @($Text) for ($k = $Path.Count - 2; $k -ge $have; $k--) { $wrapped = New-Object -TypeName System.Collections.ArrayList [void] $wrapped.Add(('{0}:' -f $Path[$k])) foreach ($current in $block) { if ([string]::IsNullOrWhiteSpace($current)) { [void] $wrapped.Add($current) continue } [void] $wrapped.Add(' ' + $current) } $block = [string[]] @($wrapped) } # -- where it lands -------------------------------------------------- $targetIndent = 0 $insertAfter = -1 $containerName = '' if ($null -ne $parentBlock) { $containerName = @($Path[0..($have - 1)]) -join '.' $targetIndent = [int] $parentBlock.ChildIndent if ($targetIndent -lt 0) { $targetIndent = [int] $parentBlock.Indent + 2 } $insertAfter = [int] $parentBlock.Index } $sibling = @(Get-HDTWorkspaceChildKey -Line $Line -Block $parentBlock) if ($order.ContainsKey($containerName)) { $rank = [array]::IndexOf($order[$containerName], [string] $Path[$have]) foreach ($current in $sibling) { $at = [array]::IndexOf($order[$containerName], [string] $current.Name) # An unknown key keeps whatever place it has in the file rather than # being jumped over; the document validator is what refuses it. if ($at -lt 0) { $at = @($order[$containerName]).Count } if ($at -lt $rank -and [int] $current.End -gt $insertAfter) { $insertAfter = [int] $current.End } } } # A top-level key with nothing ranking before it still goes after the # document's own keys rather than above its header comment. if ($null -eq $parentBlock -and $insertAfter -lt 0 -and @($sibling).Count -gt 0) { $insertAfter = [int] $sibling[@($sibling).Count - 1].End } $written = [string[]] @(Set-HDTWorkspaceIndent -Block $block -Indent $targetIndent) # A new block at the top level is a section of its own, and reads as one. $spaced = ($null -eq $parentBlock -and $insertAfter -ge 0 -and -not [string]::IsNullOrWhiteSpace($Line[$insertAfter])) $result = New-Object -TypeName System.Collections.ArrayList if ($insertAfter -lt 0) { foreach ($current in $written) { [void] $result.Add($current) } } for ($i = 0; $i -lt $Line.Count; $i++) { [void] $result.Add($Line[$i]) if ($i -ne $insertAfter) { continue } if ($spaced) { [void] $result.Add('') } foreach ($current in $written) { [void] $result.Add($current) } } return [string[]] @($result) } |