guided-setup.psm1
Set-PSDebug -Strict ############################################################################### # data-structures.ps1 (begin) ############################################################################### class Comparator { hidden [scriptblock]$compare hidden [scriptblock]$compareOriginal Comparator() { $this.compare = { param($a, $b) if ($a -eq $b) { return 0 } if ($a -lt $b) { return -1 } return 1 } } Comparator([scriptblock]$compareFunction) { $this.compare = $compareFunction } [int] equal($a, $b) { return (& $this.compare $a $b) -eq 0 } [int] lessThan($a, $b) { return (& $this.compare $a $b) -lt 0 } [int] greaterThan($a, $b) { return (& $this.compare $a $b) -gt 0 } [int] lessThanOrEqual($a, $b) { return $this.lessThan($a, $b) -Or $this.equal($a, $b) } [int] greaterThanOrEqual($a, $b) { return $this.greaterThan($a, $b) -Or $this.equal($a, $b) } reverse() { $this.compareOriginal = $this.compare $this.compare = { param($a, $b) &$this.compareOriginal $b $a } } } # https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/linked-list class LinkedListNode { $value $next LinkedListNode($value) { $this.value = $value } LinkedListNode($value, $next) { $this.value = $value $this.next = $next } [string]ToString() { return $this.value } } class LinkedList { $head $tail $compare # default ctor LinkedList() { $this.compare = New-Object Comparator } LinkedList($comparatorFunction) { $this.compare = New-Object Comparator $comparatorFunction } [object] Append($value) { $newNode = New-Object LinkedListNode $value # If there is no head yet let's make new node a head. if (!$this.head) { $this.head = $newNode $this.tail = $newNode return $this } # Attach new node to the end of linked list. $this.tail.next = $newNode $this.tail = $newNode return $this } [object] Prepend($value) { # Make new node to be a head. $this.head = New-Object LinkedListNode $value, $this.head return $this } hidden [object] Find($value, [scriptblock]$callback) { if (!$this.head) { return $null } $currentNode = $this.head while ($currentNode) { if ($callback -and (&$callback $currentNode.value)) { return $currentNode } if ($null -ne $value -and $this.compare.equal($currentNode.value, $value)) { return $currentNode } $currentNode = $currentNode.next } return $null } [object] deleteHead() { if (!$this.head) { return $null } $deletedHead = $this.head if ($this.head.next) { $this.head = $this.head.next } else { $this.head = $null $this.tail = $null } return $deletedHead; } [object] deleteTail() { if ($this.head -eq $this.tail) { $deletedTail = $this.tail $this.head = $null $this.tail = $null return $deletedTail } $deletedTail = $this.tail # Rewind to the last node and delete "next" link for the node before the last one. $currentNode = $this.head while ($currentNode.next) { if (!$currentNode.next.next) { $currentNode.next = $null } else { $currentNode = $currentNode.next } } $this.tail = $currentNode return $deletedTail } [object] Delete($value) { if (!$this.head) { return $null } $deletedNode = $null # If the head must be deleted then make 2nd node to be a head. while ($this.head -and ($this.head.value -eq $value)) { $deletedNode = $this.head $this.head = $this.head.next } $currentNode = $this.head if ($null -ne $currentNode) { # If next node must be deleted then make next node to be a next next one. while ($currentNode.next) { if ($currentNode.next.value -eq $value) { $deletedNode = $currentNode.next $currentNode.next = $currentNode.next.next } else { $currentNode = $currentNode.next } } } # Check if tail must be deleted. if ($this.tail.value -eq $value) { $this.tail = $currentNode } return $deletedNode } [object] ToArray() { $nodes = @() $currentNode = $this.head while ($currentNode) { $nodes += ($currentNode) $currentNode = $currentNode.next } return $nodes } [string] ToString() { return $this.ToString($null) } [string] ToString($callback) { return ($this.ToArray() -join ",") } } class GraphEdge { $startVertex $endVertex $weight GraphEdge($startVertex, $endVertex) { $this.DoInit($startVertex, $endVertex, 0) } GraphEdge($startVertex, $endVertex, $weight) { $this.DoInit($startVertex, $endVertex, $weight) } hidden DoInit($startVertex, $endVertex, $weight) { $this.startVertex = $startVertex $this.endVertex = $endVertex $this.weight = $weight } [object] getKey() { $startVertexKey = $this.startVertex.getKey() $endVertexKey = $this.endVertex.getKey() return "$($startVertexKey)_$($endVertexKey)" # return `${startVertexKey}_${endVertexKey}`; } [object] reverse() { $tmp = $this.startVertex $this.startVertex = $this.endVertex $this.endVertex = $tmp return $this } [string] toString() { return $this.getKey() } } class GraphVertex { $value $edges GraphVertex() { $this.DoInit($null) } GraphVertex($value) { $this.DoInit($value) } hidden DoInit($value) { if ($null -eq $value) { throw 'Graph vertex must have a value' } $edgeComparator = { param($edgeA, $edgeB) if ($edgeA.getKey() -eq $edgeB.getKey()) { return 0 } if ($edgeA.getKey() -lt $edgeB.getKey()) { return -1 } return 1 } # Normally you would store string value like vertex name. # But generally it may be any object as well $this.value = $value $this.edges = New-Object LinkedList $edgeComparator } [object] addEdge($edge) { $this.edges.append($edge) return $this } deleteEdge($edge) { $this.edges.delete($edge) } [object] getNeighbors() { $targetEdges = $this.edges.toArray() $neighborsConverter = { param($node) if ($node.value.startVertex -eq $this) { return $node.value.endVertex } return $node.value.startVertex } # Return either start or end vertex. # For undirected graphs it is possible that current vertex will be the end one. return @($targetEdges.ForEach{&$neighborsConverter $_}) } [object] getEdges() { return $this.edges.toArray().value } [object] getDegree() { return $this.edges.toArray().Count } [bool] hasEdge($requiredEdge) { $edgeNode = $this.edges.find($null, { param($edge) $edge -eq $requiredEdge }) return !!$edgeNode } [object] hasNeighbor($vertex) { $vertexNode = $this.edges.find($null, { param($edge) $edge.startVertex -eq $vertex -or $edge.endVertex -eq $vertex }) return !!$vertexNode } [object] findEdge($vertex) { $edgeFinder ={ param($edge) return $edge.startVertex -eq $vertex -Or $edge.endVertex -eq $vertex }; $targetEdge = $this.edges.find($null, $edgeFinder) if($targetEdge) { return $targetEdge.value } return $null } [object] getKey() { return $this.value } [object] deleteAllEdges() { foreach ($edge in $this.getEdges()) { $this.deleteEdge($edge) } return $this } [string] toString() { return $this.toString($null) } [string] toString($callback) { if ($callback) { return &$callback($this.value) } return $this.value -join '_' } } class Graph { $vertices $edges [bool]$isDirected Graph () { $this.DoInit($false) } Graph ($isDirected) { $this.DoInit($isDirected) } DoInit($isDirected) { $this.vertices = [Ordered]@{} $this.edges = [Ordered]@{} $this.isDirected = $isDirected } [object] addVertex($newVertex) { $this.vertices[$newVertex.getKey()] = $newVertex return $this } [object] getVertexByKey($vertexKey) { return $this.vertices[$vertexKey] } [object] getNeighbors($vertex) { return $vertex.getNeighbors() } [object[]] getAllVertices() { return $this.vertices.values } [object[]] getAllEdges() { return $this.edges.values } [object] addEdge($edge) { # Try to find and end start vertices. $startVertex = $this.getVertexByKey($edge.startVertex.getKey()) $endVertex = $this.getVertexByKey($edge.endVertex.getKey()) # Insert start vertex if it wasn't inserted. if (!$startVertex) { $this.addVertex($edge.startVertex) $startVertex = $this.getVertexByKey($edge.startVertex.getKey()) } # Insert end vertex if it wasn't inserted. if (!$endVertex) { $this.addVertex($edge.endVertex) $endVertex = $this.getVertexByKey($edge.endVertex.getKey()) } # Check if edge has been already added. if ($this.edges[$edge.getKey()]) { throw 'Edge has already been added before' } else { $this.edges[$edge.getKey()] = $edge } # Add edge to the vertices. if ($this.isDirected) { # If graph IS directed then add the edge only to start vertex. $startVertex.addEdge($edge) } else { # If graph ISN'T directed then add the edge to both vertices. $startVertex.addEdge($edge) $endVertex.addEdge($edge) } return $this } deleteEdge($edge) { # Delete edge from the list of edges. if ($this.edges[$edge.getKey()]) { $this.edges.Remove($edge.getKey()) } else { throw 'Edge not found in graph' } # Try to find and end start vertices and delete edge from them. $startVertex = $this.getVertexByKey($edge.startVertex.getKey()) $endVertex = $this.getVertexByKey($edge.endVertex.getKey()) $startVertex.deleteEdge($edge) $endVertex.deleteEdge($edge) } [object] findEdge($startVertex, $endVertex) { $vertex = $this.getVertexByKey($startVertex.getKey()) return $vertex.findEdge($endVertex) } [Object] findVertexByKey($vertexKey) { if ($this.vertices[$vertexKey]) { return $this.vertices[$vertexKey] } return $null } [object] getWeight() { $weight = 0 foreach ($graphEdge in $this.getAllEdges()) { $weight += $graphEdge.weight } return $weight } [object] reverse() { foreach ($edge in $this.getAllEdges()) { # Delete straight edge from graph and from vertices. $this.deleteEdge($edge) # Reverse the edge. $edge.reverse() # Add reversed edge back to the graph and its vertices. $this.addEdge($edge) } return $this } [System.Collections.Specialized.OrderedDictionary] getVerticesIndices() { $verticesIndices = [Ordered]@{} $allVertices = $this.getAllVertices() for ($idx = 0; $idx -lt $allVertices.Count; $idx += 1) { $item = $allVertices[$idx] $verticesIndices.($item.getKey()) = $idx } return $verticesIndices } [object] getAdjacencyMatrix() { $targetVertices = $this.getAllVertices() $verticesIndices = $this.getVerticesIndices() # # Init matrix with infinities meaning that there is no ways of # # getting from one vertex to another yet. $adjacencyMatrix = (0..($targetVertices.Count)).ForEach{New-Object object[] ($targetVertices.Count)} for ($outer = 0; $outer -le $targetVertices.Count; $outer += 1) { for ($inner = 0; $inner -le $targetVertices.Count - 1; $inner += 1) { $adjacencyMatrix[$outer][$inner] = [double]::PositiveInfinity } } # Fill the columns. for ($vertexIndex = 0; $vertexIndex -le $targetVertices.Count - 1; $vertexIndex += 1) { $vertex = $targetVertices[$vertexIndex] foreach ($neighbor in $vertex.getNeighbors()) { $neighborIndex = $verticesIndices[$neighbor.getKey()] $adjacencyMatrix[$vertexIndex][$neighborIndex] = $this.findEdge($vertex, $neighbor).weight } } return $adjacencyMatrix } [string] toString() { return $this.vertices.keys -join "," } } ############################################################################### # data-structures.ps1 (end) ############################################################################### # Note: PowerShell doesn't support declaring an interface class IQuestion { [void]Prompt() { throw [NotImplementedException] } } class Question : IQuestion { [string] $promptText [bool] $hasResponse [bool] $isResponseEmpty [string] $response [bool] $isSecure [string] $validationExpr [string] $validationHelp [int] $minimumLength [int] $maximumLength [string[]] $blacklist [bool] $allowEmptyResponse [string] $emptyResponseLabel = 'Accept Default' [string] $emptyResponseHelp = 'Use default value by providing no response' static [string] hidden $returnQuestionLabel = 'Return to Step' static [string] hidden $previousStepLabel = 'Back to Previous Step' Question([string] $promptText) { $this.promptText = $promptText } [void]Prompt() { [string] $result = '' while ($true) { $result = Read-HostText $this.promptText $this.minimumLength $this.maximumLength $this.blacklist $this.isSecure $this.validationExpr $this.validationHelp -allowBlankEntry if ($result -ne '') { break } $options = @() if ($this.allowEmptyResponse) { $options += [tuple]::Create($this.emptyResponseLabel, $this.emptyResponseHelp) } $options += @( [tuple]::Create([question]::returnQuestionLabel, 'Provide a response to the question'), [tuple]::Create([question]::previousStepLabel, 'Go back to the previous step') ) $choice = Read-HostChoice 'What do you want to do?' $options if (($options[$choice]).item1 -eq $this.emptyResponseLabel) { $this.isResponseEmpty = $true break } if (($options[$choice]).item1 -eq [question]::previousStepLabel) { $this.hasResponse = $false $this.response = '' return } } $this.hasResponse = $true $this.response = $result } [string] GetResponse([string] $whenEmpty) { if (!$this.hasResponse) { return $null } return $this.isResponseEmpty ? $whenEmpty : $this.response } } class ConfirmationQuestion : Question { ConfirmationQuestion([string] $promptText) : base($promptText) { } [void]Prompt() { $prompt = $this.promptText while ($true) { ([Question]$this).Prompt() if (-not $this.hasResponse -or $this.isResponseEmpty) { break } $response = $this.response $this.promptText = 'Confirm' $this.response = '' ([Question]$this).Prompt() if (-not $this.hasResponse -or $this.isResponseEmpty -or $response -eq $this.response) { break } Write-Host 'Responses do not match. Try again.' $this.promptText = $prompt $this.response = '' } $this.promptText = $prompt } } class IntegerQuestion : Question { [int] $minimum [int] $maximum [int] $intResponse IntegerQuestion([string] $promptText, [int] $minimum, [int] $maximum, [bool] $allowEmptyResponse) : base($promptText) { if ($minimum -gt $maximum) { throw "Unexpected min/max values - $minimum is greater than $maximum." } $this.minimum = $minimum $this.maximum = $maximum $this.allowEmptyResponse = $allowEmptyResponse $this.validationExpr = "^\d+$" $this.validationHelp = "Enter a number between $minimum and $maximum." } [void]Prompt() { while ($true) { ([Question]$this).Prompt() if (-not $this.hasResponse) { return } if ($this.isResponseEmpty) { break } [int] $val = 0 if ([Int]::TryParse($this.response, [ref]$val)) { if ($val -lt $this.minimum -or $val -gt $this.maximum) { Write-Host $this.validationHelp continue } $this.intResponse = $val break } } } } class PathQuestion : Question { [bool] $isDirectory PathQuestion([string] $promptText, [bool] $isDirectory, [bool] $allowEmptyResponse) : base($promptText) { $this.isDirectory = $isDirectory $this.allowEmptyResponse = $allowEmptyResponse } [void]Prompt() { while ($true) { ([Question]$this).Prompt() if (-not $this.hasResponse) { return } if ($this.isResponseEmpty) { break } $pathType = 'Leaf' if ($this.isDirectory) { $pathType = 'Container' } if (Test-Path $this.response -PathType $pathType) { break } $path = $this.response.Trim("'").Trim('"') if (Test-Path $path -PathType $pathType) { $this.response = $path break } $pathTypeMessage = 'file' if ($this.isDirectory) { $pathTypeMessage = 'directory' } Write-Host "Unable to read $pathTypeMessage '$($this.response)' - the $pathTypeMessage may not exist or you may not have permissions to read it - please enter another $pathTypeMessage path" } } } class CertificateFileQuestion : PathQuestion { CertificateFileQuestion([string] $promptText, [bool] $allowEmptyResponse) : base($promptText, $false, $allowEmptyResponse) { } [void]Prompt() { while ($true) { ([PathQuestion]$this).Prompt() if (-not $this.hasResponse) { return } if ($this.isResponseEmpty) { break } if (Test-KeyToolCertificate $this.response) { break } Write-Host "Unable to read certificate file '$($this.response)' - does the file contain a certificate?" } } } class EmailAddressQuestion : Question { EmailAddressQuestion([string] $promptText, [bool] $allowEmptyResponse) : base($promptText) { $this.allowEmptyResponse = $allowEmptyResponse } [void]Prompt() { while ($true) { ([Question]$this).Prompt() if (-not $this.hasResponse) { return } if ($this.isResponseEmpty) { break } if (Test-EmailAddress $this.response) { break } Write-Host "'$($this.response)' is an invalid email address." } } } class MultipleChoiceQuestion : IQuestion { [string] $promptText [tuple`2[string,string][]] $options [int] $defaultOption [bool] $hasResponse [int] $choice MultipleChoiceQuestion([string] $promptText, [tuple`2[string,string][]] $options, [int] $defaultOption) { $this.promptText = $promptText $this.options = $options $this.options += [tuple]::Create([question]::previousStepLabel, 'Go back to the previous step') $this.defaultOption = $defaultOption } [void]Prompt() { $this.choice = Read-HostChoice $this.promptText $this.options $this.defaultOption $this.hasResponse = $this.options[$this.choice].item1 -ne [question]::previousStepLabel } } class YesNoQuestion : MultipleChoiceQuestion { YesNoQuestion([string] $promptText, [string] $yesHelp, [string] $noHelp, [int] $defaultOption) : base($promptText, @( [tuple]::Create('Yes', $yesHelp), [tuple]::Create('No', $noHelp) ), $defaultOption) {} } class GuidedSetupStep : GraphVertex { [string] $name [string] $title [string] $message [string] $prompt GuidedSetupStep([string] $name, [string] $title, [string] $message, [string] $prompt) : base($name) { $this.name = $name $this.title = $title $this.message = $message $this.prompt = $prompt } [bool]CanRun() { return $true } [bool]Run() { Write-HostSection $this.title ($this.GetMessage()) while ($true) { $question = $this.MakeQuestion($this.prompt) $question.Prompt() if (-not $question.hasResponse) { return $false } if ($this.HandleResponse($question)) { break } } return $true } [IQuestion]MakeQuestion([string]$prompt) { return new-object Question($prompt) } [bool]HandleResponse([IQuestion] $question) { throw [NotImplementedException] } [string]GetMessage() { return $this.message } [void]Reset() { } [void]ApplyDefault() { throw [NotImplementedException] } [string]GetDefault() { return '' } [void]Delay() { Start-Sleep -Seconds 1 } [object]toString() { return $this.name } } Get-ChildItem "$PSScriptRoot/functions" -Recurse -Include '*.ps1' | ForEach-Object { . $_ } function Set-GuidedSetupModulePreferences([Management.Automation.ActionPreference] $errorPref, [Management.Automation.ActionPreference] $verbosePref) { Set-Variable -Scope "1" -Name 'ErrorActionPreference' -Value $errorPref Set-Variable -Scope "1" -Name 'VerbosePreference' -Value $verbosePref } Set-GuidedSetupModulePreferences ([Management.Automation.ActionPreference]::Stop) ([Management.Automation.ActionPreference]::Continue) |