Private/Get-HDTStepBlock.ps1
|
function Get-HDTStepBlock { <# .SYNOPSIS Finds the exact lines each step and group occupies in a sequence document. .DESCRIPTION THE FOUNDATION OF EVERY EDIT THE CONSOLE MAKES. Add, Remove, Up, Down and Paste are all splices of whole line ranges, and Save is then writing the string back unchanged everywhere else. Nothing round-trips through the YAML parser, because ConvertFrom-HDTYaml yields a dictionary and a dictionary has no comments in it - DEMO-M4 on the lab share is 107 lines of which about 60 are a comment header recording findings from the lab, and a save that re-serialised a model would delete every one: "a UI that reformats the file breaks git review, which is one of the reasons config-as-code fails in practice." A COMMENT ABOVE A STEP BELONGS TO THAT STEP, so Start reaches back over it while Entry stays on the dash line. Every comment in DEMO-M4 explains the step beneath it - why minRamMB is 2048, why wipe: true is the sequence declaring the disk expendable. Moving a step and leaving its explanation behind, now attached to whatever took its place, would be worse than not moving it. A TRAILING BLANK LINE IS NOT PART OF A BLOCK. Taking it would collapse the file's spacing a little more with every edit until the document became one dense wall. THE DOCUMENT HEADER BELONGS TO NO BLOCK. It explains the file rather than any step in it, so nothing here can move or delete it. .PARAMETER Line The document, already split into lines. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject[] in document order: Kind 'Step' or 'Group' Name the step or group name Indent the column the dash sits at, which an insert must match Entry the index of the '- ' line Start Entry, or the first line of the comment above it End the last line the block owns, blanks excluded .EXAMPLE Get-HDTStepBlock -Line ($text -split "`r?`n") #> [CmdletBinding()] [OutputType([pscustomobject[]])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyCollection()] [AllowEmptyString()] # a blank line IS an empty string, and this is a whole document [string[]] $Line ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $found = New-Object -TypeName System.Collections.ArrayList # -- the steps: region ------------------------------------------------- $stepsAt = -1 for ($i = 0; $i -lt $Line.Count; $i++) { if ($Line[$i] -match '^steps:\s*$') { $stepsAt = $i break } } if ($stepsAt -lt 0) { return [pscustomobject[]] @() } # It runs to the next top-level key, or to the end of the document. $regionEnd = $Line.Count - 1 for ($i = $stepsAt + 1; $i -lt $Line.Count; $i++) { $text = $Line[$i] if ([string]::IsNullOrWhiteSpace($text)) { continue } if ($text -match '^\s*#') { continue } if ($text -match '^\S' ) { $regionEnd = $i - 1 break } } # -- every entry in it ------------------------------------------------- $entry = New-Object -TypeName System.Collections.ArrayList for ($i = $stepsAt + 1; $i -le $regionEnd; $i++) { if ($Line[$i] -match '^(\s*)-\s+\S') { [void] $entry.Add([pscustomobject] @{ Index = $i Indent = $Matches[1].Length }) } } # -- each entry's span, kind and name ---------------------------------- for ($e = 0; $e -lt $entry.Count; $e++) { $current = $entry[$e] # The block ends where the next entry at the same or shallower # indentation begins. A group therefore covers its own steps, which sit # deeper than it does. $end = $regionEnd for ($n = $e + 1; $n -lt $entry.Count; $n++) { if ($entry[$n].Indent -le $current.Indent) { $end = $entry[$n].Index - 1 break } } # Blank lines and the next block's comments are not this block's. while ($end -gt $current.Index -and ([string]::IsNullOrWhiteSpace($Line[$end]) -or $Line[$end] -match '^\s*#')) { $end-- } # A comment directly above the dash line explains this block. $start = $current.Index while ($start -gt ($stepsAt + 1) -and $Line[$start - 1] -match '^\s*#') { $start-- } # -- what it is, and what it is called $kind = 'Step' $name = '' for ($s = $current.Index; $s -le $end; $s++) { if ($Line[$s] -match '^\s*(?:-\s+)?(name|group):\s*(.+?)\s*$') { $kind = 'Step' if ($Matches[1] -eq 'group') { $kind = 'Group' } $name = $Matches[2].Trim('"', "'") break } } [void] $found.Add([pscustomobject] @{ Kind = $kind Name = $name Indent = $current.Indent Entry = $current.Index Start = $start End = $end }) } return [pscustomobject[]] @($found) } |