Public/Set-HDTSequenceVariable.ps1

function Set-HDTSequenceVariable {
    <#
        .SYNOPSIS
            Sets or removes one variable in a task sequence's variables block,
            leaving every other line byte-identical.
 
        .DESCRIPTION
            THE BLOCK THE NEW SEQUENCE WINDOW FILLS AND NOTHING COULD CHANGE.
            New-HDTTaskSequence asks for the administrator password, the OS image
            and the organisation, writes them into variables:, and until this
            command existed that was the end of it - the editor edits steps, the
            detail pane edits name and description, and an administrator who
            mistyped the password re-created the sequence.
 
            The template's own comment above that block says "WHAT AN AUTHOR IS
            EXPECTED TO CHANGE", and it was the one part of a sequence a window
            could not touch.
 
            IT SPLICES ONE LINE. The comments inside the block, the order of the
            variables already there and the spacing around them all come back
            exactly as they went in - sequence.yaml is read in review, and an
            editor that reformats it makes every change unreadable.
 
            IT BUILDS THE BLOCK WHEN THERE IS NONE, before steps:. A variables:
            written after the steps belongs to the last step, or to nothing.
 
            THE NAME IS HELD TO THE SAME RULE AS rules.yaml. Every deployment
            variable is prefixed HDT and no _HDT* name may be assigned - a
            sequence that set 'ComputerName' would set something nothing reads,
            and the failure would be a machine with the wrong name rather than
            an error anybody saw.
 
            IT RETURNS LINES AND WRITES NOTHING. Save-HDTSequenceDocument is what
            touches the share, so an edit can be composed, reviewed and abandoned
            without a file ever changing.
 
        .PARAMETER Line
            The sequence document, as lines.
 
        .PARAMETER Name
            The variable. HDTSomething, and not _HDTSomething.
 
        .PARAMETER Value
            What to set it to. Ignored with -Remove.
 
        .PARAMETER Remove
            Take the variable out of the block instead of setting it. A variable
            set by mistake has to be removable, and removing one that is not
            there is refused rather than silently doing nothing.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.String[] - the document, spliced.
 
        .EXAMPLE
            $line = Get-Content -LiteralPath 'C:\HDTLab\Share\TaskSequences\DEMO\sequence.yaml'
            $line = Set-HDTSequenceVariable -Line $line -Name 'HDTAdminPassword' -Value 'P@ssw0rd!'
            Save-HDTSequenceDocument -Path 'C:\HDTLab\Share\TaskSequences\DEMO\sequence.yaml' -Line $line
 
        .EXAMPLE
            Set-HDTSequenceVariable -Line $line -Name 'HDTOSImageIndex' -Remove
 
        .LINK
            Save-HDTSequenceDocument
 
        .LINK
            Set-HDTTaskSequenceProperty
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Low', DefaultParameterSetName = 'Set')]
    [OutputType([string[]])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]] $Line,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter(Mandatory = $true, Position = 2, ParameterSetName = 'Set')]
        [AllowEmptyString()]
        [string] $Value,

        [Parameter(Mandatory = $true, ParameterSetName = 'Remove')]
        [switch] $Remove
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    # -- what is being asked for ---------------------------------------------

    if ($Name.StartsWith('_')) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name -Category InvalidArgument `
                    -Message ("'{0}' is engine-owned and cannot be assigned. A variable named _HDT* is set by the engine while the sequence runs and is read-only to a document." -f $Name)))
    }

    if ($Name -notmatch '^HDT[A-Za-z0-9_]*$') {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name -Category InvalidArgument `
                    -Message ("'{0}' is not an HDT variable name. Every deployment variable is prefixed HDT; run Get-HDTVariableMap for the MDT translation." -f $Name)))
    }

    # The document has to be readable before it is worth editing.
    $document = ConvertFrom-HDTSequenceLine -Line $Line

    $declared = ($null -ne $document.Variable) -and $document.Variable.Contains($Name)

    if ($Remove -and -not $declared) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name -Category ObjectNotFound `
                    -Message ("'{0}' is not declared by this sequence, so there is nothing to remove. The variables it does declare are: {1}." -f
                        $Name, ((@($document.Variable.Keys) -join ', '), '(none)')[(@($document.Variable.Keys).Count -eq 0)])))
    }

    if ($Remove) {
        $action = "Remove the variable '$Name'"
    } else {
        $action = "Set the variable '$Name'"
    }

    if (-not $PSCmdlet.ShouldProcess('sequence.yaml', $action)) {
        return [string[]] @($Line)
    }

    # -- the splice ----------------------------------------------------------
    #
    # THE BLOCK IS FOUND BY ITS OWN LINE, not by parsing: a rewrite from the
    # parsed document would return a tidy file with none of the author's
    # comments in it.

    $result = New-Object -TypeName System.Collections.ArrayList
    foreach ($current in @($Line)) { [void] $result.Add([string] $current) }

    $blockIndex = -1
    $stepsIndex = -1

    for ($index = 0; $index -lt $result.Count; $index++) {
        $text = [string] $result[$index]

        if ($blockIndex -lt 0 -and $text -match '^variables\s*:\s*$') { $blockIndex = $index }
        if ($stepsIndex -lt 0 -and $text -match '^steps\s*:\s*$') { $stepsIndex = $index }
    }

    # WHERE THE BLOCK ENDS: the first line at column zero after it, or the end
    # of the document. Everything between belongs to variables:, comments and
    # blank lines included.
    $blockEnd = $result.Count
    if ($blockIndex -ge 0) {
        for ($index = $blockIndex + 1; $index -lt $result.Count; $index++) {
            $text = [string] $result[$index]
            if ($text -match '^\S') { $blockEnd = $index; break }
        }
    }

    $written = ' {0}: {1}' -f $Name, (ConvertTo-HDTRuleScalarText -Value $Value)

    if ($blockIndex -ge 0) {
        $existing = -1

        for ($index = $blockIndex + 1; $index -lt $blockEnd; $index++) {
            if (([string] $result[$index]) -match ('^\s+{0}\s*:' -f [regex]::Escape($Name))) {
                $existing = $index
                break
            }
        }

        if ($existing -ge 0 -and $Remove) {
            $result.RemoveAt($existing)
        } elseif ($existing -ge 0) {
            $result[$existing] = $written
        } else {
            # AFTER THE LAST VARIABLE, not at the top of the block: the comment
            # the template writes sits at the top, and a value inserted above it
            # reads as if the comment describes something else.
            $insertAt = $blockEnd
            while ($insertAt -gt $blockIndex + 1 -and [string]::IsNullOrWhiteSpace([string] $result[$insertAt - 1])) {
                $insertAt--
            }

            $result.Insert($insertAt, $written)
        }
    } else {
        # NO BLOCK YET. It goes before steps:, or at the end of a document that
        # has none, with a blank line after it so the steps still read as their
        # own section.
        $at = $stepsIndex
        if ($at -lt 0) { $at = $result.Count }

        $result.Insert($at, '')
        $result.Insert($at + 1, 'variables:')
        $result.Insert($at + 2, $written)
    }

    $spliced = [string[]] @($result)

    try {
        [void] (ConvertFrom-HDTSequenceLine -Line $spliced)
    } catch {
        $PSCmdlet.ThrowTerminatingError($_)
    }

    return [string[]] $spliced
}