Public/Get-HDTConsolePartitionRow.ps1
|
function Get-HDTConsolePartitionRow { <# .SYNOPSIS A DiskPartition step's table, as the editor's grid shows it. .DESCRIPTION MDT'S Format and Partition Disk GRID, answered without a window. The columns are the ones that dialog has - name, type, size, file system, and the variable the drive letter is published into - and each row carries the command its buttons would run. A STEP THAT NAMES A LAYOUT HAS NO ROWS, and that is an answer rather than an error: the grid shows nothing and says which layout is in force, because a window that drew an empty table over a step laid out by uefi-standard would be describing a disk nobody is building. THE SIZE IS SHOWN AS AUTHORED. '60%' and 'remainder' are not byte counts and must not be rendered as any - what they resolve to depends on the machine, and New-HDTDiskLayoutPlan is the only thing entitled to say. IT IS A QUERY. Nothing here writes; the commands the rows name are what write, and the window shows the invocation so an administrator can learn the automation surface by clicking. .PARAMETER Line The sequence document's lines. .PARAMETER Path Where those lines came from, so a parse failure can name the file. .PARAMETER Name The step to read. .INPUTS None. This command does not accept pipeline input. .OUTPUTS System.Management.Automation.PSCustomObject with Row, Style, Layout, HasTable and the command formats the grid's buttons use. .EXAMPLE (Get-HDTConsolePartitionRow -Line $line -Name 'Format and Partition').Row | Format-Table Name, Type, Size, FileSystem .EXAMPLE $view = Get-HDTConsolePartitionRow -Line $line -Name 'Format and Partition' $view.AddCommandFormat -f 'Data', 'Primary', 'remainder' #> [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $Line, [Parameter(Mandatory = $true, Position = 1)] [ValidateNotNullOrEmpty()] [string] $Path, [Parameter(Mandatory = $true, Position = 2)] [ValidateNotNullOrEmpty()] [string] $Name , # WHAT THE HOST ALREADY PARSED. The editor rebuilds its whole right # pane after every edit and four view models each turned the same lines # back into a document to do it - about 70ms apiece, on the UI thread, # while somebody waited for a checkbox to tick. # # THE HOST GUARANTEES THEY AGREE: it parses $book.Line once and hands # the result to all four in the same refresh. Omitted, this parses the # lines exactly as it always did, which is what a script or a test # wants. [Parameter()] [AllowNull()] [object] $Document ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # THE VALUES COME OUT OF THE PARSED DOCUMENT, not off the lines: a line # reader would have to learn YAML quoting to tell 60% from '60%'. # HANDED IN? THEN NOTHING IS RE-READ. See the -Document help above. if ($null -ne $Document) { $sequence = $Document } else { $reader = New-HDTFileSystemFromText -Path $Path -Text ($Line -join [System.Environment]::NewLine) $sequence = Import-HDTSequenceDocument -Path $Path -FileSystem $reader } $step = @($sequence.Step | Where-Object { $_.Name -eq $Name }) $authored = @() $layout = '' $style = '' $disk = '' $wipe = $false # WHETHER THE PANEL BELONGS ON SCREEN AT ALL, decided here rather than by the # window: a Disk tab over Apply OS is a tab with nothing to say that invites # a click to find out. A group is selected often, and is not a step, so the # question has to survive not finding one. $isDiskStep = (@($step).Count -gt 0 -and [string] $step[0].Type -eq 'DiskPartition') if (@($step).Count -gt 0) { $property = $step[0].Property if ($null -ne $property) { if ($property.Contains('partition')) { $authored = @($property['partition']) } if ($property.Contains('layout')) { $layout = [string] $property['layout'] } if ($property.Contains('style')) { $style = [string] $property['style'] } if ($property.Contains('diskNumber')) { $disk = [string] $property['diskNumber'] } if ($property.Contains('wipe')) { $wipe = [bool] $property['wipe'] } } } # THE UNITS THE ENGINE READS, AND WHAT EACH ONE COMPOSES. MDT's dialog has # Size and Size units beside it, and a checkbox for "a percentage of # remaining free space"; this is the same choice as one list, because a # percentage IS a unit here and a separate checkbox that silently ignores # the units dropdown is the part of that dialog worth not copying. # # THE FORMAT COMES WITH THE UNIT so the window composes nothing. The rest of # the disk takes no number at all, which is what NeedsAmount says - the # window disables the box rather than deciding what an empty one means. $unitOption = [pscustomobject[]] @( [pscustomobject] @{ Display = 'MB'; Format = '{0}MB'; NeedsAmount = $true } [pscustomobject] @{ Display = 'GB'; Format = '{0}GB'; NeedsAmount = $true } [pscustomobject] @{ Display = 'TB'; Format = '{0}TB'; NeedsAmount = $true } [pscustomobject] @{ Display = '% of what is left'; Format = '{0}%'; NeedsAmount = $true } [pscustomobject] @{ Display = 'bytes'; Format = '{0}'; NeedsAmount = $true } [pscustomobject] @{ Display = 'the rest of the disk'; Format = 'remainder'; NeedsAmount = $false } ) $row = New-Object -TypeName System.Collections.ArrayList $order = 0 $declaredBoot = $false foreach ($current in @($authored)) { if ($null -eq $current) { continue } $order++ $read = { param([string] $Key) if ($current -is [System.Collections.IDictionary]) { if ($current.Contains($Key)) { return [string] $current[$Key] } return '' } if ($null -eq $current.PSObject.Properties[$Key]) { return '' } return [string] $current.$Key } $partitionName = & $read 'name' # THE TWO CHECKBOXES, ANSWERED EVEN WHEN THE DOCUMENT IS SILENT. The # engine formats quick unless told otherwise and makes the FIRST row # bootable when no row claims it, so a dialog that showed both unticked # for a silent document would be describing a disk nobody is building. $quickFormat = $true $quickText = & $read 'quickFormat' if ($quickText -match '^(?i)(false|0|no)$') { $quickFormat = $false } # BOOTABLE IS DECIDED AFTER THE WHOLE TABLE IS READ. The engine's # positional default - the first row - applies only while NO row # declares one, and `bootable: false` is a declaration: it is how a # first partition refuses the default. Deciding it here, row by row, # would make row 1 look bootable in a table where row 3 claimed it. $bootable = $false $bootText = & $read 'bootable' if ($bootText -match '^(?i)(true|1|yes)$') { $bootable = $true $declaredBoot = $true } elseif ($bootText -match '^(?i)(false|0|no)$') { $declaredBoot = $true } # THE SIZE, TAKEN APART FOR THE TWO CONTROLS BELOW THE GRID. Selecting a # row fills a number and a unit, so Edit can rewrite the whole row # without anybody having to retype what was already there. # # THE GRID STILL SHOWS THE WHOLE STRING. This is for the boxes; the # column prints what the document says. $sizeText = & $read 'size' $amount = $sizeText $unit = 'MB' if ($sizeText -match '^\s*(remainder|\*)\s*$') { $amount = '' $unit = 'the rest of the disk' } elseif ($sizeText -match '^\s*([0-9]+)\s*%\s*$') { $amount = [string] $Matches[1] $unit = '% of what is left' } elseif ($sizeText -match '^\s*([0-9]+)\s*(KB|MB|GB|TB)\s*$') { $amount = [string] $Matches[1] $unit = [string] $Matches[2].ToUpperInvariant() } elseif ($sizeText -match '^\s*[0-9]+\s*$') { $unit = 'bytes' } [void] $row.Add([pscustomobject] @{ Order = $order Name = $partitionName Type = & $read 'type' # AS AUTHORED, NEVER AS BYTES. '60%' and 'remainder' resolve # against the disk in front of the machine, and only the planner # is entitled to say what they come to. Size = $sizeText Amount = $amount Unit = $unit QuickFormat = $quickFormat Bootable = $bootable # THE GRID SHOWS A MARK, NOT A WORD. Two boolean columns of # 'yes' and 'no' are wider than the names they sit beside and # harder to scan than a tick; the checkboxes on the dialog are # where they are set. QuickText = '' BootText = '' # The document's own rows, as against a named layout's. FromLayout = $false FileSystem = & $read 'filesystem' Variable = & $read 'variable' # PARENTHESISED, AND IT HAS TO BE: inside a hash literal the # comma of -f binds as the next entry, and the file stops # parsing. RemoveCommand = ("Remove-HDTStepPartition -Line `$line -Name '{0}' -Partition '{1}'" -f $Name, $partitionName) UpCommand = ("Move-HDTStepPartition -Line `$line -Name '{0}' -Partition '{1}' -Direction Up" -f $Name, $partitionName) DownCommand = ("Move-HDTStepPartition -Line `$line -Name '{0}' -Partition '{1}' -Direction Down" -f $Name, $partitionName) }) } # WHETHER THE DOCUMENT CARRIES ITS OWN TABLE, decided before a named # layout's volumes are added below. HasTable governs the buttons, and the # buttons edit the DOCUMENT - so it has to mean "there are rows in the file", # not "there are rows on screen". $hasTable = (@($row).Count -gt 0) # A NAMED LAYOUT HAS VOLUMES TOO, AND THE PAGE SHOWS THEM. The step carries # no table, but uefi-standard is three partitions and this page's whole job # is showing the disk that will be built - an empty grid over it describes # nothing and reads as a step that does nothing. # # THEY ARE MARKED FromLayout AND THE BUTTONS STAY DARK. Editing one would # have to write a table into the step, which silently converts it from "the # standard layout, whatever that becomes" into a frozen copy of today's. # That is a decision to make deliberately, not by clicking Edit. # # A LAYOUT THIS ENGINE DOES NOT HAVE IS NOT AN ERROR HERE. The step refuses # at run time and says so; a properties page that threw could not be opened # to fix the name. if (@($row).Count -eq 0 -and -not [string]::IsNullOrWhiteSpace($layout)) { $resolved = $null try { $resolved = Get-HDTDiskLayout -Name $layout } catch { $resolved = $null } # AND $null HAS NO Partition PROPERTY. The catch above sets $resolved to # $null for a layout this engine does not have, and the very next line # read a property off it - which Set-StrictMode makes a terminating # error, from inside a click handler, which unwinds ShowDialog and takes # the whole editor down with no message. # # THE NAME IS USUALLY A VARIABLE, NOT A TYPO. An MDT-shaped sequence # parameterises the layout - DEMO-M4 carries layout: "%HDTDiskLayout%" - # and the console edits the DOCUMENT, where that token has not been # expanded and never will be. So the ordinary case for this branch was # the one that crashed: every such sequence was unopenable. $layoutRow = @() if ($null -ne $resolved) { $layoutRow = @($resolved.Partition) } $order = 0 foreach ($current in @($layoutRow)) { if ($null -eq $current) { continue } $order++ [void] $row.Add([pscustomobject] @{ Order = $order Name = [string] $current.Role Type = (Get-HDTConsolePartitionTypeText -Partition $current) Size = (Get-HDTConsolePartitionSizeText -Partition $current) Amount = '' Unit = '' QuickFormat = $true Bootable = ($order -eq 1) QuickText = '' BootText = '' FileSystem = [string] $current.FileSystem # WHAT THE STEP WILL PUBLISH FOR THIS ROLE. A built-in # layout carries no variable key - Invoke-HDTDiskPartitionStep # writes HDTSystemVolume, HDTOSVolume and HDTRecoveryVolume # by role - so leaving the column empty hid the one answer # somebody comes to this grid for: which volume the image # lands on. Variable = $( switch ([string] $current.Role) { 'System' { 'HDTSystemVolume' } 'Windows' { 'HDTOSVolume' } 'Recovery' { 'HDTRecoveryVolume' } default { '' } }) FromLayout = $true RemoveCommand = ("Get-HDTDiskLayout -Name '{0}'" -f $layout) UpCommand = '' DownCommand = '' }) } } # THE POSITIONAL DEFAULT, APPLIED ONLY IF NOBODY CLAIMED IT. if (-not $declaredBoot -and @($row).Count -gt 0) { $row[0].Bootable = $true } foreach ($current in @($row)) { if ($current.QuickFormat) { $current.QuickText = [char] 0x2713 } if ($current.Bootable) { $current.BootText = [char] 0x2713 } } # WHAT THE STYLE WILL BE IF THE STEP DOES NOT PIN IT. The step resolves it # from the firmware at run time; saying "follows the firmware" is honest, # and printing GPT here would be this build host answering for a machine # that has not booted yet. $styleText = $style if ([string]::IsNullOrWhiteSpace($styleText)) { $styleText = 'follows the firmware' } # THE SENTENCE ABOVE THE GRID, COMPOSED HERE. The window assigns it and # joins nothing itself, which is the same rule the condition picker's format # string follows. $summary = 'Partition style: {0}.' -f $styleText if (-not [string]::IsNullOrWhiteSpace($layout)) { $summary = ("This step uses the named layout '{0}', so it writes no table of its own. Partition style: {1}." -f $layout, $styleText) } return [pscustomobject] @{ IsDiskStep = $isDiskStep Summary = $summary HasTable = $hasTable Row = [pscustomobject[]] @($row) Unit = $unitOption # THE TOP OF MDT'S PAGE. Disk number and disk type sit above the volume # list there, and they belong to the same decision - which disk, laid # out how - so putting them on the Properties tab split one dialog in # half. DiskNumber = $disk Wipe = $wipe # FOLLOWING THE FIRMWARE IS ON THE LIST, not the absence of a choice: it # is what most sequences should keep, and a dropdown offering only GPT # and MBR would make pinning one look compulsory. StyleOption = [string[]] @('follows the firmware', 'GPT', 'MBR') Style = $styleText Layout = $layout AddCommandFormat = "Add-HDTStepPartition -Line `$line -Name '$Name' -Partition '{0}' -Type {1} -Size '{2}'" StyleCommandFormat = "Set-HDTStepProperty -Line `$line -Name '$Name' -Property 'style' -Value '{0}'" Command = "Get-HDTConsolePartitionRow -Line `$line -Path `$path -Name '$Name'" } } |