Skip to content

Commit 8a5f6f1

Browse files
committed
Add Invoke-ScriptGenerator
semver:feature
1 parent 7d79e5e commit 8a5f6f1

File tree

3 files changed

+133
-9
lines changed

3 files changed

+133
-9
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class TextReplacement {
2+
[int]$StartOffset = 0
3+
[int]$EndOffset = 0
4+
[string]$Text = ''
5+
}

Source/Private/ConvertToAst.ps1

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ function ConvertToAst {
1313
Write-Debug " ENTER: ConvertToAst $Code"
1414
$ParseErrors = $null
1515
$Tokens = $null
16-
if ($Code | Test-Path -ErrorAction SilentlyContinue) {
17-
Write-Debug " Parse Code as Path"
18-
$AST = [System.Management.Automation.Language.Parser]::ParseFile(($Code | Convert-Path), [ref]$Tokens, [ref]$ParseErrors)
19-
} elseif ($Code -is [System.Management.Automation.FunctionInfo]) {
16+
17+
if ($Code -is [System.Management.Automation.FunctionInfo]) {
2018
Write-Debug " Parse Code as Function"
21-
$String = "function $($Code.Name) { $($Code.Definition) }"
22-
$AST = [System.Management.Automation.Language.Parser]::ParseInput($String, [ref]$Tokens, [ref]$ParseErrors)
19+
$AST = [System.Management.Automation.Language.Parser]::ParseInput($Code.Definition, "function:$($Code.Name)", [ref]$Tokens, [ref]$ParseErrors)
2320
} else {
24-
Write-Debug " Parse Code as String"
25-
$AST = [System.Management.Automation.Language.Parser]::ParseInput([String]$Code, [ref]$Tokens, [ref]$ParseErrors)
26-
}
21+
$Provider = $null
22+
try {
23+
[string[]]$Files = $ExecutionContext.SessionState.Path.GetResolvedProviderPathFromPSPath($Code, [ref]$Provider)
24+
} catch {
25+
Write-Debug ("Exception resolving Code as Path " + $_.Exception.Message)
26+
}
2727

28+
if ($Provider.Name -eq "FileSystem" -and $Files.Count -gt 0) {
29+
Write-Debug " Parse Code as File Path"
30+
$AST = [System.Management.Automation.Language.Parser]::ParseFile(($Files[0] | Convert-Path), [ref]$Tokens, [ref]$ParseErrors)
31+
} else {
32+
Write-Debug " Parse Code as String"
33+
$AST = [System.Management.Automation.Language.Parser]::ParseInput([String]$Code, [ref]$Tokens, [ref]$ParseErrors)
34+
}
35+
}
2836
Write-Debug " EXIT: ConvertToAst"
2937
[PSCustomObject]@{
3038
PSTypeName = "PoshCode.ModuleBuilder.ParseResults"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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

Comments
 (0)