|
| 1 | +using namespace System.Text |
| 2 | + |
| 3 | +function Invoke-ScriptGenerator { |
| 4 | + <# |
| 5 | + .SYNOPSIS |
| 6 | + Generate code using Script Generator functions |
| 7 | + .DESCRIPTION |
| 8 | + Script Generators let developers modify source code as it is being built. |
| 9 | +
|
| 10 | + A generator can create new script functions on the fly, such that whole functions are added to the built module, or can inject boilerplate code like error handling, logging, tracing and timing at build-time, so this code can be maintained once, and be automatically added (and updated) in all the places where it's needed when the module is built. |
| 11 | +
|
| 12 | + This command is run internally by Build-Module if you pass Generator configuration to it. |
| 13 | + .EXAMPLE |
| 14 | + $Boilerplate = @' |
| 15 | + param( |
| 16 | + # The Foreground Color (name, #rrggbb, etc) |
| 17 | + [Alias('Fg')] |
| 18 | + [PoshCode.Pansies.RgbColor]$ForegroundColor, |
| 19 | +
|
| 20 | + # The Background Color (name, #rrggbb, etc) |
| 21 | + [Alias('Bg')] |
| 22 | + [PoshCode.Pansies.RgbColor]$BackgroundColor |
| 23 | + ) |
| 24 | + $ForegroundColor.ToVt() + $BackgroundColor.ToVt($true) + ( |
| 25 | + Use-OriginalBlock |
| 26 | + ) +"`e[0m" # Reset colors |
| 27 | + '@ |
| 28 | +
|
| 29 | + # Or use a file path instead |
| 30 | + $Source = @' |
| 31 | + function Show-Date { |
| 32 | + param( |
| 33 | + # The text to display |
| 34 | + [string]$Format |
| 35 | + ) |
| 36 | + Get-Date -Format $Format |
| 37 | + } |
| 38 | + '@ |
| 39 | +
|
| 40 | + @( @{ Generator = "Add-Parameter"; FunctionName = "*"; Boilerplate = $Boilerplate } |
| 41 | + @{ Generator = "Merge-ScriptBlock"; FunctionName = "*"; Boilerplate = $Boilerplate } |
| 42 | + ) | Invoke-ScriptGenerator $Source |
| 43 | + #> |
| 44 | + [CmdletBinding()] |
| 45 | + param( |
| 46 | + # The script content, script, function info, or file path to parse |
| 47 | + [Parameter(Mandatory)] |
| 48 | + $Source, |
| 49 | + |
| 50 | + # The name of the Script Generator to invoke. Must be a command that takes an AST as a pipeline inputand outputs TextReplacement objects. |
| 51 | + # There are two built into ModuleBuilder: |
| 52 | + # - MergeBlocks. Supports Before/After/Around blocks for aspects like error handling or authentication. |
| 53 | + # - Add-Parameter. Supports adding parameters to functions (usually in conjunction with MergeBlock that use those parameters) |
| 54 | + [Parameter(ValueFromPipelineByPropertyName)] |
| 55 | + [string]$Generator, |
| 56 | + |
| 57 | + # Additional configuration parameters for the generator |
| 58 | + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] |
| 59 | + [hashtable]$Parameters, |
| 60 | + |
| 61 | + # If set, will overwrite the Source with the generated content. |
| 62 | + # Use with care, as this will modify the source file! |
| 63 | + [switch]$Overwrite |
| 64 | + ) |
| 65 | + begin { |
| 66 | + Write-Debug "Parsing $Source for $Generator with @{$($Parameters.Keys.ForEach{ $_ } -join ', ')}" |
| 67 | + $ParseResults = ConvertToAst $Source |
| 68 | + [StringBuilder]$Builder = $ParseResults.Ast.Extent.Text |
| 69 | + $File = $ParseResults.Ast.Extent.File |
| 70 | + } |
| 71 | + process { |
| 72 | + if (-not $PSBoundParameters.ContainsKey("Generator") -and $Parameters.ContainsKey("Generator")) { |
| 73 | + $Generator = $Parameters["Generator"] |
| 74 | + $null = $Parameters.Remove("Generator") |
| 75 | + } |
| 76 | + |
| 77 | + if (-not $Generator) { |
| 78 | + Write-Error "Generator missconfiguration. The Generator name is mandatory." |
| 79 | + continue |
| 80 | + } |
| 81 | + |
| 82 | + # Find that generator... |
| 83 | + $GeneratorCmd = Get-Command -Name ${Generator} -ParameterType Ast -ErrorAction Ignore |
| 84 | + | Where-Object { $_.OutputType.Name -eq "TextReplacement" } |
| 85 | + | Select-Object -First 1 |
| 86 | + |
| 87 | + if (-not $GeneratorCmd) { |
| 88 | + Write-Error "Generator missconfiguration. Unable to find Generator = '$Generator'" |
| 89 | + continue |
| 90 | + } |
| 91 | + |
| 92 | + Write-Verbose "Generating $GeneratorCmd in $Source" |
| 93 | + #! Process replacements from the bottom up, so the line numbers work |
| 94 | + foreach ($Replacement in $ParseResults | & $GeneratorCmd @Parameters | Sort-Object StartOffset -Descending) { |
| 95 | + $Builder = $Builder.Remove($replacement.StartOffset, ($replacement.EndOffset - $replacement.StartOffset)).Insert($replacement.StartOffset, $replacement.Text) |
| 96 | + } |
| 97 | + |
| 98 | + #! If we're looping through multiple generators, we have to parse the new version of the source |
| 99 | + if ($MyInvocation.ExpectingInput) { |
| 100 | + $ParseResults = ConvertToAst $Builder |
| 101 | + } |
| 102 | + } |
| 103 | + end { |
| 104 | + Write-Debug "Overwrite: $Overwrite and it's a file: $([bool]$File) (Content is $($Builder.Length) long)" |
| 105 | + if ($Overwrite -and $File) { |
| 106 | + Set-Content $File $Builder |
| 107 | + } else { |
| 108 | + $Builder.ToString() |
| 109 | + } |
| 110 | + } |
| 111 | +} |
0 commit comments