psplaceholders.psm1
class PlaceholderMatch { hidden [string]$Tag Hidden [char]$PreChar hidden [char]$PostChar hidden [uint]$IndexOfTag } class Placeholder { [string]$Tag [System.Collections.Generic.List[String]]$Type = [System.Collections.Generic.List[String]]::new() [string]$Name [string]$Value hidden [bool]$HasValue [uint]$InFileCount = 0 [uint]$InFileNameCount = 0 } class FileWithPlaceholders { [string]$Name [string]$Folder [string]$Path hidden [System.Collections.Generic.List[string]]$PlaceholderInName = [System.Collections.Generic.List[string]]::new() hidden [System.Collections.Generic.List[string]]$PlaceholderInContent = [System.Collections.Generic.List[string]]::new() } class PlaceholderMatchEvaluators { [bool]$AllowEmptyPlaceholders [hashtable]$PlaceholderValues static [string]$Pattern = '(?''pre''[^{}]{1})?(?''placeholder''{{(?''executionFlag''&)?(?''value''[^{}]*)}})(?''post''[^{}]{1})?' [object]GetValue([GenericPlaceholderMatch]$placeholderMatch) { if ($this.PlaceholderValues.ContainsKey($placeholderMatch.Name)) { return $this.PlaceholderValues[$placeholderMatch.Name] } elseif ($this.AllowEmptyPlaceholders) { return $placeholderMatch.Tag } else { throw "Missing value for placeholder: '$($placeholderMatch.Name)'" } } [object]GetValue([ExecutablePlaceholderMatch]$placeholderMatch) { #Find required input $executionVariables = [System.Collections.Generic.List[psvariable]]::new() $missingVariables = [System.Collections.Generic.HashSet[String]]::new() foreach ($ip In $placeholderMatch.Input.Name) { if ($this.PlaceholderValues.ContainsKey($ip)) { $executionVariables.Add([psvariable]::new($ip, $this.PlaceholderValues[$ip])) } else { $null = $missingVariables.Add($ip) } } if ($missingVariables.Count -gt 0) { if (-not $this.AllowEmptyPlaceholders) { throw "Missing value for placeholder: $($ip)" } else { return $placeholderMatch.Tag } } #execute $executionResult = $placeholderMatch.ScriptBlock.InvokeWithContext($null, $executionVariables) #return result if ($executionResult.Count -gt 1) { #result is an array $result = $executionResult } else { #result is not an array, however scriptblock execution always returns an array $result = $executionResult[0] } return $result } #AdaptTo methods [string]Generic([System.Text.RegularExpressions.Match]$match) { $placeholderMatch = ConvertTo-PlaceholderMatch -Match $match $sb = [System.Text.StringBuilder]::new() if ($placeholderMatch.PreChar) { $sb.Append($placeholderMatch.PreChar) } $sb.Append($this.GetValue($placeholderMatch)) if ($placeholderMatch.PostChar) { $sb.Append($placeholderMatch.PostChar) } return $sb.ToString() } [string]Json([System.Text.RegularExpressions.Match]$match) { $placeholderMatch = ConvertTo-PlaceholderMatch -Match $match $value = $this.GetValue($placeholderMatch) $sb = [System.Text.StringBuilder]::new() if (($placeholderMatch.PreChar -eq '"') -and ($placeholderMatch.PostChar -eq '"')) { $convertToJsonParams = @{ Depth = 20 Compress = $true } if ([System.Management.Automation.LanguagePrimitives]::IsObjectEnumerable($value)) { $convertToJsonParams['AsArray'] = $true } $sb.Append(($value | ConvertTo-Json @convertToJsonParams -ErrorAction Stop)) } else { if ($placeholderMatch.PreChar) { $sb.Append($placeholderMatch.PreChar) } $sb.Append($value) if ($placeholderMatch.PostChar) { $sb.Append($placeholderMatch.PostChar) } } return $sb.ToString() } } class ExecutablePlaceholderInputMatch : PlaceholderMatch { hidden [string]$Type = 'ExecutionInput' [string]$Name [string]ToString() { return $this.Name } } class ExecutablePlaceholderMatch : PlaceholderMatch { hidden [string]$Type = 'Executable' [scriptblock]$ScriptBlock [System.Collections.Generic.HashSet[ExecutablePlaceholderInputMatch]]$Input = [System.Collections.Generic.HashSet[ExecutablePlaceholderInputMatch]]::new() } class GenericPlaceholderMatch : PlaceholderMatch { hidden [string]$Type = 'generic' [string]$Name } function ConvertTo-Placeholder { [CmdletBinding()] [Outputtype([Placeholder])] param ( [Parameter(Mandatory)] [PlaceholderMatch]$Match ) $result = [Placeholder]@{ Tag = $Match.Tag } $result.Type.Add($Match.Type) #calculate Name switch ($Match) { { $_ -is [ExecutablePlaceholderMatch] } { $result.Name = $_.ScriptBlock.ToString() break } { $_ -is [GenericPlaceholderMatch] } { $result.Name = $_.Name break } } #return result $result } function ConvertTo-PlaceholderMatch { [CmdletBinding()] [Outputtype([PlaceholderMatch])] param ( [Parameter(Mandatory)] [System.Text.RegularExpressions.Match]$Match ) if ($Match.Groups.Where({ $_.Name -eq 'executionFlag' }).Success) { $result = [ExecutablePlaceholderMatch]@{ ScriptBlock = [scriptblock]::Create($Match.Groups.Where({ $_.Name -eq 'value' }).Value) } Get-AstStatement -Ast $result.ScriptBlock.Ast -Type VariableExpressionAst | ForEach-Object -Process { $null = $result.Input.Add([ExecutablePlaceholderInputMatch]@{ Name = $_.VariablePath Tag = $_.Extent.Text IndexOfTag = $Match.Groups.Where({ $_.Name -eq 'placeholder' }).Index + 2 + $_.Extent.StartColumnNumber }) } } else { $result = [GenericPlaceholderMatch]@{ Name = $Match.Groups.Where({ $_.Name -eq 'value' }).Value } } $result.Tag = $Match.Groups.Where({ $_.Name -eq 'placeholder' }).Value $result.IndexOfTag = $Match.Groups.Where({ $_.Name -eq 'placeholder' }).Index if ($Match.Groups.Where({ $_.Name -eq 'pre' }).Success) { $result.PreChar = $Match.Groups.Where({ $_.Name -eq 'pre' }).Value } if ($Match.Groups.Where({ $_.Name -eq 'post' }).Success) { $result.PostChar = $Match.Groups.Where({ $_.Name -eq 'post' }).Value } #return result $result } function Get-PlaceholderMatchInString { [CmdletBinding()] [Outputtype([PlaceholderMatch[]])] param ( [Parameter(Mandatory)] [string]$String ) [regex]::Matches($String, [PlaceholderMatchEvaluators]::Pattern) | ForEach-Object { $placeholderMatch = ConvertTo-PlaceholderMatch -Match $_ if ($placeholderMatch -is [ExecutablePlaceholderMatch]) { foreach ($pmi in $placeholderMatch.Input) { #return as separate placeholder $pmi } } #return placeholdermatch $placeholderMatch } } function Update-PlaceholderInString { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] [CmdletBinding(SupportsShouldProcess)] [Outputtype([string])] param ( [Parameter(Mandatory)] [string]$String, [Parameter()] [hashtable]$PlaceholderValues = @{}, [Parameter()] [ValidateSet('json', 'generic')] [string]$AdaptTo = 'generic', [Parameter()] [switch]$AllowEmptyPlaceholders ) #Pick Evaluator $matchEvaluator = [PlaceholderMatchEvaluators]::New() $matchEvaluator.PlaceholderValues = $PlaceholderValues $matchEvaluator.AllowEmptyPlaceholders = $AllowEmptyPlaceholders.IsPresent switch ($AdaptTo) { 'generic' { $matchEvaluatorDelegate = [System.Delegate]::CreateDelegate([System.Text.RegularExpressions.MatchEvaluator], $matchEvaluator, 'Generic') break } 'json' { $matchEvaluatorDelegate = [System.Delegate]::CreateDelegate([System.Text.RegularExpressions.MatchEvaluator], $matchEvaluator, 'Json') break } } [regex]::Replace($String, [PlaceholderMatchEvaluators]::Pattern, $matchEvaluatorDelegate) } <# .SYNOPSIS Find unique placeholder in files or string. .DESCRIPTION Find unique placeholder in files or string in their usage statistics. File names will also be evaluated for placeholders. Placeholder pattern is: - {{placeholder name}} - for generic placeholders - {{& powershell code}} - for executable placeholders. The purpose of this placeholder is to allow the proper formatting of the date not to implement complex activities. Standard powershell variables might be used as placeholders - {{& $inputData }} - powershell variables inside executable placeholders will be parsed as generic placeholders .PARAMETER String Specify string to search for placeholders .PARAMETER Path Specify Files or Folders to search for placeholders. Folders will be evaluated recursively .PARAMETER FileStatistics Specify reference variable to collect the statistics of files and placeholders found in their content or name .EXAMPLE # will find the placeholder 'placeholder_holiday' Get-PSPlaceholder -string 'Happy Happy {{placeholder_holiday}}' .EXAMPLE # will find all placeholder in specified files/s [ref]$fileStat = $null Get-PSPlaceholder -Path <file/s path> -FileStatistics $fileStat .EXAMPLE # will find all placeholder in all files recursively to the provided folder [ref]$fileStat = $null Get-PSPlaceholder -Path <folder Path Here> -FileStatistics $fileStat .EXAMPLE # will find the executable placeholder and the 'Holydays' placeholder Get-PSPlaceholder -string 'Happy Happy {{& [string]::Join(",",$Holydays)}}' .EXAMPLE # will find the placeholder 'Name' Get-PSPlaceholder -string 'Happy Happy {{& $Name)}}' #> function Get-PSPlaceholder { [CmdletBinding(DefaultParameterSetName = 'inString')] [Outputtype([Placeholder[]])] param ( [Parameter(Mandatory, ParameterSetName = 'inString')] [string]$String, [Parameter(Mandatory, ParameterSetName = 'inFiles')] [string[]]$Path, [Parameter(ParameterSetName = 'inFiles')] [ref]$FileStatistics ) begin { $uniquePlaceholders = [System.Collections.Generic.Dictionary[string, Placeholder]]::new() $filesWithPlaceholderStatistics = [System.Collections.Generic.List[FileWithPlaceholders]]::new() } process { if ($PSBoundParameters.ContainsKey('String')) { Get-PlaceholderMatchInString -String $String | ForEach-Object -Process { #add placeholder to all placeholders collection if (-not $uniquePlaceholders.ContainsKey($_.Tag)) { $ph = ConvertTo-Placeholder -Match $_ $uniquePlaceholders.Add($ph.Tag, $ph) } } } else { #Find all items in scope $filesToCheck = [System.Collections.Generic.List[System.IO.FileInfo]]::new() foreach ($p in $Path) { if ([System.IO.File]::Exists($p)) { $filesToCheck.Add([System.IO.FileInfo]::new($p)) } elseif (Test-Path -Path $p -PathType Container) { Get-ChildItem -Path $p -Recurse -File -ErrorAction Stop | ForEach-Object -Process { $filesToCheck.Add($_) } } else { throw "Path: '$p' not found" } } #Check all files in scope for placeholders foreach ($file in $filesToCheck) { $fileWithPlaceholders = [FileWithPlaceholders]@{ Path = $file.FullName Name = $file.Name Folder = $file.Directory.FullName } #check file content for placeholders #Get file content using System.IO.File instead of get-content as it cannot read files that might contains the placeholder characters, e.g. [{}] $fileContent = [System.IO.File]::ReadAllText($fileWithPlaceholders.Path) if ($fileContent) { Get-PlaceholderMatchInString -String $fileContent -ErrorAction Stop | ForEach-Object -Process { #add placeholder to all placeholders collection if (-not $uniquePlaceholders.ContainsKey($_.Tag)) { $ph = ConvertTo-Placeholder -Match $_ $uniquePlaceholders.Add($ph.Tag, $ph) } $uniquePlaceholders[$_.Tag].InFileCount++ #mark placeholder as present in the file content $null = $fileWithPlaceholders.PlaceholderInContent.Add($_.Tag) } } #check file name for placeholders Get-PlaceholderMatchInString -String $fileWithPlaceholders.Name -ErrorAction Stop | ForEach-Object -Process { #add placeholder to all placeholders collection if (-not $uniquePlaceholders.ContainsKey($_.Tag)) { $ph = ConvertTo-Placeholder -Match $_ $uniquePlaceholders.Add($ph.Tag, $ph) } $uniquePlaceholders[$_.Tag].InFileNameCount++ #mark placeholder as present in the file content $null = $fileWithPlaceholders.PlaceholderInName.Add($_.Tag) } #include file in filesWithPlaceholderStatistics if it contains at least one placeholder if ($fileWithPlaceholders.PlaceholderInName.Count -gt 0 -or $fileWithPlaceholders.PlaceholderInContent.Count -gt 0 ) { $filesWithPlaceholderStatistics.Add($fileWithPlaceholders) } } #return file with placeholder statistics if required if ($PSBoundParameters.ContainsKey('FileStatistics')) { $FileStatistics.Value = $filesWithPlaceholderStatistics | ForEach-Object -Process { $_ } } } } end { $uniquePlaceholders.Values } } <# .SYNOPSIS Find and replace placeholder in files or string .DESCRIPTION Find and replace placeholder in files or strings. File names will also be evaluated for placeholders. Placeholder pattern is: - {{placeholder name}} - for generic placeholders - {{& powershell code}} - for executable placeholders. The purpose of this placeholder is to allow the proper formatting of the date not to implement complex activities. Standard powershell variables might be used as placeholders - {{& $inputData }} - powershell variables inside executable placeholders will be parsed as generic placeholders .PARAMETER String Specify string to search for placeholders and replace them .PARAMETER Path Specify Files or Folders to search for placeholders and replace them. Folders will be evaluated recursively .PARAMETER Values Specify set of placeholder names and their desired values to be used when replacing .PARAMETER AllowEmptyPlaceholders Allow the replacement of placeholder although not all values are provided. .PARAMETER AdaptTo Allows the language specific modifications outside of the placeholder string to be made where it make sense. For: - json: it will remove the surrounding double quotes(") if the placeholder resolves to an object and format that object as json sub element - generic: n/a .EXAMPLE # will replace the placeholder 'placeholder_holiday' with 'Easter' Update-PSPlaceholder -string 'Happy Happy {{placeholder_holiday}}' -Values @{placeholder_holiday='Easter'} .EXAMPLE # will replace the executable placeholder with 'Easter and Christmas' Update-PSPlaceholder -string 'Happy Happy {{& [string]::Join(" and ",$Holydays)}}' -Values @{Holydays='Easter','Christmas'} #> function Update-PSPlaceholder { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, ParameterSetName = 'inString')] [string]$String, [Parameter(Mandatory, ParameterSetName = 'inFiles')] [string[]]$Path, [Parameter()] [hashtable]$Values = @{}, [Parameter()] [switch]$AllowEmptyPlaceholders, [Parameter()] [ValidateSet('json', 'generic')] [string]$AdaptTo = 'generic' ) #Get placeholders if ($PSBoundParameters.ContainsKey('String')) { $placeholders = Get-PSPlaceholder -String $String } else { [ref]$filesWithPlaceholders = $null $placeholders = Get-PSPlaceholder -Path $Path -FileStatistics $filesWithPlaceholders } #Bind values to placeholders $unUsedPlacehodlerValues = @{} + $Values foreach ($p in $placeholders.Where({ $_.Type -eq 'Generic' })) { if ($Values.ContainsKey($p.Name)) { $p.Value = $Values[$p.Name] $p.HasValue = $true } #remove values to be able to track unused ones $unUsedPlacehodlerValues.Remove($p.Name) } #warn if there are unused placeholder values if ($unUsedPlacehodlerValues.Count -gt 0) { Write-Warning -Message "Unused placeholder values: $($Values.Keys -join ',')" } #Replace placeholders if ($PSCmdlet.ShouldProcess("Placeholders to be replaced:$($placeholders | Select-Object -Property Name,Value | Sort-Object -Property Name | Out-String)", '', '')) { #check for placeholders without value if (-not $AllowEmptyPlaceholders.IsPresent) { $emptyPlaceholders = $placeholders.Where({ ($_.Type -eq 'Generic') -and (-not $_.HasValue) }) if ($emptyPlaceholders) { throw "Unspecified placeholders: $($emptyPlaceholders.Name -join ', '). Use -AllowEmptyPlaceholders if you want to replace only part of the placeholders" } } #replace placeholders if ($PSBoundParameters.ContainsKey('String')) { Update-PlaceholderInString -String $String -PlaceholderValues $Values -AllowEmptyPlaceholders:$AllowEmptyPlaceholders.IsPresent -AdaptTo $AdaptTo } else { foreach ($file in $filesWithPlaceholders.Value) { #replace placeholder in content if ($file.PlaceholderInContent.count -gt 0) { $fileContent = [System.IO.File]::ReadAllText($file.Path) $updatedFileContent = Update-PlaceholderInString -String $fileContent -AllowEmptyPlaceholders:$AllowEmptyPlaceholders.IsPresent -PlaceholderValues $Values -AdaptTo $AdaptTo [System.IO.File]::WriteAllText($file.Path, $updatedFileContent) } #replace placeholder in file name if ($file.PlaceholderInName.Count -gt 0) { $newFileName = Update-PlaceholderInString -String $file.Name -PlaceholderValues $Values -AdaptTo $AdaptTo -AllowEmptyPlaceholders:$AllowEmptyPlaceholders.IsPresent [System.IO.File]::Move($file.Path, (Join-Path -Path $file.Folder -ChildPath $newFileName)) } } } } } Export-ModuleMember -Function @( 'Get-PSPlaceholder' 'Update-PSPlaceholder' ) |