diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index edf78c1..cb7800e 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -1,6 +1,6 @@ name: Publish Module on: - pull_request: + push: branches: [ master ] workflow_dispatch: jobs: diff --git a/.vscode/settings.json b/.vscode/settings.json index c97ebbc..51f4c60 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,7 @@ // -------- Search configuration -------- // Exclude the Output folder from search results. "search.exclude": { - "Output": true, + "Output/**": true }, //-------- PowerShell Configuration -------- // Use a custom PowerShell Script Analyzer settings file for this workspace. diff --git a/Plaster/Private/ConvertTo-DestinationRelativePath.ps1 b/Plaster/Private/ConvertTo-DestinationRelativePath.ps1 new file mode 100644 index 0000000..271a645 --- /dev/null +++ b/Plaster/Private/ConvertTo-DestinationRelativePath.ps1 @@ -0,0 +1,17 @@ +function ConvertTo-DestinationRelativePath { + param( + [ValidateNotNullOrEmpty()] + [string]$Path + ) + $fullDestPath = $DestinationPath + if (![System.IO.Path]::IsPathRooted($fullDestPath)) { + $fullDestPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) + } + + $fullPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) + if (!$fullPath.StartsWith($fullDestPath, 'OrdinalIgnoreCase')) { + throw ($LocalizedData.ErrorPathMustBeUnderDestPath_F2 -f $fullPath, $fullDestPath) + } + + $fullPath.Substring($fullDestPath.Length).TrimStart('\', '/') +} diff --git a/Plaster/Private/Copy-FileWithConflictDetection.ps1 b/Plaster/Private/Copy-FileWithConflictDetection.ps1 new file mode 100644 index 0000000..5cb7276 --- /dev/null +++ b/Plaster/Private/Copy-FileWithConflictDetection.ps1 @@ -0,0 +1,82 @@ +<# +Plaster zen for file handling. All file related operations should use this +method to actually write/overwrite/modify files in the DestinationPath. This +method handles detecting conflicts, gives the user a chance to determine how to +handle conflicts. The user can choose to use the Force parameter to force the +overwriting of existing files at the destination path. File processing +(expanding substitution variable, modifying file contents) should always be done +to a temp file (be sure to always remove temp file when done). That temp file is +what gets passed to this function as the $SrcPath. This allows Plaster to alert +the user when the repeated application of a template will modify any existing +file. + +NOTE: Plaster keeps track of which files it has "created" (as opposed to +overwritten) so that any later change to that file doesn't trigger conflict +handling. +#> +function Copy-FileWithConflictDetection { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [string]$SrcPath, + [string]$DstPath + ) + # Just double-checking that DstPath parameter is an absolute path otherwise + # it could fail the check that the DstPath is under the overall DestinationPath. + if (![System.IO.Path]::IsPathRooted($DstPath)) { + $DstPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DstPath) + } + + # Check if DstPath file conflicts with an existing SrcPath file. + $operation = $LocalizedData.OpCreate + $opMessage = ConvertTo-DestinationRelativePath $DstPath + if (Test-Path -LiteralPath $DstPath) { + if (Test-FilesIdentical $SrcPath $DstPath) { + $operation = $LocalizedData.OpIdentical + } elseif ($script:templateCreatedFiles.ContainsKey($DstPath)) { + # Plaster created this file previously during template invocation + # therefore, there is no conflict. We're simply updating the file. + $operation = $LocalizedData.OpUpdate + } elseif ($Force) { + $operation = $LocalizedData.OpForce + } else { + $operation = $LocalizedData.OpConflict + } + } + + # Copy the file to the destination + if ($PSCmdlet.ShouldProcess($DstPath, $operation)) { + Write-OperationStatus -Operation $operation -Message $opMessage + + if ($operation -eq $LocalizedData.OpIdentical) { + # If the files are identical, no need to do anything + return + } + + if ( + ($operation -eq $LocalizedData.OpCreate) -or + ($operation -eq $LocalizedData.OpUpdate) + ) { + Copy-Item -LiteralPath $SrcPath -Destination $DstPath + if ($PassThru) { + $InvokePlasterInfo.CreatedFiles += $DstPath + } + $script:templateCreatedFiles[$DstPath] = $null + } elseif ( + $Force -or + $PSCmdlet.ShouldContinue( + ($LocalizedData.OverwriteFile_F1 -f $DstPath), + $LocalizedData.FileConflict, + [ref]$script:fileConflictConfirmYesToAll, + [ref]$script:fileConflictConfirmNoToAll + ) + ) { + $backupFilename = New-BackupFilename $DstPath + Copy-Item -LiteralPath $DstPath -Destination $backupFilename + Copy-Item -LiteralPath $SrcPath -Destination $DstPath + if ($PassThru) { + $InvokePlasterInfo.UpdatedFiles += $DstPath + } + $script:templateCreatedFiles[$DstPath] = $null + } + } +} diff --git a/Plaster/Private/Expand-FileSourceSpec.ps1 b/Plaster/Private/Expand-FileSourceSpec.ps1 new file mode 100644 index 0000000..6f6cbe3 --- /dev/null +++ b/Plaster/Private/Expand-FileSourceSpec.ps1 @@ -0,0 +1,65 @@ +function Expand-FileSourceSpec { + [CmdletBinding()] + param( + [string]$SourceRelativePath, + [string]$DestinationRelativePath + ) + $srcPath = Join-Path $templateAbsolutePath $SourceRelativePath + $dstPath = Join-Path $destinationAbsolutePath $DestinationRelativePath + + if ($SourceRelativePath.IndexOfAny([char[]]('*', '?')) -lt 0) { + # No wildcard spec in srcRelPath so return info on single file. + # Also, if dstRelPath is empty, then use source rel path. + if (!$DestinationRelativePath) { + $dstPath = Join-Path $destinationAbsolutePath $SourceRelativePath + } + + return (New-FileSystemCopyInfo $srcPath $dstPath) + } + + # Prepare parameter values for call to Get-ChildItem to get list of files + # based on wildcard spec. + $gciParams = @{} + $parent = Split-Path $srcPath -Parent + $leaf = Split-Path $srcPath -Leaf + $gciParams['LiteralPath'] = $parent + $gciParams['File'] = $true + + if ($leaf -eq '**') { + $gciParams['Recurse'] = $true + } else { + if ($leaf.IndexOfAny([char[]]('*', '?')) -ge 0) { + $gciParams['Filter'] = $leaf + } + + $leaf = Split-Path $parent -Leaf + if ($leaf -eq '**') { + $parent = Split-Path $parent -Parent + $gciParams['LiteralPath'] = $parent + $gciParams['Recurse'] = $true + } + } + + $srcRelRootPathLength = $gciParams['LiteralPath'].Length + + # Generate a FileCopyInfo object for every file expanded by the wildcard spec. + $files = @(Microsoft.PowerShell.Management\Get-ChildItem @gciParams) + foreach ($file in $files) { + $fileSrcPath = $file.FullName + $relPath = $fileSrcPath.Substring($srcRelRootPathLength) + $fileDstPath = Join-Path $dstPath $relPath + New-FileSystemCopyInfo $fileSrcPath $fileDstPath + } + + # Copy over empty directories - if any. + $gciParams.Remove('File') + $gciParams['Directory'] = $true + $dirs = @(Microsoft.PowerShell.Management\Get-ChildItem @gciParams | + Where-Object { $_.GetFileSystemInfos().Length -eq 0 }) + foreach ($dir in $dirs) { + $dirSrcPath = $dir.FullName + $relPath = $dirSrcPath.Substring($srcRelRootPathLength) + $dirDstPath = Join-Path $dstPath $relPath + New-FileSystemCopyInfo $dirSrcPath $dirDstPath + } +} diff --git a/Plaster/Private/Get-ColorForOperation.ps1 b/Plaster/Private/Get-ColorForOperation.ps1 new file mode 100644 index 0000000..a541dc3 --- /dev/null +++ b/Plaster/Private/Get-ColorForOperation.ps1 @@ -0,0 +1,16 @@ +function Get-ColorForOperation { + param( + $operation + ) + switch ($operation) { + $LocalizedData.OpConflict { 'Red' } + $LocalizedData.OpCreate { 'Green' } + $LocalizedData.OpForce { 'Yellow' } + $LocalizedData.OpIdentical { 'Cyan' } + $LocalizedData.OpModify { 'Magenta' } + $LocalizedData.OpUpdate { 'Green' } + $LocalizedData.OpMissing { 'Red' } + $LocalizedData.OpVerify { 'Green' } + default { $Host.UI.RawUI.ForegroundColor } + } +} diff --git a/Plaster/Private/Get-ErrorLocationFileAttrVal.ps1 b/Plaster/Private/Get-ErrorLocationFileAttrVal.ps1 new file mode 100644 index 0000000..4331bcc --- /dev/null +++ b/Plaster/Private/Get-ErrorLocationFileAttrVal.ps1 @@ -0,0 +1,7 @@ +function Get-ErrorLocationFileAttrVal { + param( + [string]$ElementName, + [string]$AttributeName + ) + $LocalizedData.ExpressionErrorLocationFile_F2 -f $ElementName, $AttributeName +} diff --git a/Plaster/Private/Get-ErrorLocationModifyAttrVal.ps1 b/Plaster/Private/Get-ErrorLocationModifyAttrVal.ps1 new file mode 100644 index 0000000..4167567 --- /dev/null +++ b/Plaster/Private/Get-ErrorLocationModifyAttrVal.ps1 @@ -0,0 +1,6 @@ +function Get-ErrorLocationModifyAttrVal { + param( + [string]$AttributeName + ) + $LocalizedData.ExpressionErrorLocationModify_F1 -f $AttributeName +} diff --git a/Plaster/Private/Get-ErrorLocationNewModManifestAttrVal.ps1 b/Plaster/Private/Get-ErrorLocationNewModManifestAttrVal.ps1 new file mode 100644 index 0000000..60e0cf3 --- /dev/null +++ b/Plaster/Private/Get-ErrorLocationNewModManifestAttrVal.ps1 @@ -0,0 +1,6 @@ +function Get-ErrorLocationNewModManifestAttrVal { + param( + [string]$AttributeName + ) + $LocalizedData.ExpressionErrorLocationNewModManifest_F1 -f $AttributeName +} diff --git a/Plaster/Private/Get-ErrorLocationParameterAttrVal.ps1 b/Plaster/Private/Get-ErrorLocationParameterAttrVal.ps1 new file mode 100644 index 0000000..8f62254 --- /dev/null +++ b/Plaster/Private/Get-ErrorLocationParameterAttrVal.ps1 @@ -0,0 +1,7 @@ +function Get-ErrorLocationParameterAttrVal { + param( + [string]$ParameterName, + [string]$AttributeName + ) + $LocalizedData.ExpressionErrorLocationParameter_F2 -f $ParameterName, $AttributeName +} diff --git a/Plaster/Private/Get-ErrorLocationRequireModuleAttrVal.ps1 b/Plaster/Private/Get-ErrorLocationRequireModuleAttrVal.ps1 new file mode 100644 index 0000000..0f85b4d --- /dev/null +++ b/Plaster/Private/Get-ErrorLocationRequireModuleAttrVal.ps1 @@ -0,0 +1,7 @@ +function Get-ErrorLocationRequireModuleAttrVal { + param( + [string]$ModuleName, + [string]$AttributeName + ) + $LocalizedData.ExpressionErrorLocationRequireModule_F2 -f $ModuleName, $AttributeName +} diff --git a/Plaster/Private/Get-GitConfigValue.ps1 b/Plaster/Private/Get-GitConfigValue.ps1 new file mode 100644 index 0000000..b495c44 --- /dev/null +++ b/Plaster/Private/Get-GitConfigValue.ps1 @@ -0,0 +1,25 @@ +function Get-GitConfigValue { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$name + ) + # Very simplistic git config lookup + # Won't work with namespace, just use final element, e.g. 'name' instead of 'user.name' + + # The $Home dir may not be reachable e.g. if on network share and/or script not running as admin. + # See issue https://github.com/PowerShell/Plaster/issues/92 + if (!(Test-Path -LiteralPath $Home)) { + return + } + + $gitConfigPath = Join-Path $Home '.gitconfig' + $PSCmdlet.WriteDebug("Looking for '$name' value in Git config: $gitConfigPath") + + if (Test-Path -LiteralPath $gitConfigPath) { + $matches = Select-String -LiteralPath $gitConfigPath -Pattern "\s+$name\s+=\s+(.+)$" + if (@($matches).Count -gt 0) { + $matches.Matches.Groups[1].Value + } + } +} diff --git a/Plaster/Private/Get-ManifestsUnderPath.ps1 b/Plaster/Private/Get-ManifestsUnderPath.ps1 new file mode 100644 index 0000000..ca7a941 --- /dev/null +++ b/Plaster/Private/Get-ManifestsUnderPath.ps1 @@ -0,0 +1,61 @@ +function Get-ManifestsUnderPath { + <# + .SYNOPSIS + Retrieves Plaster manifest files under a specified path. + + .DESCRIPTION + This function searches for Plaster manifest files (`plasterManifest.xml`) + under a specified root path and returns template objects created from those + manifests. + + .PARAMETER RootPath + The root path to search for Plaster manifest files. + + .PARAMETER Recurse + Whether to search subdirectories for manifest files. + + .PARAMETER Name + The name of the template to retrieve. + If not specified, all templates will be returned. + + .PARAMETER Tag + The tag of the template to retrieve. + If not specified, templates with any tag will be returned. + + .EXAMPLE + Get-ManifestsUnderPath -RootPath "C:\Templates" -Recurse -Name "MyTemplate" -Tag "Tag1" + + Retrieves all Plaster templates named "MyTemplate" with the tag "Tag1" + under the "C:\Templates" directory and its subdirectories. + + .NOTES + This is a private function used internally by Plaster to manage templates. + It is not intended for direct use by end users. + #> + [CmdletBinding()] + param( + [string] + $RootPath, + [bool] + $Recurse, + [string] + $Name, + [string] + $Tag + ) + $getChildItemSplat = @{ + Path = $RootPath + Include = "plasterManifest.xml" + Recurse = $Recurse + } + $manifestPaths = Get-ChildItem @getChildItemSplat + foreach ($manifestPath in $manifestPaths) { + $newTemplateObjectFromManifestSplat = @{ + ManifestPath = $manifestPath + Name = $Name + Tag = $Tag + ErrorAction = 'SilentlyContinue' + } + New-TemplateObjectFromManifest @newTemplateObjectFromManifestSplat + } +} diff --git a/Plaster/Private/Get-MaxOperationLabelLength.ps1 b/Plaster/Private/Get-MaxOperationLabelLength.ps1 new file mode 100644 index 0000000..54e191b --- /dev/null +++ b/Plaster/Private/Get-MaxOperationLabelLength.ps1 @@ -0,0 +1,15 @@ +function Get-MaxOperationLabelLength { + [CmdletBinding()] + [OutputType([int])] + param() + ( + $LocalizedData.OpCreate, + $LocalizedData.OpIdentical, + $LocalizedData.OpConflict, + $LocalizedData.OpForce, + $LocalizedData.OpMissing, + $LocalizedData.OpModify, + $LocalizedData.OpUpdate, + $LocalizedData.OpVerify | + Measure-Object -Property Length -Maximum).Maximum +} diff --git a/Plaster/Private/Get-PSSnippetFunction.ps1 b/Plaster/Private/Get-PSSnippetFunction.ps1 new file mode 100644 index 0000000..f78dc6b --- /dev/null +++ b/Plaster/Private/Get-PSSnippetFunction.ps1 @@ -0,0 +1,12 @@ +function Get-PSSnippetFunction { + param( + [String]$FilePath + ) + + # Test if Path Exists + if (!(Test-Path $substitute -PathType Leaf)) { + throw ($LocalizedData.ErrorPathDoesNotExist_F1 -f $FilePath) + } + # Load File + return Get-Content -LiteralPath $substitute -Raw +} diff --git a/Plaster/Private/Invoke-ExpressionImpl.ps1 b/Plaster/Private/Invoke-ExpressionImpl.ps1 new file mode 100644 index 0000000..5df07fb --- /dev/null +++ b/Plaster/Private/Invoke-ExpressionImpl.ps1 @@ -0,0 +1,32 @@ +function Invoke-ExpressionImpl { + [CmdletBinding()] + param ( + [string]$Expression + ) + try { + $powershell = [PowerShell]::Create() + + if ($null -eq $script:constrainedRunspace) { + $script:constrainedRunspace = New-ConstrainedRunspace + } + $powershell.Runspace = $script:constrainedRunspace + + try { + $powershell.AddScript($Expression) > $null + $res = $powershell.Invoke() + $res + } catch { + throw ($LocalizedData.ExpressionInvalid_F2 -f $Expression, $_) + } + + # Check for non-terminating errors. + if ($powershell.Streams.Error.Count -gt 0) { + $err = $powershell.Streams.Error[0] + throw ($LocalizedData.ExpressionNonTermErrors_F2 -f $Expression, $err) + } + } finally { + if ($powershell) { + $powershell.Dispose() + } + } +} diff --git a/Plaster/Private/New-BackupFilename.ps1 b/Plaster/Private/New-BackupFilename.ps1 new file mode 100644 index 0000000..533c6be --- /dev/null +++ b/Plaster/Private/New-BackupFilename.ps1 @@ -0,0 +1,16 @@ +function New-BackupFilename { + [CmdletBinding()] + param( + [string]$Path + ) + $dir = [System.IO.Path]::GetDirectoryName($Path) + $filename = [System.IO.Path]::GetFileName($Path) + $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak" + $i = 1 + while (Test-Path -LiteralPath $backupPath) { + $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak$i" + $i++ + } + + $backupPath +} diff --git a/Plaster/Private/New-ConstrainedRunspace.ps1 b/Plaster/Private/New-ConstrainedRunspace.ps1 new file mode 100644 index 0000000..afbe0a6 --- /dev/null +++ b/Plaster/Private/New-ConstrainedRunspace.ps1 @@ -0,0 +1,76 @@ +function New-ConstrainedRunspace { + [CmdletBinding()] + param () + $iss = [System.Management.Automation.Runspaces.InitialSessionState]::Create() + if (!$IsCoreCLR) { + $iss.ApartmentState = [System.Threading.ApartmentState]::STA + } + $iss.LanguageMode = [System.Management.Automation.PSLanguageMode]::ConstrainedLanguage + $iss.DisableFormatUpdates = $true + + $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'Environment', ([Microsoft.PowerShell.Commands.EnvironmentProvider]), $null + $iss.Providers.Add($sspe) + + $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'FileSystem', ([Microsoft.PowerShell.Commands.FileSystemProvider]), $null + $iss.Providers.Add($sspe) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Content', ([Microsoft.PowerShell.Commands.GetContentCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Date', ([Microsoft.PowerShell.Commands.GetDateCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ChildItem', ([Microsoft.PowerShell.Commands.GetChildItemCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Item', ([Microsoft.PowerShell.Commands.GetItemCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ItemProperty', ([Microsoft.PowerShell.Commands.GetItemPropertyCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Module', ([Microsoft.PowerShell.Commands.GetModuleCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Variable', ([Microsoft.PowerShell.Commands.GetVariableCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Test-Path', ([Microsoft.PowerShell.Commands.TestPathCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Out-String', ([Microsoft.PowerShell.Commands.OutStringCommand]), $null + $iss.Commands.Add($ssce) + + $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Compare-Object', ([Microsoft.PowerShell.Commands.CompareObjectCommand]), $null + $iss.Commands.Add($ssce) + + $scopedItemOptions = [System.Management.Automation.ScopedItemOptions]::AllScope + $plasterVars = Get-Variable -Name PLASTER_*, PSVersionTable + if (Test-Path Variable:\IsLinux) { + $plasterVars += Get-Variable -Name IsLinux + } + if (Test-Path Variable:\IsOSX) { + $plasterVars += Get-Variable -Name IsOSX + } + if (Test-Path Variable:\IsMacOS) { + $plasterVars += Get-Variable -Name IsMacOS + } + if (Test-Path Variable:\IsWindows) { + $plasterVars += Get-Variable -Name IsWindows + } + foreach ($var in $plasterVars) { + $ssve = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry ` + $var.Name, $var.Value, $var.Description, $scopedItemOptions + $iss.Variables.Add($ssve) + } + + # Create new runspace with the above defined entries. Then open and set its working dir to $destinationAbsolutePath + # so all condition attribute expressions can use a relative path to refer to file paths e.g. + # condition="Test-Path src\${PLASTER_PARAM_ModuleName}.psm1" + $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($iss) + $runspace.Open() + if ($destinationAbsolutePath) { + $runspace.SessionStateProxy.Path.SetLocation($destinationAbsolutePath) > $null + } + $runspace +} diff --git a/Plaster/Private/New-FileSystemCopyInfo.ps1 b/Plaster/Private/New-FileSystemCopyInfo.ps1 new file mode 100644 index 0000000..7c4c76a --- /dev/null +++ b/Plaster/Private/New-FileSystemCopyInfo.ps1 @@ -0,0 +1,11 @@ +function New-FileSystemCopyInfo { + [CmdletBinding()] + param( + [string]$srcPath, + [string]$dstPath + ) + [PSCustomObject]@{ + SrcFileName = $srcPath + DstFileName = $dstPath + } +} diff --git a/Plaster/Private/New-TemplateObjectFromManifest.ps1 b/Plaster/Private/New-TemplateObjectFromManifest.ps1 new file mode 100644 index 0000000..d937318 --- /dev/null +++ b/Plaster/Private/New-TemplateObjectFromManifest.ps1 @@ -0,0 +1,59 @@ +function New-TemplateObjectFromManifest { + <# + .SYNOPSIS + Creates a Plaster template object from a manifest file. + + .DESCRIPTION + This function takes a path to a Plaster manifest file and creates a + template object from its contents. + + .PARAMETER ManifestPath + The path to the Plaster manifest file. + + .PARAMETER Name + The name of the template. + If not specified, all templates will be returned. + + .PARAMETER Tag + The tag of the template. + If not specified, templates with any tag will be returned. + + .EXAMPLE + Get-TemplateObjectFromManifest -ManifestPath "C:\Templates\MyTemplate\plasterManifest.xml" -Name "MyTemplate" -Tag "Tag1" + + Retrieves a template object for the specified manifest file with the given name and tag. + .NOTES + This function is used internally by Plaster to manage templates. + It is not intended for direct use by end users. + #> + [CmdletBinding()] + [OutputType([PSCustomObject])] + param ( + [System.IO.FileInfo]$ManifestPath, + [string]$Name, + [string]$Tag + ) + + $manifestXml = Test-PlasterManifest -Path $ManifestPath + $metadata = $manifestXml["plasterManifest"]["metadata"] + + $manifestObj = [PSCustomObject]@{ + Name = $metadata["name"].InnerText + Title = $metadata["title"].InnerText + Author = $metadata["author"].InnerText + Version = [System.Version]::Parse($metadata["version"].InnerText) + Description = $metadata["description"].InnerText + Tags = $metadata["tags"].InnerText.split(",") | ForEach-Object { $_.Trim() } + TemplatePath = $manifestPath.Directory.FullName + } + + $manifestObj.PSTypeNames.Insert(0, "Microsoft.PowerShell.Plaster.PlasterTemplate") + $addMemberSplat = @{ + MemberType = 'ScriptMethod' + InputObject = $manifestObj + Name = "InvokePlaster" + Value = { Invoke-Plaster -TemplatePath $this.TemplatePath } + } + Add-Member @addMemberSplat + return $manifestObj | Where-Object Name -Like $Name | Where-Object Tags -Like $Tag +} diff --git a/Plaster/Private/Read-PromptForChoice.ps1 b/Plaster/Private/Read-PromptForChoice.ps1 new file mode 100644 index 0000000..3f47ed2 --- /dev/null +++ b/Plaster/Private/Read-PromptForChoice.ps1 @@ -0,0 +1,48 @@ +function Read-PromptForChoice { + [CmdletBinding()] + param( + [string] + $ParameterName, + [ValidateNotNull()] + $ChoiceNodes, + [string] + $prompt, + [int[]] + $defaults, + [switch] + $IsMultiChoice + ) + $choices = New-Object 'System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]' + $values = New-Object object[] $ChoiceNodes.Count + $i = 0 + + foreach ($choiceNode in $ChoiceNodes) { + $label = Resolve-AttributeValue $choiceNode.label (Get-ErrorLocationParameterAttrVal $ParameterName label) + $help = Resolve-AttributeValue $choiceNode.help (Get-ErrorLocationParameterAttrVal $ParameterName help) + $value = Resolve-AttributeValue $choiceNode.value (Get-ErrorLocationParameterAttrVal $ParameterName value) + + $choice = New-Object System.Management.Automation.Host.ChoiceDescription -Arg $label, $help + $choices.Add($choice) + $values[$i++] = $value + } + + $returnValue = [PSCustomObject]@{Values = @(); Indices = @() } + + if ($IsMultiChoice) { + $selections = $Host.UI.PromptForChoice('', $prompt, $choices, $defaults) + foreach ($selection in $selections) { + $returnValue.Values += $values[$selection] + $returnValue.Indices += $selection + } + } else { + if ($defaults.Count -gt 1) { + throw ($LocalizedData.ParameterTypeChoiceMultipleDefault_F1 -f $ChoiceNodes.ParentNode.name) + } + + $selection = $Host.UI.PromptForChoice('', $prompt, $choices, $defaults[0]) + $returnValue.Values = $values[$selection] + $returnValue.Indices = $selection + } + + $returnValue +} diff --git a/Plaster/Private/Read-PromptForInput.ps1 b/Plaster/Private/Read-PromptForInput.ps1 new file mode 100644 index 0000000..e4aa2bf --- /dev/null +++ b/Plaster/Private/Read-PromptForInput.ps1 @@ -0,0 +1,27 @@ +function Read-PromptForInput { + [CmdletBinding()] + param( + $prompt, + $default, + $pattern + ) + if (!$pattern) { + $patternMatch = $true + } + + do { + $value = Read-Host -Prompt $prompt + if (!$value -and $default) { + $value = $default + $patternMatch = $true + } elseif ($value -and $pattern) { + if ($value -match $pattern) { + $patternMatch = $true + } else { + $PSCmdlet.WriteDebug("Value '$value' did not match the pattern '$pattern'") + } + } + } while (!$value -or !$patternMatch) + + $value +} diff --git a/Plaster/Private/Resolve-AttributeValue.ps1 b/Plaster/Private/Resolve-AttributeValue.ps1 new file mode 100644 index 0000000..136df1a --- /dev/null +++ b/Plaster/Private/Resolve-AttributeValue.ps1 @@ -0,0 +1,20 @@ +function Resolve-AttributeValue { + [CmdletBinding()] + param( + [string]$Value, + [string]$Location + ) + + if ($null -eq $Value) { + return [string]::Empty + } elseif ([string]::IsNullOrWhiteSpace($Value)) { + return $Value + } + + try { + $res = @(Invoke-ExpressionImpl "`"$Value`"") + [string]$res[0] + } catch { + throw ($LocalizedData.InterpolationError_F3 -f $Value.Trim(), $Location, $_) + } +} diff --git a/Plaster/Private/Resolve-ProcessMessage.ps1 b/Plaster/Private/Resolve-ProcessMessage.ps1 new file mode 100644 index 0000000..d119044 --- /dev/null +++ b/Plaster/Private/Resolve-ProcessMessage.ps1 @@ -0,0 +1,23 @@ +function Resolve-ProcessMessage { + [CmdletBinding()] + param( + [ValidateNotNull()] + $Node + ) + $text = Resolve-AttributeValue $Node.InnerText '' + $noNewLine = $Node.nonewline -eq 'true' + + # Eliminate whitespace before and after the text that just happens to get inserted because you want + # the text on different lines than the start/end element tags. + $trimmedText = $text -replace '^[ \t]*\n', '' -replace '\n[ \t]*$', '' + + $condition = $Node.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + $debugText = $trimmedText -replace '\r|\n', ' ' + $maxLength = [Math]::Min(40, $debugText.Length) + $PSCmdlet.WriteDebug("Skipping message '$($debugText.Substring(0, $maxLength))', condition evaluated to false.") + return + } + + Write-Host $trimmedText -NoNewline:($noNewLine -eq 'true') +} diff --git a/Plaster/Private/Resolve-ProcessNewModuleManifest.ps1 b/Plaster/Private/Resolve-ProcessNewModuleManifest.ps1 new file mode 100644 index 0000000..42d9afd --- /dev/null +++ b/Plaster/Private/Resolve-ProcessNewModuleManifest.ps1 @@ -0,0 +1,118 @@ +function Resolve-ProcessNewModuleManifest { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [ValidateNotNull()]$Node + ) + $moduleVersion = Resolve-AttributeValue $Node.moduleVersion (Get-ErrorLocationNewModManifestAttrVal moduleVersion) + $rootModule = Resolve-AttributeValue $Node.rootModule (Get-ErrorLocationNewModManifestAttrVal rootModule) + $author = Resolve-AttributeValue $Node.author (Get-ErrorLocationNewModManifestAttrVal author) + $companyName = Resolve-AttributeValue $Node.companyName (Get-ErrorLocationNewModManifestAttrVal companyName) + $description = Resolve-AttributeValue $Node.description (Get-ErrorLocationNewModManifestAttrVal description) + $dstRelPath = Resolve-AttributeValue $Node.destination (Get-ErrorLocationNewModManifestAttrVal destination) + $powerShellVersion = Resolve-AttributeValue $Node.powerShellVersion (Get-ErrorLocationNewModManifestAttrVal powerShellVersion) + $nestedModules = Resolve-AttributeValue $Node.NestedModules (Get-ErrorLocationNewModManifestAttrVal NestedModules) + $dscResourcesToExport = Resolve-AttributeValue $Node.DscResourcesToExport (Get-ErrorLocationNewModManifestAttrVal DscResourcesToExport) + $copyright = Resolve-AttributeValue $Node.copyright (Get-ErrorLocationNewModManifestAttrVal copyright) + + # We could choose to not check this if the condition eval'd to false + # but I think it is better to let the template author know they've broken the + # rules for any of the file directives (not just the ones they're testing/enabled). + if ([System.IO.Path]::IsPathRooted($dstRelPath)) { + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) + } + + $dstPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $dstRelPath)) + + $condition = $Node.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + $PSCmdlet.WriteDebug("Skipping module manifest generation for '$dstPath', condition evaluated to false.") + return + } + + $encoding = $Node.encoding + if (!$encoding) { + $encoding = $DefaultEncoding + } + + if ($PSCmdlet.ShouldProcess($dstPath, $LocalizedData.ShouldProcessNewModuleManifest)) { + $manifestDir = Split-Path $dstPath -Parent + if (!(Test-Path $manifestDir)) { + Test-PathIsUnderDestinationPath $manifestDir + Write-Verbose ($LocalizedData.NewModManifest_CreatingDir_F1 -f $manifestDir) + New-Item $manifestDir -ItemType Directory > $null + } + + $newModuleManifestParams = @{} + + # If there is an existing module manifest, load it so we can reuse old values not specified by + # template. + if (Test-Path -LiteralPath $dstPath) { + $manifestFileName = Split-Path $dstPath -Leaf + $newModuleManifestParams = Import-LocalizedData -BaseDirectory $manifestDir -FileName $manifestFileName + if ($newModuleManifestParams.PrivateData) { + $newModuleManifestParams += $newModuleManifestParams.PrivateData.psdata + $newModuleManifestParams.Remove('PrivateData') + } + } + + if (![string]::IsNullOrWhiteSpace($moduleVersion)) { + $newModuleManifestParams['ModuleVersion'] = $moduleVersion + } + if (![string]::IsNullOrWhiteSpace($rootModule)) { + $newModuleManifestParams['RootModule'] = $rootModule + } + if (![string]::IsNullOrWhiteSpace($author)) { + $newModuleManifestParams['Author'] = $author + } + if (![string]::IsNullOrWhiteSpace($companyName)) { + $newModuleManifestParams['CompanyName'] = $companyName + } + if (![string]::IsNullOrWhiteSpace($copyright)) { + $newModuleManifestParams['Copyright'] = $copyright + } + if (![string]::IsNullOrWhiteSpace($description)) { + $newModuleManifestParams['Description'] = $description + } + if (![string]::IsNullOrWhiteSpace($powerShellVersion)) { + $newModuleManifestParams['PowerShellVersion'] = $powerShellVersion + } + if (![string]::IsNullOrWhiteSpace($nestedModules)) { + $newModuleManifestParams['NestedModules'] = $nestedModules + } + if (![string]::IsNullOrWhiteSpace($dscResourcesToExport)) { + $newModuleManifestParams['DscResourcesToExport'] = $dscResourcesToExport + } + + $tempFile = $null + + try { + $tempFileBaseName = "moduleManifest-" + [Guid]::NewGuid() + $tempFile = [System.IO.Path]::GetTempPath() + "${tempFileBaseName}.psd1" + $PSCmdlet.WriteDebug("Created temp file for new module manifest - $tempFile") + $newModuleManifestParams['Path'] = $tempFile + + # Generate manifest into a temp file. + New-ModuleManifest @newModuleManifestParams + + # Typically the manifest is re-written with a new encoding (UTF8-NoBOM) because Git hates UTF-16. + $content = Get-Content -LiteralPath $tempFile -Raw + + # Replace the temp filename in the generated manifest file's comment header with the actual filename. + $dstBaseName = [System.IO.Path]::GetFileNameWithoutExtension($dstPath) + $content = $content -replace "(?<=\s*#.*?)$tempFileBaseName", $dstBaseName + + Write-ContentWithEncoding -Path $tempFile -Content $content -Encoding $encoding + + Copy-FileWithConflictDetection $tempFile $dstPath + + if ($PassThru -and ($Node.openInEditor -eq 'true')) { + $InvokePlasterInfo.OpenFiles += $dstPath + } + } finally { + if ($tempFile -and (Test-Path $tempFile)) { + Remove-Item -LiteralPath $tempFile + $PSCmdlet.WriteDebug("Removed temp file for new module manifest - $tempFile") + } + } + } +} diff --git a/Plaster/Private/Resolve-ProcessParameter.ps1 b/Plaster/Private/Resolve-ProcessParameter.ps1 new file mode 100644 index 0000000..e7ba7db --- /dev/null +++ b/Plaster/Private/Resolve-ProcessParameter.ps1 @@ -0,0 +1,152 @@ +function Resolve-ProcessParameter { + [CmdletBinding()] + param( + [ValidateNotNull()]$Node + ) + + $name = $Node.name + $type = $Node.type + $store = $Node.store + + $pattern = $Node.pattern + + $condition = $Node.condition + + $default = Resolve-AttributeValue $Node.default (Get-ErrorLocationParameterAttrVal $name default) + + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + if (-not [string]::IsNullOrEmpty($default) -and $type -eq 'text') { + Set-PlasterVariable -Name $name -Value $default -IsParam $true + $PSCmdlet.WriteDebug("The condition of the parameter $($name) with the type 'text' evaluated to false. The parameter has a default value which will be used.") + } else { + # Define the parameter so later conditions can use it but its value will be $null + Set-PlasterVariable -Name $name -Value $null -IsParam $true + $PSCmdlet.WriteDebug("Skipping parameter $($name), condition evaluated to false.") + } + + return + } + + $prompt = Resolve-AttributeValue $Node.prompt (Get-ErrorLocationParameterAttrVal $name prompt) + + # Check if parameter was provided via a dynamic parameter. + if ($script:boundParameters.ContainsKey($name)) { + $value = $script:boundParameters[$name] + } else { + # Not a dynamic parameter so prompt user for the value but first check for a stored default value. + if ($store -and ($null -ne $script:defaultValueStore[$name])) { + $default = $script:defaultValueStore[$name] + $PSCmdlet.WriteDebug("Read default value '$default' for parameter '$name' from default value store.") + + if (($store -eq 'encrypted') -and ($default -is [System.Security.SecureString])) { + try { + $cred = New-Object -TypeName PSCredential -ArgumentList 'jsbplh', $default + $default = $cred.GetNetworkCredential().Password + $PSCmdlet.WriteDebug("Unencrypted default value for parameter '$name'.") + } catch [System.Exception] { + Write-Warning ($LocalizedData.ErrorUnencryptingSecureString_F1 -f $name) + } + } + } + + # If the prompt message failed to evaluate or was empty, supply a diagnostic prompt message + if (!$prompt) { + $prompt = $LocalizedData.MissingParameterPrompt_F1 -f $name + } + + # Some default values might not come from the template e.g. some are harvested from .gitconfig if it exists. + $defaultNotFromTemplate = $false + + $splat = @{} + + if ($null -ne $pattern) { + $splat.Add('pattern', $pattern) + } + + # Now prompt user for parameter value based on the parameter type. + switch -regex ($type) { + 'text' { + # Display an appropriate "default" value in the prompt string. + if ($default) { + if ($store -eq 'encrypted') { + $obscuredDefault = $default -replace '(....).*', '$1****' + $prompt += " ($obscuredDefault)" + } else { + $prompt += " ($default)" + } + } + # Prompt the user for text input. + $value = Read-PromptForInput $prompt $default @splat + $valueToStore = $value + } + 'user-fullname' { + # If no default, try to get a name from git config. + if (!$default) { + $default = Get-GitConfigValue('name') + $defaultNotFromTemplate = $true + } + + if ($default) { + if ($store -eq 'encrypted') { + $obscuredDefault = $default -replace '(....).*', '$1****' + $prompt += " ($obscuredDefault)" + } else { + $prompt += " ($default)" + } + } + + # Prompt the user for text input. + $value = Read-PromptForInput $prompt $default @splat + $valueToStore = $value + } + 'user-email' { + # If no default, try to get an email from git config + if (-not $default) { + $default = Get-GitConfigValue('email') + $defaultNotFromTemplate = $true + } + + if ($default) { + if ($store -eq 'encrypted') { + $obscuredDefault = $default -replace '(....).*', '$1****' + $prompt += " ($obscuredDefault)" + } else { + $prompt += " ($default)" + } + } + + # Prompt the user for text input. + $value = Read-PromptForInput $prompt $default @splat + $valueToStore = $value + } + 'choice|multichoice' { + $choices = $Node.ChildNodes + $defaults = [int[]]($default -split ',') + + # Prompt the user for choice or multichoice selection input. + $selections = Read-PromptForChoice $name $choices $prompt $defaults -IsMultiChoice:($type -eq 'multichoice') + $value = $selections.Values + $OFS = "," + $valueToStore = "$($selections.Indices)" + } + default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type, $Node.LocalName) } + } + + # If parameter specifies that user's input be stored as the default value, + # store it to file if the value has changed. + if ($store -and (($default -ne $valueToStore) -or $defaultNotFromTemplate)) { + if ($store -eq 'encrypted') { + $PSCmdlet.WriteDebug("Storing new, encrypted default value for parameter '$name' to default value store.") + $script:defaultValueStore[$name] = ConvertTo-SecureString -String $valueToStore -AsPlainText -Force + } else { + $PSCmdlet.WriteDebug("Storing new default value '$valueToStore' for parameter '$name' to default value store.") + $script:defaultValueStore[$name] = $valueToStore + } + + $script:flags.DefaultValueStoreDirty = $true + } + } + + # Make template defined parameters available as a PowerShell variable PLASTER_PARAM_. + Set-PlasterVariable -Name $name -Value $value -IsParam $true +} diff --git a/Plaster/Private/Set-PlasterVariable.ps1 b/Plaster/Private/Set-PlasterVariable.ps1 new file mode 100644 index 0000000..13a19e2 --- /dev/null +++ b/Plaster/Private/Set-PlasterVariable.ps1 @@ -0,0 +1,61 @@ +function Set-PlasterVariable { + <# + .SYNOPSIS + Sets a Plaster variable in the script scope and updates the + ConstrainedRunspace if it exists. + + .DESCRIPTION + This function sets a variable in the script scope and updates the + ConstrainedRunspace if it exists. It is used to manage Plaster variables, + which can be parameters or other types of variables. + + .PARAMETER Name + The name of the variable to set. + + .PARAMETER Value + The value to assign to the variable. + + .PARAMETER IsParam + Indicates if the variable is a parameter. + If true, the variable is treated as a Plaster parameter and prefixed with + "PLASTER_PARAM_". + + .EXAMPLE + Set-PlasterVariable -Name "MyVariable" -Value "MyValue" -IsParam $true + + Sets a Plaster parameter variable named "PLASTER_PARAM_MyVariable" with the + value "MyValue". + .NOTES + All Plaster variables should be set via this method so that the + ConstrainedRunspace can be configured to use the new variable. This method + will null out the ConstrainedRunspace so that later, when we need to + evaluate script in that runspace, it will get recreated first with all + the latest Plaster variables. + #> + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [Parameter(Mandatory = $true)] + [AllowNull()] + $Value, + + [Parameter()] + [bool] + $IsParam = $true + ) + + # Variables created from a in the Plaster manifest are prefixed + # PLASTER_PARAM all others are just PLASTER_. + $variableName = if ($IsParam) { "PLASTER_PARAM_$Name" } else { "PLASTER_$Name" } + + Set-Variable -Name $variableName -Value $Value -Scope Script -WhatIf:$false + + # If the constrained runspace has been created, it needs to be disposed so that the next string + # expansion (or condition eval) gets an updated runspace that contains this variable or its new value. + if ($null -ne $script:ConstrainedRunspace) { + $script:ConstrainedRunspace.Dispose() + $script:ConstrainedRunspace = $null + } +} diff --git a/Plaster/Private/Start-ProcessFile.ps1 b/Plaster/Private/Start-ProcessFile.ps1 new file mode 100644 index 0000000..52f5bd8 --- /dev/null +++ b/Plaster/Private/Start-ProcessFile.ps1 @@ -0,0 +1,123 @@ +# Processes both the and directives. +function Start-ProcessFile { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [ValidateNotNull()] + $Node + ) + $srcRelPath = Resolve-AttributeValue $Node.source (Get-ErrorLocationFileAttrVal $Node.localName source) + $dstRelPath = Resolve-AttributeValue $Node.destination (Get-ErrorLocationFileAttrVal $Node.localName destination) + + $condition = $Node.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + $PSCmdlet.WriteDebug("Skipping $($Node.localName) '$srcRelPath' -> '$dstRelPath', condition evaluated to false.") + return + } + + # Only validate paths for conditions that evaluate to true. + # The path may not be valid if it evaluates to false depending + # on whether or not conditional parameters are used in the template. + if ([System.IO.Path]::IsPathRooted($srcRelPath)) { + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $srcRelPath, $Node.LocalName) + } + + if ([System.IO.Path]::IsPathRooted($dstRelPath)) { + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) + } + + # Check if node is the specialized, node. + # Only nodes expand templates and use the encoding attribute. + $isTemplateFile = $Node.localName -eq 'templateFile' + if ($isTemplateFile) { + $encoding = $Node.encoding + if (!$encoding) { + $encoding = $DefaultEncoding + } + } + + # Check if source specifies a wildcard and if so, expand the wildcard + # and then process each file system object (file or empty directory). + $expandFileSourceSpecSplat = @{ + SourceRelativePath = $srcRelPath + DestinationRelativePath = $dstRelPath + } + $fileSystemCopyInfoObjs = Expand-FileSourceSpec @expandFileSourceSpecSplat + foreach ($fileSystemCopyInfo in $fileSystemCopyInfoObjs) { + $srcPath = $fileSystemCopyInfo.SrcFileName + $dstPath = $fileSystemCopyInfo.DstFileName + + # The file's destination path must be under the DestinationPath specified by the user. + Test-PathIsUnderDestinationPath $dstPath + + # Check to see if we're copying an empty dir + if (Test-Path -LiteralPath $srcPath -PathType Container) { + if (!(Test-Path -LiteralPath $dstPath)) { + if ($PSCmdlet.ShouldProcess($parentDir, $LocalizedData.ShouldProcessCreateDir)) { + Write-OperationStatus $LocalizedData.OpCreate ` + ($dstRelPath.TrimEnd(([char]'\'), ([char]'/')) + [System.IO.Path]::DirectorySeparatorChar) + New-Item -Path $dstPath -ItemType Directory > $null + } + } + + continue + } + + # If the file's parent dir doesn't exist, create it. + $parentDir = Split-Path $dstPath -Parent + if (!(Test-Path -LiteralPath $parentDir)) { + if ($PSCmdlet.ShouldProcess($parentDir, $LocalizedData.ShouldProcessCreateDir)) { + New-Item -Path $parentDir -ItemType Directory > $null + } + } + + $tempFile = $null + + try { + # If processing a , copy to a temp file to expand the template file, + # then apply the normal file conflict detection/resolution handling. + $target = $LocalizedData.TempFileTarget_F1 -f (ConvertTo-DestinationRelativePath $dstPath) + if ($isTemplateFile -and $PSCmdlet.ShouldProcess($target, $LocalizedData.ShouldProcessExpandTemplate)) { + $content = Get-Content -LiteralPath $srcPath -Raw + + # Eval script expression delimiters + if ($content -and ($content.Count -gt 0)) { + $newContent = [regex]::Replace($content, '(<%=)(.*?)(%>)', { + param($match) + $expr = $match.groups[2].value + $res = Test-Expression $expr "templateFile '$srcRelPath'" + $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") + $res + }, @('IgnoreCase')) + + # Eval script block delimiters + $newContent = [regex]::Replace($newContent, '(^<%)(.*?)(^%>)', { + param($match) + $expr = $match.groups[2].value + $res = Test-Script $expr "templateFile '$srcRelPath'" + $res = $res -join [System.Environment]::NewLine + $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") + $res + }, @('IgnoreCase', 'SingleLine', 'MultiLine')) + + $srcPath = $tempFile = [System.IO.Path]::GetTempFileName() + $PSCmdlet.WriteDebug("Created temp file for expanded templateFile - $tempFile") + + Write-ContentWithEncoding -Path $tempFile -Content $newContent -Encoding $encoding + } else { + $PSCmdlet.WriteDebug("Skipping template file expansion for $($Node.localName) '$srcPath', file is empty.") + } + } + + Copy-FileWithConflictDetection $srcPath $dstPath + + if ($PassThru -and ($Node.openInEditor -eq 'true')) { + $InvokePlasterInfo.OpenFiles += $dstPath + } + } finally { + if ($tempFile -and (Test-Path $tempFile)) { + Remove-Item -LiteralPath $tempFile + $PSCmdlet.WriteDebug("Removed temp file for expanded templateFile - $tempFile") + } + } + } +} diff --git a/Plaster/Private/Start-ProcessFileProcessRequireModule.ps1 b/Plaster/Private/Start-ProcessFileProcessRequireModule.ps1 new file mode 100644 index 0000000..218dd9e --- /dev/null +++ b/Plaster/Private/Start-ProcessFileProcessRequireModule.ps1 @@ -0,0 +1,97 @@ +function Start-ProcessFileProcessRequireModule { + [CmdletBinding()] + param( + [ValidateNotNull()] + $Node + ) + + $name = $Node.name + + $condition = $Node.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + $PSCmdlet.WriteDebug("Skipping $($Node.localName) for module '$name', condition evaluated to false.") + return + } + + $message = Resolve-AttributeValue $Node.message (Get-ErrorLocationRequireModuleAttrVal $name message) + $minimumVersion = $Node.minimumVersion + $maximumVersion = $Node.maximumVersion + $requiredVersion = $Node.requiredVersion + + $getModuleParams = @{ + ListAvailable = $true + ErrorAction = 'SilentlyContinue' + } + + # Configure $getModuleParams with correct parameters based on parameterset to be used. + # Also construct an array of version strings that can be displayed to the user. + $versionInfo = @() + if ($requiredVersion) { + $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name; RequiredVersion = $requiredVersion } + $versionInfo += $LocalizedData.RequireModuleRequiredVersion_F1 -f $requiredVersion + } elseif ($minimumVersion -or $maximumVersion) { + $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name } + + if ($minimumVersion) { + $getModuleParams.FullyQualifiedName["ModuleVersion"] = $minimumVersion + $versionInfo += $LocalizedData.RequireModuleMinVersion_F1 -f $minimumVersion + } + if ($maximumVersion) { + $getModuleParams.FullyQualifiedName["MaximumVersion"] = $maximumVersion + $versionInfo += $LocalizedData.RequireModuleMaxVersion_F1 -f $maximumVersion + } + } else { + $getModuleParams["Name"] = $name + } + + # Flatten array of version strings into a single string. + $versionRequirements = "" + if ($versionInfo.Length -gt 0) { + $OFS = ", " + $versionRequirements = " ($versionInfo)" + } + + # PowerShell v3 Get-Module command does not have the FullyQualifiedName parameter. + if ($PSVersionTable.PSVersion.Major -lt 4) { + $getModuleParams.Remove("FullyQualifiedName") + $getModuleParams["Name"] = $name + } + + $module = Get-Module @getModuleParams + + $moduleDesc = if ($versionRequirements) { "${name}:$versionRequirements" } else { $name } + + if ($null -eq $module) { + Write-OperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) + if ($message) { + Write-OperationAdditionalStatus $message + } + if ($PassThru) { + $InvokePlasterInfo.MissingModules += $moduleDesc + } + } else { + if ($PSVersionTable.PSVersion.Major -gt 3) { + Write-OperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) + } else { + # On V3, we have to the version matching with the results that Get-Module return. + $installedVersion = $module | Sort-Object Version -Descending | Select-Object -First 1 | ForEach-Object Version + if ($installedVersion.Build -eq -1) { + $installedVersion = [System.Version]"${installedVersion}.0.0" + } elseif ($installedVersion.Revision -eq -1) { + $installedVersion = [System.Version]"${installedVersion}.0" + } + + if (($requiredVersion -and ($installedVersion -ne $requiredVersion)) -or + ($minimumVersion -and ($installedVersion -lt $minimumVersion)) -or + ($maximumVersion -and ($installedVersion -gt $maximumVersion))) { + + Write-OperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) + if ($PassThru) { + $InvokePlasterInfo.MissingModules += $moduleDesc + } + } else { + Write-OperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) + } + } + } +} diff --git a/Plaster/Private/Start-ProcessModifyFile.ps1 b/Plaster/Private/Start-ProcessModifyFile.ps1 new file mode 100644 index 0000000..6c61c77 --- /dev/null +++ b/Plaster/Private/Start-ProcessModifyFile.ps1 @@ -0,0 +1,126 @@ +function Start-ProcessModifyFile { + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [ValidateNotNull()] + $Node + ) + $path = Resolve-AttributeValue $Node.path (Get-ErrorLocationModifyAttrVal path) + + # We could choose to not check this if the condition eval'd to false + # but I think it is better to let the template author know they've broken the + # rules for any of the file directives (not just the ones they're testing/enabled). + if ([System.IO.Path]::IsPathRooted($path)) { + throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $path, $Node.LocalName) + } + + $filePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $path)) + + # The file's path must be under the DestinationPath specified by the user. + Test-PathIsUnderDestinationPath $filePath + + $condition = $Node.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)>'")) { + $PSCmdlet.WriteDebug("Skipping $($Node.LocalName) of '$filePath', condition evaluated to false.") + return + } + + $fileContent = [string]::Empty + if (Test-Path -LiteralPath $filePath) { + $fileContent = Get-Content -LiteralPath $filePath -Raw + } + + # Set a Plaster (non-parameter) variable in this and the constrained runspace. + Set-PlasterVariable -Name 'FileContent' -Value $fileContent -IsParam $false + + $encoding = $Node.encoding + if (!$encoding) { + $encoding = $DefaultEncoding + } + + # If processing a directive, write the modified contents to a temp file, + # then apply the normal file conflict detection/resolution handling. + $target = $LocalizedData.TempFileTarget_F1 -f $filePath + if ($PSCmdlet.ShouldProcess($target, $LocalizedData.OpModify)) { + Write-OperationStatus $LocalizedData.OpModify ($LocalizedData.TempFileOperation_F1 -f (ConvertTo-DestinationRelativePath $filePath)) + + $modified = $false + + foreach ($childNode in $Node.ChildNodes) { + if ($childNode -isnot [System.Xml.XmlElement]) { continue } + + switch ($childNode.LocalName) { + 'replace' { + $condition = $childNode.condition + if ($condition -and !(Test-ConditionAttribute $condition "'<$($Node.LocalName)><$($childNode.LocalName)>'")) { + $PSCmdlet.WriteDebug("Skipping $($Node.LocalName) $($childNode.LocalName) of '$filePath', condition evaluated to false.") + continue + } + + if ($childNode.original -is [string]) { + $original = $childNode.original + } else { + $original = $childNode.original.InnerText + } + + if ($childNode.original.expand -eq 'true') { + $original = Resolve-AttributeValue $original (Get-ErrorLocationModifyAttrVal original) + } + + if ($childNode.substitute -is [string]) { + $substitute = $childNode.substitute + } else { + $substitute = $childNode.substitute.InnerText + } + + if ($childNode.substitute.isFile -eq 'true') { + $substitute = Get-PSSnippetFunction $substitute + } elseif ($childNode.substitute.expand -eq 'true') { + $substitute = Resolve-AttributeValue $substitute (Get-ErrorLocationModifyAttrVal substitute) + } + + # Perform Literal Replacement on FileContent (since it will have regex characters) + if ($childNode.substitute.isFile) { + $fileContent = $fileContent.Replace($original, $substitute) + } else { + $fileContent = $fileContent -replace $original, $substitute + } + + # Update the Plaster (non-parameter) variable's value in this and the constrained runspace. + Set-PlasterVariable -Name FileContent -Value $fileContent -IsParam $false + + $modified = $true + } + default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $childNode.LocalName) } + } + } + + $tempFile = $null + + try { + # We could use Copy-FileWithConflictDetection to handle the "identical" (not modified) case + # but if nothing was changed, I'd prefer not to generate a temp file, copy the unmodified contents + # into that temp file with hopefully the right encoding and then potentially overwrite the original file + # (different encoding will make the files look different) with the same contents but different encoding. + # If the intent of the was simply to change an existing file's encoding then the directive will + # need to make a whitespace change to the file. + if ($modified) { + $tempFile = [System.IO.Path]::GetTempFileName() + $PSCmdlet.WriteDebug("Created temp file for modified file - $tempFile") + + Write-ContentWithEncoding -Path $tempFile -Content $PLASTER_FileContent -Encoding $encoding + Copy-FileWithConflictDetection $tempFile $filePath + + if ($PassThru -and ($Node.openInEditor -eq 'true')) { + $InvokePlasterInfo.OpenFiles += $filePath + } + } else { + Write-OperationStatus $LocalizedData.OpIdentical (ConvertTo-DestinationRelativePath $filePath) + } + } finally { + if ($tempFile -and (Test-Path $tempFile)) { + Remove-Item -LiteralPath $tempFile + $PSCmdlet.WriteDebug("Removed temp file for modified file - $tempFile") + } + } + } +} diff --git a/Plaster/Private/Test-ConditionAttribute.ps1 b/Plaster/Private/Test-ConditionAttribute.ps1 new file mode 100644 index 0000000..05cbb2b --- /dev/null +++ b/Plaster/Private/Test-ConditionAttribute.ps1 @@ -0,0 +1,19 @@ +function Test-ConditionAttribute { + [CmdletBinding()] + param( + [string]$Expression, + [string]$Location + ) + if ($null -eq $Expression) { + return [string]::Empty + } elseif ([string]::IsNullOrWhiteSpace($Expression)) { + return $Expression + } + + try { + $res = @(Invoke-ExpressionImpl $Expression) + [bool]$res[0] + } catch { + throw ($LocalizedData.ExpressionInvalidCondition_F3 -f $Expression, $Location, $_) + } +} diff --git a/Plaster/Private/Test-Expression.ps1 b/Plaster/Private/Test-Expression.ps1 new file mode 100644 index 0000000..4e88cf8 --- /dev/null +++ b/Plaster/Private/Test-Expression.ps1 @@ -0,0 +1,19 @@ +function Test-Expression { + [CmdletBinding()] + param( + [string]$Expression, + [string]$Location + ) + if ($null -eq $Expression) { + return [string]::Empty + } elseif ([string]::IsNullOrWhiteSpace($Expression)) { + return $Expression + } + + try { + $res = @(Invoke-ExpressionImpl $Expression) + [string]$res[0] + } catch { + throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) + } +} diff --git a/Plaster/Private/Test-FilesIdentical.ps1 b/Plaster/Private/Test-FilesIdentical.ps1 new file mode 100644 index 0000000..4256496 --- /dev/null +++ b/Plaster/Private/Test-FilesIdentical.ps1 @@ -0,0 +1,18 @@ +function Test-FilesIdentical { + [CmdletBinding()] + param( + $Path1, + $Path2 + ) + $file1 = Get-Item -LiteralPath $Path1 -Force + $file2 = Get-Item -LiteralPath $Path2 -Force + + if ($file1.Length -ne $file2.Length) { + return $false + } + + $hash1 = (Get-FileHash -LiteralPath $path1 -Algorithm SHA1).Hash + $hash2 = (Get-FileHash -LiteralPath $path2 -Algorithm SHA1).Hash + + $hash1 -eq $hash2 +} diff --git a/Plaster/Private/Test-PathIsUnderDestinationPath.ps1 b/Plaster/Private/Test-PathIsUnderDestinationPath.ps1 new file mode 100644 index 0000000..58cfddd --- /dev/null +++ b/Plaster/Private/Test-PathIsUnderDestinationPath.ps1 @@ -0,0 +1,20 @@ +function Test-PathIsUnderDestinationPath() { + [CmdletBinding()] + param( + [ValidateNotNullOrEmpty()] + [string] + $FullPath + ) + if (![System.IO.Path]::IsPathRooted($FullPath)) { + $PSCmdlet.WriteDebug("The FullPath parameter '$FullPath' must be an absolute path.") + } + + $fullDestPath = $DestinationPath + if (![System.IO.Path]::IsPathRooted($fullDestPath)) { + $fullDestPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) + } + + if (!$FullPath.StartsWith($fullDestPath, [StringComparison]::OrdinalIgnoreCase)) { + throw ($LocalizedData.ErrorPathMustBeUnderDestPath_F2 -f $FullPath, $fullDestPath) + } +} diff --git a/Plaster/Private/Test-Script.ps1 b/Plaster/Private/Test-Script.ps1 new file mode 100644 index 0000000..b8be199 --- /dev/null +++ b/Plaster/Private/Test-Script.ps1 @@ -0,0 +1,19 @@ +function Test-Script { + [CmdletBinding()] + param( + [string]$Script, + [string]$Location + ) + if ($null -eq $Script) { + return @([string]::Empty) + } elseif ([string]::IsNullOrWhiteSpace($Script)) { + return $Script + } + + try { + $res = @(Invoke-ExpressionImpl $Script) + [string[]]$res + } catch { + throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) + } +} diff --git a/Plaster/Private/Write-ContentWithEncoding.ps1 b/Plaster/Private/Write-ContentWithEncoding.ps1 new file mode 100644 index 0000000..d69c4cd --- /dev/null +++ b/Plaster/Private/Write-ContentWithEncoding.ps1 @@ -0,0 +1,28 @@ +function Write-ContentWithEncoding { + [CmdletBinding()] + param( + [string] + $Path, + [string[]] + $Content, + [string] + $Encoding + ) + + if ($Encoding -match '-nobom') { + $Encoding, $dummy = $Encoding -split '-' + + $noBomEncoding = $null + switch ($Encoding) { + 'utf8' { $noBomEncoding = New-Object System.Text.UTF8Encoding($false) } + } + + if ($null -eq $Content) { + $Content = [string]::Empty + } + + [System.IO.File]::WriteAllLines($Path, $Content, $noBomEncoding) + } else { + Set-Content -LiteralPath $Path -Value $Content -Encoding $Encoding + } +} diff --git a/Plaster/Private/Write-OperationAdditionalStatus.ps1 b/Plaster/Private/Write-OperationAdditionalStatus.ps1 new file mode 100644 index 0000000..f2f261a --- /dev/null +++ b/Plaster/Private/Write-OperationAdditionalStatus.ps1 @@ -0,0 +1,13 @@ +function Write-OperationAdditionalStatus { + [CmdletBinding()] + param( + [string[]]$Message + ) + $maxLen = Get-MaxOperationLabelLength + foreach ($msg in $Message) { + $lines = $msg -split "`n" + foreach ($line in $lines) { + Write-Host ("{0,$maxLen} {1}" -f "", $line) + } + } +} diff --git a/Plaster/Private/Write-OperationStatus.ps1 b/Plaster/Private/Write-OperationStatus.ps1 new file mode 100644 index 0000000..2c1bff4 --- /dev/null +++ b/Plaster/Private/Write-OperationStatus.ps1 @@ -0,0 +1,10 @@ +function Write-OperationStatus { + [CmdletBinding()] + param( + $Operation, + $Message + ) + $maxLen = Get-MaxOperationLabelLength + Write-Host ("{0,$maxLen} " -f $Operation) -ForegroundColor (Get-ColorForOperation $Operation) -NoNewline + Write-Host $Message +} diff --git a/Plaster/Public/Get-PlasterTemplate.ps1 b/Plaster/Public/Get-PlasterTemplate.ps1 index 9452f97..c1f5ca9 100644 --- a/Plaster/Public/Get-PlasterTemplate.ps1 +++ b/Plaster/Public/Get-PlasterTemplate.ps1 @@ -46,33 +46,6 @@ function Get-PlasterTemplate { ) process { - function CreateTemplateObjectFromManifest([System.IO.FileInfo]$manifestPath, [string]$name, [string]$tag) { - - $manifestXml = Test-PlasterManifest -Path $manifestPath - $metadata = $manifestXml["plasterManifest"]["metadata"] - - $manifestObj = [PSCustomObject]@{ - Name = $metadata["name"].InnerText - Title = $metadata["title"].InnerText - Author = $metadata["author"].InnerText - Version = New-Object -TypeName "System.Version" -ArgumentList $metadata["version"].InnerText - Description = $metadata["description"].InnerText - Tags = $metadata["tags"].InnerText.split(",") | ForEach-Object { $_.Trim() } - TemplatePath = $manifestPath.Directory.FullName - } - - $manifestObj.PSTypeNames.Insert(0, "Microsoft.PowerShell.Plaster.PlasterTemplate") - Add-Member -MemberType ScriptMethod -InputObject $manifestObj -Name "InvokePlaster" -Value { Invoke-Plaster -TemplatePath $this.TemplatePath } - return $manifestObj | Where-Object Name -Like $name | Where-Object Tags -Like $tag - } - - function GetManifestsUnderPath([string]$rootPath, [bool]$recurse, [string]$name, [string]$tag) { - $manifestPaths = Get-ChildItem -Path $rootPath -Include "plasterManifest.xml" -Recurse:$recurse - foreach ($manifestPath in $manifestPaths) { - CreateTemplateObjectFromManifest $manifestPath $name $tag -ErrorAction SilentlyContinue - } - } - if ($Path) { # Is this a folder path or a Plaster manifest file path? if (!$Recurse.IsPresent) { @@ -82,14 +55,31 @@ function Get-PlasterTemplate { # Use Test-PlasterManifest to load the manifest file Write-Verbose "Attempting to get Plaster template at path: $Path" - CreateTemplateObjectFromManifest $Path $Name $Tag + $newTemplateObjectFromManifestSplat = @{ + ManifestPath = $Path + Name = $Name + Tag = $Tag + } + New-TemplateObjectFromManifest @newTemplateObjectFromManifestSplat } else { Write-Verbose "Attempting to get Plaster templates recursively under path: $Path" - GetManifestsUnderPath $Path $Recurse.IsPresent $Name $Tag + $getManifestsUnderPathSplat = @{ + RootPath = $Path + Recurse = $Recurse.IsPresent + Name = $Name + Tag = $Tag + } + Get-ManifestsUnderPath @getManifestsUnderPathSplat } } else { # Return all templates included with Plaster - GetManifestsUnderPath "$PSScriptRoot\Templates" $true $Name $Tag + $getManifestsUnderPathSplat = @{ + RootPath = "$PSScriptRoot\Templates" + Recurse = $true + Name = $Name + Tag = $Tag + } + Get-ManifestsUnderPath @getManifestsUnderPathSplat if ($IncludeInstalledModules.IsPresent) { # Search for templates in module path @@ -98,7 +88,6 @@ function Get-PlasterTemplate { ModuleVersion = $PlasterVersion ListAvailable = $ListAvailable } - $extensions = Get-ModuleExtension @GetModuleExtensionParams foreach ($extension in $extensions) { @@ -110,7 +99,13 @@ function Get-PlasterTemplate { $templatePath, "plasterManifest.xml") - CreateTemplateObjectFromManifest $expandedTemplatePath $Name $Tag -ErrorAction SilentlyContinue + $newTemplateObjectFromManifestSplat = @{ + ManifestPath = $expandedTemplatePath + Name = $Name + Tag = $Tag + ErrorAction = 'SilentlyContinue' + } + New-TemplateObjectFromManifest @newTemplateObjectFromManifestSplat } } } diff --git a/Plaster/Public/Invoke-Plaster.ps1 b/Plaster/Public/Invoke-Plaster.ps1 index fd1cba8..5463b7a 100644 --- a/Plaster/Public/Invoke-Plaster.ps1 +++ b/Plaster/Public/Invoke-Plaster.ps1 @@ -7,7 +7,7 @@ ## Plaster.psm1 ## 2. If a new manifest element is added, it must be added to the Schema\PlasterManifest-v1.xsd file and then ## processed in the appropriate function in this script. Any changes to attributes must be -## processed not only in the ProcessParameter function but also in the dynamicparam function. +## processed not only in the Resolve-ProcessParameter function but also in the dynamicparam function. ## ## 3. Non-exported functions should avoid using the PowerShell standard Verb-Noun naming convention. ## They should use PascalCase instead. @@ -15,13 +15,6 @@ ## 4. Please follow the scripting style of this file when adding new script. function Invoke-Plaster { - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidShouldContinueWithoutForce', '', Scope = 'Function', Target = 'CopyFileWithConflictDetection')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function', Target = 'ProcessParameter')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'CopyFileWithConflictDetection')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessFile')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessModifyFile')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessNewModuleManifest')] - [System.Diagnostics.CodeAnalysis.SuppressMessage('PSShouldProcess', '', Scope = 'Function', Target = 'ProcessRequireModule')] [CmdletBinding(SupportsShouldProcess = $true)] param( [Parameter(Position = 0, Mandatory = $true)] @@ -66,7 +59,7 @@ function Invoke-Plaster { $ErrorActionPreference = 'Stop' # The constrained runspace is not available in the dynamicparam block. Shouldn't be needed - # since we are only evaluating the parameters in the manifest - no need for EvaluateConditionAttribute as we + # since we are only evaluating the parameters in the manifest - no need for Test-ConditionAttribute as we # are not building up multiple parametersets. And no need for EvaluateAttributeValue since we are only # grabbing the parameter's value which is static. $templateAbsolutePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($TemplatePath) @@ -155,15 +148,18 @@ function Invoke-Plaster { Write-Host ("=" * 50) } - $boundParameters = $PSBoundParameters - $constrainedRunspace = $null - $templateCreatedFiles = @{} - $defaultValueStore = @{} - $fileConflictConfirmNoToAll = $false - $fileConflictConfirmYesToAll = $false - $flags = @{ + #region Script Scope Variables + # These are used across different private functions. + $script:boundParameters = $PSBoundParameters + $script:constrainedRunspace = $null + $script:templateCreatedFiles = @{} + $script:defaultValueStore = @{} + $script:fileConflictConfirmNoToAll = $false + $script:fileConflictConfirmYesToAll = $false + $script:flags = @{ DefaultValueStoreDirty = $false } + #endregion Script Scope Variables # Verify TemplatePath parameter value is valid. $templateAbsolutePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($TemplatePath) @@ -214,1179 +210,13 @@ function Invoke-Plaster { $templateVersion = $manifest.plasterManifest.metadata.version $templateName = $manifest.plasterManifest.metadata.name $storeFilename = "$templateName-$templateVersion-$templateId.clixml" - $defaultValueStorePath = Join-Path $ParameterDefaultValueStoreRootPath $storeFilename - if (Test-Path $defaultValueStorePath) { + $script:defaultValueStorePath = Join-Path $ParameterDefaultValueStoreRootPath $storeFilename + if (Test-Path $script:defaultValueStorePath) { try { - $PSCmdlet.WriteDebug("Loading default value store from '$defaultValueStorePath'.") - $defaultValueStore = Import-Clixml $defaultValueStorePath -ErrorAction Stop + $PSCmdlet.WriteDebug("Loading default value store from '$script:defaultValueStorePath'.") + $script:defaultValueStore = Import-Clixml $script:defaultValueStorePath -ErrorAction Stop } catch { - Write-Warning ($LocalizedData.ErrorFailedToLoadStoreFile_F1 -f $defaultValueStorePath) - } - } - - function NewConstrainedRunspace() { - $iss = [System.Management.Automation.Runspaces.InitialSessionState]::Create() - if (!$IsCoreCLR) { - $iss.ApartmentState = [System.Threading.ApartmentState]::STA - } - $iss.LanguageMode = [System.Management.Automation.PSLanguageMode]::ConstrainedLanguage - $iss.DisableFormatUpdates = $true - - $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'Environment', ([Microsoft.PowerShell.Commands.EnvironmentProvider]), $null - $iss.Providers.Add($sspe) - - $sspe = New-Object System.Management.Automation.Runspaces.SessionStateProviderEntry 'FileSystem', ([Microsoft.PowerShell.Commands.FileSystemProvider]), $null - $iss.Providers.Add($sspe) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Content', ([Microsoft.PowerShell.Commands.GetContentCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Date', ([Microsoft.PowerShell.Commands.GetDateCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ChildItem', ([Microsoft.PowerShell.Commands.GetChildItemCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Item', ([Microsoft.PowerShell.Commands.GetItemCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-ItemProperty', ([Microsoft.PowerShell.Commands.GetItemPropertyCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Module', ([Microsoft.PowerShell.Commands.GetModuleCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Get-Variable', ([Microsoft.PowerShell.Commands.GetVariableCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Test-Path', ([Microsoft.PowerShell.Commands.TestPathCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Out-String', ([Microsoft.PowerShell.Commands.OutStringCommand]), $null - $iss.Commands.Add($ssce) - - $ssce = New-Object System.Management.Automation.Runspaces.SessionStateCmdletEntry 'Compare-Object', ([Microsoft.PowerShell.Commands.CompareObjectCommand]), $null - $iss.Commands.Add($ssce) - - $scopedItemOptions = [System.Management.Automation.ScopedItemOptions]::AllScope - $plasterVars = Get-Variable -Name PLASTER_*, PSVersionTable - if (Test-Path Variable:\IsLinux) { - $plasterVars += Get-Variable -Name IsLinux - } - if (Test-Path Variable:\IsOSX) { - $plasterVars += Get-Variable -Name IsOSX - } - if (Test-Path Variable:\IsMacOS) { - $plasterVars += Get-Variable -Name IsMacOS - } - if (Test-Path Variable:\IsWindows) { - $plasterVars += Get-Variable -Name IsWindows - } - foreach ($var in $plasterVars) { - $ssve = New-Object System.Management.Automation.Runspaces.SessionStateVariableEntry ` - $var.Name, $var.Value, $var.Description, $scopedItemOptions - $iss.Variables.Add($ssve) - } - - # Create new runspace with the above defined entries. Then open and set its working dir to $destinationAbsolutePath - # so all condition attribute expressions can use a relative path to refer to file paths e.g. - # condition="Test-Path src\${PLASTER_PARAM_ModuleName}.psm1" - $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($iss) - $runspace.Open() - if ($destinationAbsolutePath) { - $runspace.SessionStateProxy.Path.SetLocation($destinationAbsolutePath) > $null - } - $runspace - } - - function ExecuteExpressionImpl([string]$Expression) { - try { - $powershell = [PowerShell]::Create() - - if ($null -eq $constrainedRunspace) { - $constrainedRunspace = NewConstrainedRunspace - } - $powershell.Runspace = $constrainedRunspace - - try { - $powershell.AddScript($Expression) > $null - $res = $powershell.Invoke() - $res - } catch { - throw ($LocalizedData.ExpressionInvalid_F2 -f $Expression, $_) - } - - # Check for non-terminating errors. - if ($powershell.Streams.Error.Count -gt 0) { - $err = $powershell.Streams.Error[0] - throw ($LocalizedData.ExpressionNonTermErrors_F2 -f $Expression, $err) - } - } finally { - if ($powershell) { - $powershell.Dispose() - } - } - } - - function InterpolateAttributeValue([string]$Value, [string]$Location) { - if ($null -eq $Value) { - return [string]::Empty - } elseif ([string]::IsNullOrWhiteSpace($Value)) { - return $Value - } - - try { - $res = @(ExecuteExpressionImpl "`"$Value`"") - [string]$res[0] - } catch { - throw ($LocalizedData.InterpolationError_F3 -f $Value.Trim(), $Location, $_) - } - } - - function EvaluateConditionAttribute([string]$Expression, [string]$Location) { - if ($null -eq $Expression) { - return [string]::Empty - } elseif ([string]::IsNullOrWhiteSpace($Expression)) { - return $Expression - } - - try { - $res = @(ExecuteExpressionImpl $Expression) - [bool]$res[0] - } catch { - throw ($LocalizedData.ExpressionInvalidCondition_F3 -f $Expression, $Location, $_) - } - } - - function EvaluateExpression([string]$Expression, [string]$Location) { - if ($null -eq $Expression) { - return [string]::Empty - } elseif ([string]::IsNullOrWhiteSpace($Expression)) { - return $Expression - } - - try { - $res = @(ExecuteExpressionImpl $Expression) - [string]$res[0] - } catch { - throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) - } - } - - function EvaluateScript([string]$Script, [string]$Location) { - if ($null -eq $Script) { - return @([string]::Empty) - } elseif ([string]::IsNullOrWhiteSpace($Script)) { - return $Script - } - - try { - $res = @(ExecuteExpressionImpl $Script) - [string[]]$res - } catch { - throw ($LocalizedData.ExpressionExecError_F2 -f $Location, $_) - } - } - - function GetErrorLocationFileAttrVal([string]$ElementName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationFile_F2 -f $ElementName, $AttributeName - } - - function GetErrorLocationModifyAttrVal([string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationModify_F1 -f $AttributeName - } - - function GetErrorLocationNewModManifestAttrVal([string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationNewModManifest_F1 -f $AttributeName - } - - function GetErrorLocationParameterAttrVal([string]$ParameterName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationParameter_F2 -f $ParameterName, $AttributeName - } - - function GetErrorLocationRequireModuleAttrVal([string]$ModuleName, [string]$AttributeName) { - $LocalizedData.ExpressionErrorLocationRequireModule_F2 -f $ModuleName, $AttributeName - } - - function GetPSSnippetFunction([String]$FilePath) { - # Test if Path Exists - if (!(Test-Path $substitute -PathType Leaf)) { - throw ($LocalizedData.ErrorPathDoesNotExist_F1 -f $FilePath) - } - # Load File - return Get-Content -LiteralPath $substitute -Raw - } - - function ConvertToDestinationRelativePath($Path) { - $fullDestPath = $DestinationPath - if (![System.IO.Path]::IsPathRooted($fullDestPath)) { - $fullDestPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) - } - - $fullPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path) - if (!$fullPath.StartsWith($fullDestPath, 'OrdinalIgnoreCase')) { - throw ($LocalizedData.ErrorPathMustBeUnderDestPath_F2 -f $fullPath, $fullDestPath) - } - - $fullPath.Substring($fullDestPath.Length).TrimStart('\', '/') - } - - function VerifyPathIsUnderDestinationPath([ValidateNotNullOrEmpty()][string]$FullPath) { - if (![System.IO.Path]::IsPathRooted($FullPath)) { - $PSCmdlet.WriteDebug("The FullPath parameter '$FullPath' must be an absolute path.") - } - - $fullDestPath = $DestinationPath - if (![System.IO.Path]::IsPathRooted($fullDestPath)) { - $fullDestPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DestinationPath) - } - - if (!$FullPath.StartsWith($fullDestPath, [StringComparison]::OrdinalIgnoreCase)) { - throw ($LocalizedData.ErrorPathMustBeUnderDestPath_F2 -f $FullPath, $fullDestPath) - } - } - - function WriteContentWithEncoding([string]$path, [string[]]$content, [string]$encoding) { - if ($encoding -match '-nobom') { - $encoding, $dummy = $encoding -split '-' - - $noBomEncoding = $null - switch ($encoding) { - 'utf8' { $noBomEncoding = New-Object System.Text.UTF8Encoding($false) } - } - - if ($null -eq $content) { - $content = [string]::Empty - } - - [System.IO.File]::WriteAllLines($path, $content, $noBomEncoding) - } else { - Set-Content -LiteralPath $path -Value $content -Encoding $encoding - } - } - - function ColorForOperation($operation) { - switch ($operation) { - $LocalizedData.OpConflict { 'Red' } - $LocalizedData.OpCreate { 'Green' } - $LocalizedData.OpForce { 'Yellow' } - $LocalizedData.OpIdentical { 'Cyan' } - $LocalizedData.OpModify { 'Magenta' } - $LocalizedData.OpUpdate { 'Green' } - $LocalizedData.OpMissing { 'Red' } - $LocalizedData.OpVerify { 'Green' } - default { $Host.UI.RawUI.ForegroundColor } - } - } - - function GetMaxOperationLabelLength { - ($LocalizedData.OpCreate, $LocalizedData.OpIdentical, - $LocalizedData.OpConflict, $LocalizedData.OpForce, - $LocalizedData.OpMissing, $LocalizedData.OpModify, - $LocalizedData.OpUpdate, $LocalizedData.OpVerify | - Measure-Object -Property Length -Maximum).Maximum - } - - function WriteOperationStatus($operation, $message) { - $maxLen = GetMaxOperationLabelLength - Write-Host ("{0,$maxLen} " -f $operation) -ForegroundColor (ColorForOperation $operation) -NoNewline - Write-Host $message - } - - function WriteOperationAdditionalStatus([string[]]$Message) { - $maxLen = GetMaxOperationLabelLength - foreach ($msg in $Message) { - $lines = $msg -split "`n" - foreach ($line in $lines) { - Write-Host ("{0,$maxLen} {1}" -f "", $line) - } - } - } - - function GetGitConfigValue($name) { - # Very simplistic git config lookup - # Won't work with namespace, just use final element, e.g. 'name' instead of 'user.name' - - # The $Home dir may not be reachable e.g. if on network share and/or script not running as admin. - # See issue https://github.com/PowerShell/Plaster/issues/92 - if (!(Test-Path -LiteralPath $Home)) { - return - } - - $gitConfigPath = Join-Path $Home '.gitconfig' - $PSCmdlet.WriteDebug("Looking for '$name' value in Git config: $gitConfigPath") - - if (Test-Path -LiteralPath $gitConfigPath) { - $matches = Select-String -LiteralPath $gitConfigPath -Pattern "\s+$name\s+=\s+(.+)$" - if (@($matches).Count -gt 0) { - $matches.Matches.Groups[1].Value - } - } - } - - function PromptForInput($prompt, $default, $pattern) { - if (!$pattern) { - $patternMatch = $true - } - - do { - $value = Read-Host -Prompt $prompt - if (!$value -and $default) { - $value = $default - $patternMatch = $true - } elseif ($value -and $pattern) { - if ($value -match $pattern) { - $patternMatch = $true - } else { - $PSCmdlet.WriteDebug("Value '$value' did not match the pattern '$pattern'") - } - } - } while (!$value -or !$patternMatch) - - $value - } - - function PromptForChoice([string]$ParameterName, [ValidateNotNull()]$ChoiceNodes, [string]$prompt, - [int[]]$defaults, [switch]$IsMultiChoice) { - $choices = New-Object 'System.Collections.ObjectModel.Collection[System.Management.Automation.Host.ChoiceDescription]' - $values = New-Object object[] $ChoiceNodes.Count - $i = 0 - - foreach ($choiceNode in $ChoiceNodes) { - $label = InterpolateAttributeValue $choiceNode.label (GetErrorLocationParameterAttrVal $ParameterName label) - $help = InterpolateAttributeValue $choiceNode.help (GetErrorLocationParameterAttrVal $ParameterName help) - $value = InterpolateAttributeValue $choiceNode.value (GetErrorLocationParameterAttrVal $ParameterName value) - - $choice = New-Object System.Management.Automation.Host.ChoiceDescription -Arg $label, $help - $choices.Add($choice) - $values[$i++] = $value - } - - $retval = [PSCustomObject]@{Values = @(); Indices = @() } - - if ($IsMultiChoice) { - $selections = $Host.UI.PromptForChoice('', $prompt, $choices, $defaults) - foreach ($selection in $selections) { - $retval.Values += $values[$selection] - $retval.Indices += $selection - } - } else { - if ($defaults.Count -gt 1) { - throw ($LocalizedData.ParameterTypeChoiceMultipleDefault_F1 -f $ChoiceNodes.ParentNode.name) - } - - $selection = $Host.UI.PromptForChoice('', $prompt, $choices, $defaults[0]) - $retval.Values = $values[$selection] - $retval.Indices = $selection - } - - $retval - } - - # All Plaster variables should be set via this method so that the ConstrainedRunspace can be - # configured to use the new variable. This method will null out the ConstrainedRunspace so that - # later, when we need to evaluate script in that runspace, it will get recreated first with all - # the latest Plaster variables. - function SetPlasterVariable() { - param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - - [Parameter(Mandatory = $true)] - [AllowNull()] - $Value, - - [Parameter()] - [bool] - $IsParam = $true - ) - - # Variables created from a in the Plaster manifset are prefixed PLASTER_PARAM all others - # are just PLASTER_. - $variableName = if ($IsParam) { "PLASTER_PARAM_$Name" } else { "PLASTER_$Name" } - - Set-Variable -Name $variableName -Value $Value -Scope Script -WhatIf:$false - - # If the constrained runspace has been created, it needs to be disposed so that the next string - # expansion (or condition eval) gets an updated runspace that contains this variable or its new value. - if ($null -ne $script:ConstrainedRunspace) { - $script:ConstrainedRunspace.Dispose() - $script:ConstrainedRunspace = $null - } - } - - function ProcessParameter([ValidateNotNull()]$Node) { - $name = $Node.name - $type = $Node.type - $store = $Node.store - - $pattern = $Node.pattern - - $condition = $Node.condition - - $default = InterpolateAttributeValue $Node.default (GetErrorLocationParameterAttrVal $name default) - - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - if (-not [string]::IsNullOrEmpty($default) -and $type -eq 'text') { - SetPlasterVariable -Name $name -Value $default -IsParam $true - $PSCmdlet.WriteDebug("The condition of the parameter $($name) with the type 'text' evaluated to false. The parameter has a default value which will be used.") - } else { - # Define the parameter so later conditions can use it but its value will be $null - SetPlasterVariable -Name $name -Value $null -IsParam $true - $PSCmdlet.WriteDebug("Skipping parameter $($name), condition evaluated to false.") - } - - return - } - - $prompt = InterpolateAttributeValue $Node.prompt (GetErrorLocationParameterAttrVal $name prompt) - - # Check if parameter was provided via a dynamic parameter. - if ($boundParameters.ContainsKey($name)) { - $value = $boundParameters[$name] - } else { - # Not a dynamic parameter so prompt user for the value but first check for a stored default value. - if ($store -and ($null -ne $defaultValueStore[$name])) { - $default = $defaultValueStore[$name] - $PSCmdlet.WriteDebug("Read default value '$default' for parameter '$name' from default value store.") - - if (($store -eq 'encrypted') -and ($default -is [System.Security.SecureString])) { - try { - $cred = New-Object -TypeName PSCredential -ArgumentList 'jsbplh', $default - $default = $cred.GetNetworkCredential().Password - $PSCmdlet.WriteDebug("Unencrypted default value for parameter '$name'.") - } catch [System.Exception] { - Write-Warning ($LocalizedData.ErrorUnencryptingSecureString_F1 -f $name) - } - } - } - - # If the prompt message failed to evaluate or was empty, supply a diagnostic prompt message - if (!$prompt) { - $prompt = $LocalizedData.MissingParameterPrompt_F1 -f $name - } - - # Some default values might not come from the template e.g. some are harvested from .gitconfig if it exists. - $defaultNotFromTemplate = $false - - $splat = @{} - - if ($null -ne $pattern) { - $splat.Add('pattern', $pattern) - } - - # Now prompt user for parameter value based on the parameter type. - switch -regex ($type) { - 'text' { - # Display an appropriate "default" value in the prompt string. - if ($default) { - if ($store -eq 'encrypted') { - $obscuredDefault = $default -replace '(....).*', '$1****' - $prompt += " ($obscuredDefault)" - } else { - $prompt += " ($default)" - } - } - # Prompt the user for text input. - $value = PromptForInput $prompt $default @splat - $valueToStore = $value - } - 'user-fullname' { - # If no default, try to get a name from git config. - if (!$default) { - $default = GetGitConfigValue('name') - $defaultNotFromTemplate = $true - } - - if ($default) { - if ($store -eq 'encrypted') { - $obscuredDefault = $default -replace '(....).*', '$1****' - $prompt += " ($obscuredDefault)" - } else { - $prompt += " ($default)" - } - } - - # Prompt the user for text input. - $value = PromptForInput $prompt $default @splat - $valueToStore = $value - } - 'user-email' { - # If no default, try to get an email from git config - if (-not $default) { - $default = GetGitConfigValue('email') - $defaultNotFromTemplate = $true - } - - if ($default) { - if ($store -eq 'encrypted') { - $obscuredDefault = $default -replace '(....).*', '$1****' - $prompt += " ($obscuredDefault)" - } else { - $prompt += " ($default)" - } - } - - # Prompt the user for text input. - $value = PromptForInput $prompt $default @splat - $valueToStore = $value - } - 'choice|multichoice' { - $choices = $Node.ChildNodes - $defaults = [int[]]($default -split ',') - - # Prompt the user for choice or multichoice selection input. - $selections = PromptForChoice $name $choices $prompt $defaults -IsMultiChoice:($type -eq 'multichoice') - $value = $selections.Values - $OFS = "," - $valueToStore = "$($selections.Indices)" - } - default { throw ($LocalizedData.UnrecognizedParameterType_F2 -f $type, $Node.LocalName) } - } - - # If parameter specifies that user's input be stored as the default value, - # store it to file if the value has changed. - if ($store -and (($default -ne $valueToStore) -or $defaultNotFromTemplate)) { - if ($store -eq 'encrypted') { - $PSCmdlet.WriteDebug("Storing new, encrypted default value for parameter '$name' to default value store.") - $defaultValueStore[$name] = ConvertTo-SecureString -String $valueToStore -AsPlainText -Force - } else { - $PSCmdlet.WriteDebug("Storing new default value '$valueToStore' for parameter '$name' to default value store.") - $defaultValueStore[$name] = $valueToStore - } - - $flags.DefaultValueStoreDirty = $true - } - } - - # Make template defined parameters available as a PowerShell variable PLASTER_PARAM_. - SetPlasterVariable -Name $name -Value $value -IsParam $true - } - - function ProcessMessage([ValidateNotNull()]$Node) { - $text = InterpolateAttributeValue $Node.InnerText '' - $nonewline = $Node.nonewline -eq 'true' - - # Eliminate whitespace before and after the text that just happens to get inserted because you want - # the text on different lines than the start/end element tags. - $trimmedText = $text -replace '^[ \t]*\n', '' -replace '\n[ \t]*$', '' - - $condition = $Node.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $debugText = $trimmedText -replace '\r|\n', ' ' - $maxLength = [Math]::Min(40, $debugText.Length) - $PSCmdlet.WriteDebug("Skipping message '$($debugText.Substring(0, $maxLength))', condition evaluated to false.") - return - } - - Write-Host $trimmedText -NoNewline:($nonewline -eq 'true') - } - - function ProcessNewModuleManifest([ValidateNotNull()]$Node) { - $moduleVersion = InterpolateAttributeValue $Node.moduleVersion (GetErrorLocationNewModManifestAttrVal moduleVersion) - $rootModule = InterpolateAttributeValue $Node.rootModule (GetErrorLocationNewModManifestAttrVal rootModule) - $author = InterpolateAttributeValue $Node.author (GetErrorLocationNewModManifestAttrVal author) - $companyName = InterpolateAttributeValue $Node.companyName (GetErrorLocationNewModManifestAttrVal companyName) - $description = InterpolateAttributeValue $Node.description (GetErrorLocationNewModManifestAttrVal description) - $dstRelPath = InterpolateAttributeValue $Node.destination (GetErrorLocationNewModManifestAttrVal destination) - $powerShellVersion = InterpolateAttributeValue $Node.powerShellVersion (GetErrorLocationNewModManifestAttrVal powerShellVersion) - $nestedModules = InterpolateAttributeValue $Node.NestedModules (GetErrorLocationNewModManifestAttrVal NestedModules) - $dscResourcesToExport = InterpolateAttributeValue $Node.DscResourcesToExport (GetErrorLocationNewModManifestAttrVal DscResourcesToExport) - $copyright = InterpolateAttributeValue $Node.copyright (GetErrorLocationNewModManifestAttrVal copyright) - - # We could choose to not check this if the condition eval'd to false - # but I think it is better to let the template author know they've broken the - # rules for any of the file directives (not just the ones they're testing/enabled). - if ([System.IO.Path]::IsPathRooted($dstRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) - } - - $dstPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $dstRelPath)) - - $condition = $Node.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $PSCmdlet.WriteDebug("Skipping module manifest generation for '$dstPath', condition evaluated to false.") - return - } - - $encoding = $Node.encoding - if (!$encoding) { - $encoding = $DefaultEncoding - } - - if ($PSCmdlet.ShouldProcess($dstPath, $LocalizedData.ShouldProcessNewModuleManifest)) { - $manifestDir = Split-Path $dstPath -Parent - if (!(Test-Path $manifestDir)) { - VerifyPathIsUnderDestinationPath $manifestDir - Write-Verbose ($LocalizedData.NewModManifest_CreatingDir_F1 -f $manifestDir) - New-Item $manifestDir -ItemType Directory > $null - } - - $newModuleManifestParams = @{} - - # If there is an existing module manifest, load it so we can reuse old values not specified by - # template. - if (Test-Path -LiteralPath $dstPath) { - $manifestFileName = Split-Path $dstPath -Leaf - $newModuleManifestParams = Import-LocalizedData -BaseDirectory $manifestDir -FileName $manifestFileName - if ($newModuleManifestParams.PrivateData) { - $newModuleManifestParams += $newModuleManifestParams.PrivateData.psdata - $newModuleManifestParams.Remove('PrivateData') - } - } - - if (![string]::IsNullOrWhiteSpace($moduleVersion)) { - $newModuleManifestParams['ModuleVersion'] = $moduleVersion - } - if (![string]::IsNullOrWhiteSpace($rootModule)) { - $newModuleManifestParams['RootModule'] = $rootModule - } - if (![string]::IsNullOrWhiteSpace($author)) { - $newModuleManifestParams['Author'] = $author - } - if (![string]::IsNullOrWhiteSpace($companyName)) { - $newModuleManifestParams['CompanyName'] = $companyName - } - if (![string]::IsNullOrWhiteSpace($copyright)) { - $newModuleManifestParams['Copyright'] = $copyright - } - if (![string]::IsNullOrWhiteSpace($description)) { - $newModuleManifestParams['Description'] = $description - } - if (![string]::IsNullOrWhiteSpace($powerShellVersion)) { - $newModuleManifestParams['PowerShellVersion'] = $powerShellVersion - } - if (![string]::IsNullOrWhiteSpace($nestedModules)) { - $newModuleManifestParams['NestedModules'] = $nestedModules - } - if (![string]::IsNullOrWhiteSpace($dscResourcesToExport)) { - $newModuleManifestParams['DscResourcesToExport'] = $dscResourcesToExport - } - - $tempFile = $null - - try { - $tempFileBaseName = "moduleManifest-" + [Guid]::NewGuid() - $tempFile = [System.IO.Path]::GetTempPath() + "${tempFileBaseName}.psd1" - $PSCmdlet.WriteDebug("Created temp file for new module manifest - $tempFile") - $newModuleManifestParams['Path'] = $tempFile - - # Generate manifest into a temp file. - New-ModuleManifest @newModuleManifestParams - - # Typically the manifest is re-written with a new encoding (UTF8-NoBOM) because Git hates UTF-16. - $content = Get-Content -LiteralPath $tempFile -Raw - - # Replace the temp filename in the generated manifest file's comment header with the actual filename. - $dstBaseName = [System.IO.Path]::GetFileNameWithoutExtension($dstPath) - $content = $content -replace "(?<=\s*#.*?)$tempFileBaseName", $dstBaseName - - WriteContentWithEncoding -Path $tempFile -Content $content -Encoding $encoding - - CopyFileWithConflictDetection $tempFile $dstPath - - if ($PassThru -and ($Node.openInEditor -eq 'true')) { - $InvokePlasterInfo.OpenFiles += $dstPath - } - } finally { - if ($tempFile -and (Test-Path $tempFile)) { - Remove-Item -LiteralPath $tempFile - $PSCmdlet.WriteDebug("Removed temp file for new module manifest - $tempFile") - } - } - } - } - - # - # Begin ProcessFile helper methods - # - function NewBackupFilename([string]$Path) { - $dir = [System.IO.Path]::GetDirectoryName($Path) - $filename = [System.IO.Path]::GetFileName($Path) - $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak" - $i = 1 - while (Test-Path -LiteralPath $backupPath) { - $backupPath = Join-Path -Path $dir -ChildPath "${filename}.bak$i" - $i++ - } - - $backupPath - } - - function AreFilesIdentical($Path1, $Path2) { - $file1 = Get-Item -LiteralPath $Path1 -Force - $file2 = Get-Item -LiteralPath $Path2 -Force - - if ($file1.Length -ne $file2.Length) { - return $false - } - - $hash1 = (Get-FileHash -LiteralPath $path1 -Algorithm SHA1).Hash - $hash2 = (Get-FileHash -LiteralPath $path2 -Algorithm SHA1).Hash - - $hash1 -eq $hash2 - } - - function NewFileSystemCopyInfo([string]$srcPath, [string]$dstPath) { - [PSCustomObject]@{SrcFileName = $srcPath; DstFileName = $dstPath } - } - - function ExpandFileSourceSpec([string]$srcRelPath, [string]$dstRelPath) { - $srcPath = Join-Path $templateAbsolutePath $srcRelPath - $dstPath = Join-Path $destinationAbsolutePath $dstRelPath - - if ($srcRelPath.IndexOfAny([char[]]('*', '?')) -lt 0) { - # No wildcard spec in srcRelPath so return info on single file. - # Also, if dstRelPath is empty, then use source rel path. - if (!$dstRelPath) { - $dstPath = Join-Path $destinationAbsolutePath $srcRelPath - } - - return NewFileSystemCopyInfo $srcPath $dstPath - } - - # Prepare parameter values for call to Get-ChildItem to get list of files based on wildcard spec. - $gciParams = @{} - $parent = Split-Path $srcPath -Parent - $leaf = Split-Path $srcPath -Leaf - $gciParams['LiteralPath'] = $parent - $gciParams['File'] = $true - - if ($leaf -eq '**') { - $gciParams['Recurse'] = $true - } else { - if ($leaf.IndexOfAny([char[]]('*', '?')) -ge 0) { - $gciParams['Filter'] = $leaf - } - - $leaf = Split-Path $parent -Leaf - if ($leaf -eq '**') { - $parent = Split-Path $parent -Parent - $gciParams['LiteralPath'] = $parent - $gciParams['Recurse'] = $true - } - } - - $srcRelRootPathLength = $gciParams['LiteralPath'].Length - - # Generate a FileCopyInfo object for every file expanded by the wildcard spec. - $files = @(Microsoft.PowerShell.Management\Get-ChildItem @gciParams) - foreach ($file in $files) { - $fileSrcPath = $file.FullName - $relPath = $fileSrcPath.Substring($srcRelRootPathLength) - $fileDstPath = Join-Path $dstPath $relPath - NewFileSystemCopyInfo $fileSrcPath $fileDstPath - } - - # Copy over empty directories - if any. - $gciParams.Remove('File') - $gciParams['Directory'] = $true - $dirs = @(Microsoft.PowerShell.Management\Get-ChildItem @gciParams | - Where-Object { $_.GetFileSystemInfos().Length -eq 0 }) - foreach ($dir in $dirs) { - $dirSrcPath = $dir.FullName - $relPath = $dirSrcPath.Substring($srcRelRootPathLength) - $dirDstPath = Join-Path $dstPath $relPath - NewFileSystemCopyInfo $dirSrcPath $dirDstPath - } - } - - # Plaster zen for file handling. All file related operations should use this method - # to actually write/overwrite/modify files in the DestinationPath. This method - # handles detecting conflicts, gives the user a chance to determine how to handle - # conflicts. The user can choose to use the Force parameter to force the overwriting - # of existing files at the destination path. - # File processing (expanding substitution variable, modifying file contents) should always - # be done to a temp file (be sure to always remove temp file when done). That temp file - # is what gets passed to this function as the $SrcPath. This allows Plaster to alert the - # user when the repeated application of a template will modify any existing file. - # NOTE: Plaster keeps track of which files it has "created" (as opposed to overwritten) - # so that any later change to that file doesn't trigger conflict handling. - function CopyFileWithConflictDetection([string]$SrcPath, [string]$DstPath) { - # Just double-checking that DstPath parameter is an absolute path otherwise - # it could fail the check that the DstPath is under the overall DestinationPath. - if (![System.IO.Path]::IsPathRooted($DstPath)) { - $DstPath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($DstPath) - } - - # Check if DstPath file conflicts with an existing SrcPath file. - $operation = $LocalizedData.OpCreate - $opmessage = (ConvertToDestinationRelativePath $DstPath) - if (Test-Path -LiteralPath $DstPath) { - if (AreFilesIdentical $SrcPath $DstPath) { - $operation = $LocalizedData.OpIdentical - } elseif ($templateCreatedFiles.ContainsKey($DstPath)) { - # Plaster created this file previously during template invocation - # therefore, there is no conflict. We're simply updating the file. - $operation = $LocalizedData.OpUpdate - } elseif ($Force) { - $operation = $LocalizedData.OpForce - } else { - $operation = $LocalizedData.OpConflict - } - } - - # Copy the file to the destination - if ($PSCmdlet.ShouldProcess($DstPath, $operation)) { - WriteOperationStatus $operation $opmessage - - if ($operation -eq $LocalizedData.OpIdentical) { - # If the files are identical, no need to do anything - return - } - - if (($operation -eq $LocalizedData.OpCreate) -or ($operation -eq $LocalizedData.OpUpdate)) { - Copy-Item -LiteralPath $SrcPath -Destination $DstPath - if ($PassThru) { - $InvokePlasterInfo.CreatedFiles += $DstPath - } - $templateCreatedFiles[$DstPath] = $null - } elseif ($Force -or $PSCmdlet.ShouldContinue(($LocalizedData.OverwriteFile_F1 -f $DstPath), - $LocalizedData.FileConflict, - [ref]$fileConflictConfirmYesToAll, - [ref]$fileConflictConfirmNoToAll)) { - $backupFilename = NewBackupFilename $DstPath - Copy-Item -LiteralPath $DstPath -Destination $backupFilename - Copy-Item -LiteralPath $SrcPath -Destination $DstPath - if ($PassThru) { - $InvokePlasterInfo.UpdatedFiles += $DstPath - } - $templateCreatedFiles[$DstPath] = $null - } - } - } - - # - # End ProcessFile helper methods - # - - # Processes both the and directives. - function ProcessFile([ValidateNotNull()]$Node) { - $srcRelPath = InterpolateAttributeValue $Node.source (GetErrorLocationFileAttrVal $Node.localName source) - $dstRelPath = InterpolateAttributeValue $Node.destination (GetErrorLocationFileAttrVal $Node.localName destination) - - $condition = $Node.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $PSCmdlet.WriteDebug("Skipping $($Node.localName) '$srcRelPath' -> '$dstRelPath', condition evaluated to false.") - return - } - - # Only validate paths for conditions that evaluate to true. - # The path may not be valid if it evaluates to false depending - # on whether or not conditional parameters are used in the template. - if ([System.IO.Path]::IsPathRooted($srcRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $srcRelPath, $Node.LocalName) - } - - if ([System.IO.Path]::IsPathRooted($dstRelPath)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $dstRelPath, $Node.LocalName) - } - - # Check if node is the specialized, node. - # Only nodes expand templates and use the encoding attribute. - $isTemplateFile = $Node.localName -eq 'templateFile' - if ($isTemplateFile) { - $encoding = $Node.encoding - if (!$encoding) { - $encoding = $DefaultEncoding - } - } - - # Check if source specifies a wildcard and if so, expand the wildcard - # and then process each file system object (file or empty directory). - $fileSystemCopyInfoObjs = ExpandFileSourceSpec $srcRelPath $dstRelPath - foreach ($fileSystemCopyInfo in $fileSystemCopyInfoObjs) { - $srcPath = $fileSystemCopyInfo.SrcFileName - $dstPath = $fileSystemCopyInfo.DstFileName - - # The file's destination path must be under the DestinationPath specified by the user. - VerifyPathIsUnderDestinationPath $dstPath - - # Check to see if we're copying an empty dir - if (Test-Path -LiteralPath $srcPath -PathType Container) { - if (!(Test-Path -LiteralPath $dstPath)) { - if ($PSCmdlet.ShouldProcess($parentDir, $LocalizedData.ShouldProcessCreateDir)) { - WriteOperationStatus $LocalizedData.OpCreate ` - ($dstRelPath.TrimEnd(([char]'\'), ([char]'/')) + [System.IO.Path]::DirectorySeparatorChar) - New-Item -Path $dstPath -ItemType Directory > $null - } - } - - continue - } - - # If the file's parent dir doesn't exist, create it. - $parentDir = Split-Path $dstPath -Parent - if (!(Test-Path -LiteralPath $parentDir)) { - if ($PSCmdlet.ShouldProcess($parentDir, $LocalizedData.ShouldProcessCreateDir)) { - New-Item -Path $parentDir -ItemType Directory > $null - } - } - - $tempFile = $null - - try { - # If processing a , copy to a temp file to expand the template file, - # then apply the normal file conflict detection/resolution handling. - $target = $LocalizedData.TempFileTarget_F1 -f (ConvertToDestinationRelativePath $dstPath) - if ($isTemplateFile -and $PSCmdlet.ShouldProcess($target, $LocalizedData.ShouldProcessExpandTemplate)) { - $content = Get-Content -LiteralPath $srcPath -Raw - - # Eval script expression delimiters - if ($content -and ($content.Count -gt 0)) { - $newContent = [regex]::Replace($content, '(<%=)(.*?)(%>)', { - param($match) - $expr = $match.groups[2].value - $res = EvaluateExpression $expr "templateFile '$srcRelPath'" - $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") - $res - }, @('IgnoreCase')) - - # Eval script block delimiters - $newContent = [regex]::Replace($newContent, '(^<%)(.*?)(^%>)', { - param($match) - $expr = $match.groups[2].value - $res = EvaluateScript $expr "templateFile '$srcRelPath'" - $res = $res -join [System.Environment]::NewLine - $PSCmdlet.WriteDebug("Replacing '$expr' with '$res' in contents of template file '$srcPath'") - $res - }, @('IgnoreCase', 'SingleLine', 'MultiLine')) - - $srcPath = $tempFile = [System.IO.Path]::GetTempFileName() - $PSCmdlet.WriteDebug("Created temp file for expanded templateFile - $tempFile") - - WriteContentWithEncoding -Path $tempFile -Content $newContent -Encoding $encoding - } else { - $PSCmdlet.WriteDebug("Skipping template file expansion for $($Node.localName) '$srcPath', file is empty.") - } - } - - CopyFileWithConflictDetection $srcPath $dstPath - - if ($PassThru -and ($Node.openInEditor -eq 'true')) { - $InvokePlasterInfo.OpenFiles += $dstPath - } - } finally { - if ($tempFile -and (Test-Path $tempFile)) { - Remove-Item -LiteralPath $tempFile - $PSCmdlet.WriteDebug("Removed temp file for expanded templateFile - $tempFile") - } - } - } - } - - function ProcessModifyFile([ValidateNotNull()]$Node) { - $path = InterpolateAttributeValue $Node.path (GetErrorLocationModifyAttrVal path) - - # We could choose to not check this if the condition eval'd to false - # but I think it is better to let the template author know they've broken the - # rules for any of the file directives (not just the ones they're testing/enabled). - if ([System.IO.Path]::IsPathRooted($path)) { - throw ($LocalizedData.ErrorPathMustBeRelativePath_F2 -f $path, $Node.LocalName) - } - - $filePath = $PSCmdlet.GetUnresolvedProviderPathFromPSPath((Join-Path $DestinationPath $path)) - - # The file's path must be under the DestinationPath specified by the user. - VerifyPathIsUnderDestinationPath $filePath - - $condition = $Node.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $PSCmdlet.WriteDebug("Skipping $($Node.LocalName) of '$filePath', condition evaluated to false.") - return - } - - $fileContent = [string]::Empty - if (Test-Path -LiteralPath $filePath) { - $fileContent = Get-Content -LiteralPath $filePath -Raw - } - - # Set a Plaster (non-parameter) variable in this and the constrained runspace. - SetPlasterVariable -Name FileContent -Value $fileContent -IsParam $false - - $encoding = $Node.encoding - if (!$encoding) { - $encoding = $DefaultEncoding - } - - # If processing a directive, write the modified contents to a temp file, - # then apply the normal file conflict detection/resolution handling. - $target = $LocalizedData.TempFileTarget_F1 -f $filePath - if ($PSCmdlet.ShouldProcess($target, $LocalizedData.OpModify)) { - WriteOperationStatus $LocalizedData.OpModify ($LocalizedData.TempFileOperation_F1 -f (ConvertToDestinationRelativePath $filePath)) - - $modified = $false - - foreach ($childNode in $Node.ChildNodes) { - if ($childNode -isnot [System.Xml.XmlElement]) { continue } - - switch ($childNode.LocalName) { - 'replace' { - $condition = $childNode.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)><$($childNode.LocalName)>'")) { - $PSCmdlet.WriteDebug("Skipping $($Node.LocalName) $($childNode.LocalName) of '$filePath', condition evaluated to false.") - continue - } - - if ($childNode.original -is [string]) { - $original = $childNode.original - } else { - $original = $childNode.original.InnerText - } - - if ($childNode.original.expand -eq 'true') { - $original = InterpolateAttributeValue $original (GetErrorLocationModifyAttrVal original) - } - - if ($childNode.substitute -is [string]) { - $substitute = $childNode.substitute - } else { - $substitute = $childNode.substitute.InnerText - } - - if ($childNode.substitute.isFile -eq 'true') { - $substitute = GetPSSnippetFunction $substitute - } elseif ($childNode.substitute.expand -eq 'true') { - $substitute = InterpolateAttributeValue $substitute (GetErrorLocationModifyAttrVal substitute) - } - - # Perform Literal Replacement on FileContent (since it will have regex characters) - if ($childNode.substitute.isFile) { - $fileContent = $fileContent.Replace($original, $substitute) - } else { - $fileContent = $fileContent -replace $original, $substitute - } - - # Update the Plaster (non-parameter) variable's value in this and the constrained runspace. - SetPlasterVariable -Name FileContent -Value $fileContent -IsParam $false - - $modified = $true - } - default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $childNode.LocalName) } - } - } - - $tempFile = $null - - try { - # We could use CopyFileWithConflictDetection to handle the "identical" (not modified) case - # but if nothing was changed, I'd prefer not to generate a temp file, copy the unmodified contents - # into that temp file with hopefully the right encoding and then potentially overwrite the original file - # (different encoding will make the files look different) with the same contents but different encoding. - # If the intent of the was simply to change an existing file's encoding then the directive will - # need to make a whitespace change to the file. - if ($modified) { - $tempFile = [System.IO.Path]::GetTempFileName() - $PSCmdlet.WriteDebug("Created temp file for modified file - $tempFile") - - WriteContentWithEncoding -Path $tempFile -Content $PLASTER_FileContent -Encoding $encoding - CopyFileWithConflictDetection $tempFile $filePath - - if ($PassThru -and ($Node.openInEditor -eq 'true')) { - $InvokePlasterInfo.OpenFiles += $filePath - } - } else { - WriteOperationStatus $LocalizedData.OpIdentical (ConvertToDestinationRelativePath $filePath) - } - } finally { - if ($tempFile -and (Test-Path $tempFile)) { - Remove-Item -LiteralPath $tempFile - $PSCmdlet.WriteDebug("Removed temp file for modified file - $tempFile") - } - } - } - } - - function ProcessRequireModule([ValidateNotNull()]$Node) { - $name = $Node.name - - $condition = $Node.condition - if ($condition -and !(EvaluateConditionAttribute $condition "'<$($Node.LocalName)>'")) { - $PSCmdlet.WriteDebug("Skipping $($Node.localName) for module '$name', condition evaluated to false.") - return - } - - $message = InterpolateAttributeValue $Node.message (GetErrorLocationRequireModuleAttrVal $name message) - $minimumVersion = $Node.minimumVersion - $maximumVersion = $Node.maximumVersion - $requiredVersion = $Node.requiredVersion - - $getModuleParams = @{ - ListAvailable = $true - ErrorAction = 'SilentlyContinue' - } - - # Configure $getModuleParams with correct parameters based on parameterset to be used. - # Also construct an array of version strings that can be displayed to the user. - $versionInfo = @() - if ($requiredVersion) { - $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name; RequiredVersion = $requiredVersion } - $versionInfo += $LocalizedData.RequireModuleRequiredVersion_F1 -f $requiredVersion - } elseif ($minimumVersion -or $maximumVersion) { - $getModuleParams["FullyQualifiedName"] = @{ModuleName = $name } - - if ($minimumVersion) { - $getModuleParams.FullyQualifiedName["ModuleVersion"] = $minimumVersion - $versionInfo += $LocalizedData.RequireModuleMinVersion_F1 -f $minimumVersion - } - if ($maximumVersion) { - $getModuleParams.FullyQualifiedName["MaximumVersion"] = $maximumVersion - $versionInfo += $LocalizedData.RequireModuleMaxVersion_F1 -f $maximumVersion - } - } else { - $getModuleParams["Name"] = $name - } - - # Flatten array of version strings into a single string. - $versionRequirements = "" - if ($versionInfo.Length -gt 0) { - $OFS = ", " - $versionRequirements = " ($versionInfo)" - } - - # PowerShell v3 Get-Module command does not have the FullyQualifiedName parameter. - if ($PSVersionTable.PSVersion.Major -lt 4) { - $getModuleParams.Remove("FullyQualifiedName") - $getModuleParams["Name"] = $name - } - - $module = Get-Module @getModuleParams - - $moduleDesc = if ($versionRequirements) { "${name}:$versionRequirements" } else { $name } - - if ($null -eq $module) { - WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) - if ($message) { - WriteOperationAdditionalStatus $message - } - if ($PassThru) { - $InvokePlasterInfo.MissingModules += $moduleDesc - } - } else { - if ($PSVersionTable.PSVersion.Major -gt 3) { - WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) - } else { - # On V3, we have to the version matching with the results that Get-Module return. - $installedVersion = $module | Sort-Object Version -Descending | Select-Object -First 1 | ForEach-Object Version - if ($installedVersion.Build -eq -1) { - $installedVersion = [System.Version]"${installedVersion}.0.0" - } elseif ($installedVersion.Revision -eq -1) { - $installedVersion = [System.Version]"${installedVersion}.0" - } - - if (($requiredVersion -and ($installedVersion -ne $requiredVersion)) -or - ($minimumVersion -and ($installedVersion -lt $minimumVersion)) -or - ($maximumVersion -and ($installedVersion -gt $maximumVersion))) { - - WriteOperationStatus $LocalizedData.OpMissing ($LocalizedData.RequireModuleMissing_F2 -f $name, $versionRequirements) - if ($PassThru) { - $InvokePlasterInfo.MissingModules += $moduleDesc - } - } else { - WriteOperationStatus $LocalizedData.OpVerify ($LocalizedData.RequireModuleVerified_F2 -f $name, $versionRequirements) - } - } + Write-Warning ($LocalizedData.ErrorFailedToLoadStoreFile_F1 -f $script:defaultValueStorePath) } } } @@ -1397,7 +227,7 @@ function Invoke-Plaster { foreach ($node in $manifest.plasterManifest.parameters.ChildNodes) { if ($node -isnot [System.Xml.XmlElement]) { continue } switch ($node.LocalName) { - 'parameter' { ProcessParameter $node } + 'parameter' { Resolve-ProcessParameter $node } default { throw ($LocalizedData.UnrecognizedParametersElement_F1 -f $node.LocalName) } } } @@ -1407,15 +237,15 @@ function Invoke-Plaster { $PSCmdlet.WriteDebug("Parameter values are:`n$($parameters -split "`n")") # Stores any updated default values back to the store file. - if ($flags.DefaultValueStoreDirty) { - $directory = Split-Path $defaultValueStorePath -Parent + if ($script:flags.DefaultValueStoreDirty) { + $directory = Split-Path $script:defaultValueStorePath -Parent if (!(Test-Path $directory)) { $PSCmdlet.WriteDebug("Creating directory for template's DefaultValueStore '$directory'.") New-Item $directory -ItemType Directory > $null } - $PSCmdlet.WriteDebug("DefaultValueStore is dirty, saving updated values to '$defaultValueStorePath'.") - $defaultValueStore | Export-Clixml -LiteralPath $defaultValueStorePath + $PSCmdlet.WriteDebug("DefaultValueStore is dirty, saving updated values to '$script:defaultValueStorePath'.") + $script:defaultValueStore | Export-Clixml -LiteralPath $script:defaultValueStorePath } # Output the DestinationPath @@ -1426,11 +256,11 @@ function Invoke-Plaster { if ($node -isnot [System.Xml.XmlElement]) { continue } switch -Regex ($node.LocalName) { - 'file|templateFile' { ProcessFile $node; break } - 'message' { ProcessMessage $node; break } - 'modify' { ProcessModifyFile $node; break } - 'newModuleManifest' { ProcessNewModuleManifest $node; break } - 'requireModule' { ProcessRequireModule $node; break } + 'file|templateFile' { Start-ProcessFile $node; break } + 'message' { Resolve-ProcessMessage $node; break } + 'modify' { Start-ProcessModifyFile $node; break } + 'newModuleManifest' { Resolve-ProcessNewModuleManifest $node; break } + 'requireModule' { Start-ProcessFileProcessRequireModule $node; break } default { throw ($LocalizedData.UnrecognizedContentElement_F1 -f $node.LocalName) } } } @@ -1441,9 +271,9 @@ function Invoke-Plaster { } } finally { # Dispose of the ConstrainedRunspace. - if ($constrainedRunspace) { - $constrainedRunspace.Dispose() - $constrainedRunspace = $null + if ($script:constrainedRunspace) { + $script:constrainedRunspace.Dispose() + $script:constrainedRunspace = $null } } } diff --git a/build.ps1 b/build.ps1 index c408421..4c27d60 100644 --- a/build.ps1 +++ b/build.ps1 @@ -36,6 +36,7 @@ param( ) $ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' # Bootstrap dependencies if ($Bootstrap.IsPresent) { @@ -61,4 +62,4 @@ if ($PSCmdlet.ParameterSetName -eq 'Help') { Set-BuildEnvironment -Force Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -properties $Properties -parameters $Parameters exit ([int](-not $psake.build_success)) -} \ No newline at end of file +}