├── .circleci └── config.yml ├── .gitattributes ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── PSStringTemplate.build.ps1 ├── PSStringTemplate.sln ├── README.md ├── ThirdPartyNotices.txt ├── appveyor.yml ├── debugHarness.ps1 ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── en-US │ ├── Invoke-StringTemplate.md │ ├── New-StringTemplateGroup.md │ └── PSStringTemplate.md ├── module ├── PSStringTemplate.format.ps1xml ├── PSStringTemplate.psd1 └── PSStringTemplate.psm1 ├── nuget.config ├── src └── PSStringTemplate │ ├── AdapterUtil.cs │ ├── ErrorListener.cs │ ├── InvokeStringTemplateCommand.cs │ ├── MessageHelper.cs │ ├── NewStringTemplateGroupCommand.cs │ ├── PSObjectAdaptor.cs │ ├── PSStringTemplate.csproj │ ├── PSStringTemplate.ruleset │ ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ └── Resources.resx │ ├── TemplateGroupInfo.cs │ ├── TemplateInfo.cs │ ├── TypeAdapter.cs │ └── stylecop.json ├── test ├── Invoke-StringTemplate.Tests.ps1 ├── New-StringTemplateGroup.Tests.ps1 └── PSStringTemplate.Tests.ps1 └── tools ├── GetDotNet.ps1 ├── GetOpenCover.ps1 └── InvokeCircleCI.ps1 /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | build: 4 | working_directory: ~/psstringtemplate 5 | docker: 6 | - image: microsoft/powershell@sha256:6d36c498c1fdbb3ef42acabea1d06b12870e390fd3053993974ebaa6ba507849 7 | steps: 8 | - checkout 9 | - run: 10 | name: Run build script 11 | command: 'powershell -File ./tools/InvokeCircleCI.ps1' 12 | - store_artifacts: 13 | path: ./testresults/pester.xml 14 | prefix: tests 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | SHiPS/ 5 | 6 | # Project specific files 7 | tools/dotnet 8 | tools/opencover 9 | 10 | # User-specific files 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | bld/ 27 | [Bb]in/ 28 | [Oo]bj/ 29 | [Ll]og/ 30 | 31 | # Visual Studio 2015 cache/options directory 32 | .vs/ 33 | # Uncomment if you have tasks that create the project's static files in wwwroot 34 | #wwwroot/ 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | 44 | # Build Results of an ATL Project 45 | [Dd]ebugPS/ 46 | [Rr]eleasePS/ 47 | dlldata.c 48 | 49 | # DNX 50 | project.lock.json 51 | project.fragment.lock.json 52 | artifacts/ 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | *.VC.VC.opendb 92 | 93 | # Visual Studio profiler 94 | *.psess 95 | *.vsp 96 | *.vspx 97 | *.sap 98 | 99 | # TFS 2012 Local Workspace 100 | $tf/ 101 | 102 | # Guidance Automation Toolkit 103 | *.gpState 104 | 105 | # ReSharper is a .NET coding add-in 106 | _ReSharper*/ 107 | *.[Rr]e[Ss]harper 108 | *.DotSettings.user 109 | 110 | # JustCode is a .NET coding add-in 111 | .JustCode 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # NCrunch 120 | _NCrunch_* 121 | .*crunch*.local.xml 122 | nCrunchTemp_* 123 | 124 | # MightyMoose 125 | *.mm.* 126 | AutoTest.Net/ 127 | 128 | # Web workbench (sass) 129 | .sass-cache/ 130 | 131 | # Installshield output folder 132 | [Ee]xpress/ 133 | 134 | # DocProject is a documentation generator add-in 135 | DocProject/buildhelp/ 136 | DocProject/Help/*.HxT 137 | DocProject/Help/*.HxC 138 | DocProject/Help/*.hhc 139 | DocProject/Help/*.hhk 140 | DocProject/Help/*.hhp 141 | DocProject/Help/Html2 142 | DocProject/Help/html 143 | 144 | # Click-Once directory 145 | publish/ 146 | 147 | # Publish Web Output 148 | *.[Pp]ublish.xml 149 | *.azurePubxml 150 | # TODO: Comment the next line if you want to checkin your web deploy settings 151 | # but database connection strings (with potential passwords) will be unencrypted 152 | #*.pubxml 153 | *.publishproj 154 | 155 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 156 | # checkin your Azure Web App publish settings, but sensitive information contained 157 | # in these scripts will be unencrypted 158 | PublishScripts/ 159 | 160 | # NuGet Packages 161 | *.nupkg 162 | # The packages folder can be ignored because of Package Restore 163 | **/packages/* 164 | # except build/, which is used as an MSBuild target. 165 | !**/packages/build/ 166 | # Uncomment if necessary however generally it will be regenerated when needed 167 | #!**/packages/repositories.config 168 | # NuGet v3's project.json files produces more ignoreable files 169 | *.nuget.props 170 | *.nuget.targets 171 | 172 | # Microsoft Azure Build Output 173 | csx/ 174 | *.build.csdef 175 | 176 | # Microsoft Azure Emulator 177 | ecf/ 178 | rcf/ 179 | 180 | # Windows Store app package directories and files 181 | AppPackages/ 182 | BundleArtifacts/ 183 | Package.StoreAssociation.xml 184 | _pkginfo.txt 185 | 186 | # Visual Studio cache files 187 | # files ending in .cache can be ignored 188 | *.[Cc]ache 189 | # but keep track of directories ending in .cache 190 | !*.[Cc]ache/ 191 | 192 | # Others 193 | ClientBin/ 194 | ~$* 195 | *~ 196 | *.dbmdl 197 | *.dbproj.schemaview 198 | *.jfm 199 | *.pfx 200 | *.publishsettings 201 | node_modules/ 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | 223 | # Business Intelligence projects 224 | *.rdl.data 225 | *.bim.layout 226 | *.bim_*.settings 227 | 228 | # Microsoft Fakes 229 | FakesAssemblies/ 230 | 231 | # GhostDoc plugin setting file 232 | *.GhostDoc.xml 233 | 234 | # Node.js Tools for Visual Studio 235 | .ntvs_analysis.dat 236 | 237 | # Visual Studio 6 build log 238 | *.plg 239 | 240 | # Visual Studio 6 workspace options file 241 | *.opt 242 | 243 | # Visual Studio LightSwitch build output 244 | **/*.HTMLClient/GeneratedArtifacts 245 | **/*.DesktopClient/GeneratedArtifacts 246 | **/*.DesktopClient/ModelManifest.xml 247 | **/*.Server/GeneratedArtifacts 248 | **/*.Server/ModelManifest.xml 249 | _Pvt_Extensions 250 | 251 | # Paket dependency manager 252 | .paket/paket.exe 253 | paket-files/ 254 | 255 | # FAKE - F# Make 256 | .fake/ 257 | 258 | # JetBrains Rider 259 | .idea/ 260 | *.sln.iml 261 | 262 | # CodeRush 263 | .cr/ 264 | 265 | # Python Tools for Visual Studio (PTVS) 266 | __pycache__/ 267 | *.pyc 268 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.csharp", 6 | "ms-vscode.powershell" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Attach", 6 | "type": "clr", 7 | "request": "attach", 8 | "processId": "${command:pickProcess}" 9 | }, 10 | { 11 | "name": ".NET Core Launch (console)", 12 | "type": "clr", 13 | "request": "launch", 14 | "preLaunchTask": "Build", 15 | "program": "powershell", 16 | "args": [ 17 | "-NoExit", 18 | ". ${workspaceRoot}/debugHarness.ps1"], 19 | "cwd": "${workspaceRoot}", 20 | "stopAtEntry": false, 21 | "console": "externalTerminal" 22 | } 23 | ] 24 | } 25 | 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | //-------- Files configuration -------- 3 | 4 | // When enabled, will trim trailing whitespace when you save a file. 5 | "files.trimTrailingWhitespace": true, 6 | 7 | // When enabled, insert a final new line at the end of the file when saving it. 8 | "files.insertFinalNewline": true, 9 | 10 | "search.exclude": { 11 | "Release": true, 12 | "tools/dotnet": true, 13 | "tools/opencover": true 14 | }, 15 | 16 | //-------- PowerShell Configuration -------- 17 | 18 | // Use a custom PowerShell Script Analyzer settings file for this workspace. 19 | // Relative paths for this setting are always relative to the workspace root dir. 20 | "powershell.scriptAnalysis.settingsPath": "ScriptAnalyzerSettings.psd1" 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${relativeFile}: the current opened file relative to workspaceRoot 5 | // ${fileBasename}: the current opened file's basename 6 | // ${fileDirname}: the current opened file's dirname 7 | // ${fileExtname}: the current opened file's extension 8 | // ${cwd}: the current working directory of the spawned process 9 | { 10 | // See https://go.microsoft.com/fwlink/?LinkId=733558 11 | // for the documentation about the tasks.json format 12 | "version": "0.1.0", 13 | "runner": "terminal", 14 | 15 | // Start PowerShell 16 | "windows": { 17 | "command": "PowerShell.exe" 18 | }, 19 | "linux": { 20 | "command": "/usr/bin/powershell" 21 | }, 22 | "osx": { 23 | "command": "/usr/local/bin/powershell" 24 | }, 25 | 26 | // The command is a shell script 27 | "isShellCommand": true, 28 | 29 | // Show the output window always 30 | "showOutput": "always", 31 | 32 | "args": [ 33 | "-NoProfile", "-ExecutionPolicy", "Bypass" 34 | ], 35 | 36 | // Associate with test task runner 37 | "tasks": [ 38 | { 39 | "taskName": "Clean", 40 | "suppressTaskName": true, 41 | "showOutput": "always", 42 | "args": [ 43 | "Invoke-Build -Task Clean" 44 | ] 45 | }, 46 | { 47 | "taskName": "Build", 48 | "suppressTaskName": true, 49 | "isBuildCommand": true, 50 | "showOutput": "always", 51 | "args": [ 52 | "Invoke-Build" 53 | ] 54 | }, 55 | { 56 | "taskName": "Test", 57 | "suppressTaskName": true, 58 | "isTestCommand": true, 59 | "showOutput": "always", 60 | "args": [ 61 | "Invoke-Build -Task Test" 62 | ] 63 | }, 64 | { 65 | "taskName": "Test With Coverage", 66 | "suppressTaskName": true, 67 | "isTestCommand": true, 68 | "showOutput": "always", 69 | "args": [ 70 | "Invoke-Build -Task Test -GenerateCodeCoverage" 71 | ] 72 | }, 73 | { 74 | "taskName": "Install", 75 | "suppressTaskName": true, 76 | "showOutput": "always", 77 | "args": [ 78 | "Invoke-Build -Task Install -Configuration Release" 79 | ] 80 | } 81 | ] 82 | } 83 | 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Patrick Meinecke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PSStringTemplate.build.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module InvokeBuild, Pester, PlatyPS -Version 5.1 2 | 3 | [CmdletBinding()] 4 | param( 5 | [ValidateSet('Debug', 'Release')] 6 | [string] 7 | $Configuration = 'Debug', 8 | 9 | [switch] 10 | $GenerateCodeCoverage 11 | ) 12 | 13 | $moduleName = 'PSStringTemplate' 14 | $manifest = Test-ModuleManifest -Path $PSScriptRoot\module\$moduleName.psd1 ` 15 | -ErrorAction Ignore ` 16 | -WarningAction Ignore 17 | 18 | $script:Settings = @{ 19 | Name = $moduleName 20 | Manifest = $manifest 21 | Version = $manifest.Version 22 | ShouldTest = $true 23 | } 24 | 25 | $script:Folders = @{ 26 | PowerShell = "$PSScriptRoot\module" 27 | CSharp = "$PSScriptRoot\src" 28 | Build = '{0}\src\{1}*\bin\{2}' -f $PSScriptRoot, $moduleName, $Configuration 29 | Release = '{0}\Release\{1}\{2}' -f $PSScriptRoot, $moduleName, $manifest.Version 30 | Docs = "$PSScriptRoot\docs" 31 | Test = "$PSScriptRoot\test" 32 | Results = "$PSScriptRoot\testresults" 33 | } 34 | 35 | $script:Discovery = @{ 36 | HasDocs = Test-Path ('{0}\{1}\*.md' -f $Folders.Docs, $PSCulture) 37 | HasTests = Test-Path ('{0}\*.Tests.ps1' -f $Folders.Test) 38 | IsUnix = $PSVersionTable.PSEdition -eq "Core" -and -not $IsWindows 39 | } 40 | 41 | $tools = "$PSScriptRoot\tools" 42 | $script:dotnet = & $tools\GetDotNet.ps1 -Unix:$Discovery.IsUnix 43 | 44 | if ($GenerateCodeCoverage.IsPresent) { 45 | $script:openCover = & $tools\GetOpenCover.ps1 46 | } 47 | 48 | 49 | task Clean { 50 | if ($PSScriptRoot -and (Test-Path $PSScriptRoot\Release)) { 51 | Remove-Item $PSScriptRoot\Release -Recurse 52 | } 53 | 54 | $null = New-Item $Folders.Release -ItemType Directory 55 | if (Test-Path $Folders.Results) { 56 | Remove-Item $Folders.Results -Recurse 57 | } 58 | 59 | $null = New-Item $Folders.Results -ItemType Directory 60 | & $dotnet clean 61 | } 62 | 63 | task BuildDocs -If { $Discovery.HasDocs } { 64 | $sourceDocs = "$PSScriptRoot\docs\$PSCulture" 65 | $releaseDocs = '{0}\{1}' -f $Folders.Release, $PSCulture 66 | 67 | $null = New-Item $releaseDocs -ItemType Directory -Force -ErrorAction SilentlyContinue 68 | if ($Discovery.IsUnix) { 69 | Write-Host -ForegroundColor Green 'The mkdir errors below are fine, they''re due to a alias in platyPS' 70 | } 71 | $null = New-ExternalHelp -Path $sourceDocs -OutputPath $releaseDocs 72 | } 73 | 74 | task BuildDll { 75 | if (-not $Discovery.IsUnix) { 76 | & $dotnet build --configuration $Configuration --framework net452 77 | } 78 | & $dotnet build --configuration $Configuration --framework netcoreapp2.0 79 | } 80 | 81 | task CopyToRelease { 82 | $powershellSource = '{0}\*' -f $Folders.PowerShell 83 | $release = $Folders.Release 84 | $releaseDesktopBin = "$release\bin\Desktop" 85 | $releaseCoreBin = "$release\bin\Core" 86 | $sourceDesktopBin = '{0}\net452' -f $Folders.Build 87 | $sourceCoreBin = '{0}\netcoreapp2.0' -f $Folders.Build 88 | Copy-Item -Path $powershellSource -Destination $release -Recurse -Force 89 | 90 | if (-not $Discovery.IsUnix) { 91 | $null = New-Item $releaseDesktopBin -Force -ItemType Directory 92 | Copy-Item -Path $sourceDesktopBin\PSStringTemplate* -Destination $releaseDesktopBin -Force 93 | Copy-Item -Path $sourceDesktopBin\Antlr* -Destination $releaseDesktopBin -Force 94 | } 95 | 96 | $null = New-Item $releaseCoreBin -Force -ItemType Directory 97 | Copy-Item -Path $sourceCoreBin\PSStringTemplate* -Destination $releaseCoreBin -Force 98 | Copy-Item -Path $sourceCoreBin\Antlr* -Destination $releaseCoreBin -Force 99 | } 100 | 101 | task DoTest -If { $Discovery.HasTests -and $Settings.ShouldTest } { 102 | if ($Discovery.IsUnix) { 103 | $scriptString = ' 104 | $projectPath = "{0}" 105 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" 106 | ' -f $PSScriptRoot 107 | } else { 108 | $scriptString = ' 109 | Set-ExecutionPolicy Bypass -Force -Scope Process 110 | $projectPath = "{0}" 111 | Invoke-Pester "$projectPath" -OutputFormat NUnitXml -OutputFile "$projectPath\testresults\pester.xml" 112 | ' -f $PSScriptRoot 113 | } 114 | 115 | $encodedCommand = 116 | [convert]::ToBase64String( 117 | [System.Text.Encoding]::Unicode.GetBytes( 118 | $scriptString)) 119 | 120 | $powershell = (Get-Command powershell).Source 121 | 122 | if ($GenerateCodeCoverage.IsPresent) { 123 | if ($Discovery.IsUnix) { 124 | throw 'Generating code coverage from .NET core is currently unsupported.' 125 | } 126 | # OpenCover needs full pdb's. I'm very open to suggestions for streamlining this... 127 | & $dotnet clean 128 | & $dotnet build --configuration $Configuration --framework net452 /p:DebugType=Full 129 | 130 | $moduleName = $Settings.Name 131 | $release = '{0}\bin\Desktop\{1}' -f $Folders.Release, $moduleName 132 | $coverage = '{0}\net452\{1}' -f $Folders.Build, $moduleName 133 | 134 | Rename-Item "$release.pdb" -NewName "$moduleName.pdb.tmp" 135 | Rename-Item "$release.dll" -NewName "$moduleName.dll.tmp" 136 | Copy-Item "$coverage.pdb" "$release.pdb" 137 | Copy-Item "$coverage.dll" "$release.dll" 138 | 139 | & $openCover ` 140 | -target:$powershell ` 141 | -register:user ` 142 | -output:$PSScriptRoot\testresults\opencover.xml ` 143 | -hideskipped:all ` 144 | -filter:+[PSStringTemplate*]* ` 145 | -targetargs:"-NoProfile -EncodedCommand $encodedCommand" 146 | 147 | Remove-Item "$release.pdb" 148 | Remove-Item "$release.dll" 149 | Rename-Item "$release.pdb.tmp" -NewName "$moduleName.pdb" 150 | Rename-Item "$release.dll.tmp" -NewName "$moduleName.dll" 151 | } else { 152 | & $powershell -NoProfile -EncodedCommand $encodedCommand 153 | } 154 | } 155 | 156 | task DoInstall { 157 | $sourcePath = '{0}\*' -f $Folders.Release 158 | $installBase = $Home 159 | if ($profile) { $installBase = $profile | Split-Path } 160 | $installPath = '{0}\Modules\{1}\{2}' -f $installBase, $Settings.Name, $Settings.Version 161 | 162 | if (-not (Test-Path $installPath)) { 163 | $null = New-Item $installPath -ItemType Directory 164 | } 165 | 166 | Copy-Item -Path $sourcePath -Destination $installPath -Force -Recurse 167 | } 168 | 169 | task DoPublish { 170 | if ($Configuration -eq 'Debug') { 171 | throw 'Configuration must not be Debug to publish!' 172 | } 173 | 174 | if (-not (Test-Path $env:USERPROFILE\.PSGallery\apikey.xml)) { 175 | throw 'Could not find PSGallery API key!' 176 | } 177 | 178 | $apiKey = (Import-Clixml $env:USERPROFILE\.PSGallery\apikey.xml).GetNetworkCredential().Password 179 | Publish-Module -Name $Folders.Release -NuGetApiKey $apiKey -Confirm 180 | } 181 | 182 | task Build -Jobs Clean, BuildDll, CopyToRelease, BuildDocs 183 | 184 | task Test -Jobs Build, DoTest 185 | 186 | task PreRelease -Jobs Test 187 | 188 | task Install -Jobs PreRelease, DoInstall 189 | 190 | task Publish -Jobs PreRelease, DoPublish 191 | 192 | task . Build 193 | 194 | -------------------------------------------------------------------------------- /PSStringTemplate.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSStringTemplate", "src\PSStringTemplate\PSStringTemplate.csproj", "{7EE9A67A-F825-4977-B13A-7D0B9ADC69A2}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{7F10773E-D370-40DC-8D9C-A9C45C52FE66}" 9 | ProjectSection(SolutionItems) = preProject 10 | docs\Invoke-StringTemplate.md = docs\Invoke-StringTemplate.md 11 | docs\New-StringTemplateGroup.md = docs\New-StringTemplateGroup.md 12 | docs\PSStringTemplate.md = docs\PSStringTemplate.md 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{11A01620-D423-4F3E-842B-42A12F0717C8}" 16 | ProjectSection(SolutionItems) = preProject 17 | LICENSE = LICENSE 18 | PSStringTemplate.build.ps1 = PSStringTemplate.build.ps1 19 | ThirdPartyNotices.txt = ThirdPartyNotices.txt 20 | EndProjectSection 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6C34D8F8-41F8-4B4D-B727-F78A41C681F9}" 23 | EndProject 24 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "module", "module", "{956F2E8C-F7D0-4BB2-B80D-F659D80F9811}" 25 | ProjectSection(SolutionItems) = preProject 26 | module\PSStringTemplate.format.ps1xml = module\PSStringTemplate.format.ps1xml 27 | module\PSStringTemplate.psd1 = module\PSStringTemplate.psd1 28 | EndProjectSection 29 | EndProject 30 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{BAD1D9D7-C20B-4D49-BC97-2FE94A66F34F}" 31 | ProjectSection(SolutionItems) = preProject 32 | test\PSStringTemplate.tests.ps1 = test\PSStringTemplate.tests.ps1 33 | EndProjectSection 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Release|Any CPU = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 41 | {7EE9A67A-F825-4977-B13A-7D0B9ADC69A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {7EE9A67A-F825-4977-B13A-7D0B9ADC69A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {7EE9A67A-F825-4977-B13A-7D0B9ADC69A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {7EE9A67A-F825-4977-B13A-7D0B9ADC69A2}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | GlobalSection(NestedProjects) = preSolution 50 | {7EE9A67A-F825-4977-B13A-7D0B9ADC69A2} = {6C34D8F8-41F8-4B4D-B727-F78A41C681F9} 51 | EndGlobalSection 52 | EndGlobal 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSStringTemplate 2 | 3 | The PSStringTemplate module provides a PowerShell friendly interface for creating templates using the 4 | [StringTemplate4](https://github.com/antlr/antlrcs) template engine. 5 | 6 | This project adheres to the Contributor Covenant [code of conduct](https://github.com/SeeminglyScience/PSStringTemplate/tree/master/docs/CODE_OF_CONDUCT.md). 7 | By participating, you are expected to uphold this code. Please report unacceptable behavior to seeminglyscience@gmail.com. 8 | 9 | ## Build Status 10 | 11 | |AppVeyor (Windows)|CircleCI (Linux)|CodeCov| 12 | |---|---|---| 13 | |[![Build status](https://ci.appveyor.com/api/projects/status/3uvr9oq297uhvj8p?svg=true)](https://ci.appveyor.com/project/SeeminglyScience/psstringtemplate)|[![CircleCI](https://circleci.com/gh/SeeminglyScience/PSStringTemplate.svg?style=svg)](https://circleci.com/gh/SeeminglyScience/PSStringTemplate)|[![codecov](https://codecov.io/gh/SeeminglyScience/PSStringTemplate/branch/master/graph/badge.svg)](https://codecov.io/gh/SeeminglyScience/PSStringTemplate)| 14 | 15 | ## Documentation 16 | 17 | Check out our **[documentation](https://github.com/SeeminglyScience/PSStringTemplate/tree/master/docs/en-US/PSStringTemplate.md)** for information about how to use this project. For more details on the template definition syntax specifically see the documentation for the [StringTemplate4 project](https://github.com/antlr/stringtemplate4/blob/master/doc/index.md). 18 | 19 | ## Installation 20 | 21 | ### Gallery 22 | 23 | ```powershell 24 | Install-Module PSStringTemplate -Scope CurrentUser 25 | ``` 26 | 27 | ### Source 28 | 29 | ```powershell 30 | git clone 'https://github.com/SeeminglyScience/PSStringTemplate.git' 31 | Set-Location ./PSStringTemplate 32 | Install-Module platyPS, Pester, InvokeBuild -Force 33 | Import-Module platyPS, Pester, InvokeBuild 34 | Invoke-Build -Task Install 35 | ``` 36 | 37 | ## Usage 38 | 39 | ### Anonymous template with dictionary parameters 40 | 41 | ```powershell 42 | Invoke-StringTemplate -Definition ' is very !' -Parameters @{ 43 | language = 'PowerShell' 44 | adjective = 'cool' 45 | } 46 | ``` 47 | 48 | ```txt 49 | PowerShell is very cool! 50 | ``` 51 | 52 | ### Anonymous template with object as parameters 53 | 54 | ```powershell 55 | $definition = 'Name: <\n>Commands: ' 56 | Invoke-StringTemplate -Definition $definition -Parameters (Get-Module PSReadLine) 57 | ``` 58 | 59 | ```txt 60 | Name: PSReadline 61 | Commands: Get-PSReadlineKeyHandler, Get-PSReadlineOption, Remove-PSReadlineKeyHandler, Set-PSReadlineKeyHandler, Set-PSReadlineOption, PSConsoleHostReadline 62 | ``` 63 | 64 | ### TemplateGroup definition 65 | 66 | ```powershell 67 | $definition = @' 68 | Param(parameter) ::= "[] $" 69 | Method(member) ::= << 70 | [] static () { 71 | throw [NotImplementedException]::new() 72 | } 73 | >> 74 | Class(Name, DeclaredMethods) ::= << 75 | class MyClass : { 76 | 77 | } 78 | >> 79 | '@ 80 | $group = New-StringTemplateGroup -Definition $definition 81 | $group | Invoke-StringTemplate -Name Class -Parameters ([System.Runtime.InteropServices.ICustomMarshaler]) 82 | ``` 83 | 84 | ```txt 85 | class MyClass : ICustomMarshaler { 86 | [Object] MarshalNativeToManaged ([IntPtr] $pNativeData) { 87 | throw [NotImplementedException]::new() 88 | } 89 | 90 | [IntPtr] MarshalManagedToNative ([Object] $ManagedObj) { 91 | throw [NotImplementedException]::new() 92 | } 93 | 94 | [Void] CleanUpNativeData ([IntPtr] $pNativeData) { 95 | throw [NotImplementedException]::new() 96 | } 97 | 98 | [Void] CleanUpManagedData ([Object] $ManagedObj) { 99 | throw [NotImplementedException]::new() 100 | } 101 | 102 | [Int32] GetNativeDataSize () { 103 | throw [NotImplementedException]::new() 104 | } 105 | } 106 | ``` 107 | 108 | ## Contributions Welcome! 109 | 110 | We would love to incorporate community contributions into this project. If you would like to 111 | contribute code, documentation, tests, or bug reports, please read our [Contribution Guide](https://github.com/SeeminglyScience/ClassExplorer/tree/master/docs/CONTRIBUTING.md) to learn more. 112 | -------------------------------------------------------------------------------- /ThirdPartyNotices.txt: -------------------------------------------------------------------------------- 1 | This project uses third-party libraries or other resources that may 2 | be distributed under different licenses. 3 | 4 | In the event that we accidentally failed to list a required notice, 5 | please bring it to our attention through any of the ways detailed here : 6 | 7 | seeminglyscience@gmail.com 8 | 9 | The attached notices are provided for information only. 10 | 11 | 1.) License Notice for The ANTLR Project 12 | 13 | https://github.com/antlr/antlrcs/blob/master/LICENSE.txt 14 | 15 | [The "BSD license"] 16 | Copyright (c) 2011 The ANTLR Project 17 | All rights reserved. 18 | 19 | Redistribution and use in source and binary forms, with or without 20 | modification, are permitted provided that the following conditions 21 | are met: 22 | 23 | 1. Redistributions of source code must retain the above copyright 24 | notice, this list of conditions and the following disclaimer. 25 | 2. Redistributions in binary form must reproduce the above copyright 26 | notice, this list of conditions and the following disclaimer in the 27 | documentation and/or other materials provided with the distribution. 28 | 3. Neither the name of the copyright holder nor the names of its 29 | contributors may be used to endorse or promote products derived from 30 | this software without specific prior written permission. 31 | 32 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 33 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 34 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 35 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 36 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 37 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 38 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 39 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 40 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 41 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.0.{build} 2 | image: Visual Studio 2017 3 | environment: 4 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 5 | DOTNET_CLI_TELEMETRY_OPTOUT: true 6 | install: 7 | - ps: >- 8 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 9 | 10 | Install-Module Pester -RequiredVersion 4.0.6 -Scope CurrentUser -Force -SkipPublisherCheck 11 | 12 | Install-Module InvokeBuild -RequiredVersion 3.2.1 -Scope CurrentUser -Force 13 | 14 | Install-Module platyPS -RequiredVersion 0.8.1 -Scope CurrentUser -Force 15 | 16 | choco install codecov --no-progress 17 | build_script: 18 | - ps: >- 19 | Invoke-Build -Task Prerelease -Configuration Release -GenerateCodeCoverage 20 | 21 | $resultsFile = "$PWD\testresults\pester.xml" 22 | 23 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures) 24 | 25 | 26 | if ($passed) { 27 | [System.Net.WebClient]::new().UploadFile("https://ci.appveyor.com/api/testresults/nunit/${env:APPVEYOR_JOB_ID}", $resultsFile) 28 | codecov -f "$PWD\testresults\opencover.xml" 29 | } else { 30 | $Error | Format-List * -Force 31 | exit 1; 32 | } 33 | on_finish: 34 | - ps: >- 35 | Add-Type -AssemblyName System.IO.Compression.FileSystem 36 | 37 | $zipPath = "$pwd\PSStringTemplate.zip" 38 | 39 | [System.IO.Compression.ZipFile]::CreateFromDirectory("$pwd\Release", $zipPath) 40 | 41 | Push-AppveyorArtifact $zipPath 42 | -------------------------------------------------------------------------------- /debugHarness.ps1: -------------------------------------------------------------------------------- 1 | # Use this file to debug the module. 2 | Import-Module -Name $PSScriptRoot\Release\PSStringTemplate\*\PSStringTemplate.psd1 -Force 3 | 4 | Invoke-StringTemplate -Definition '' @{Name = 'Test'} -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at seeminglyscience@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | 76 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | We welcome many kinds of community contributions to this project! Whether it's a feature implementation, 4 | bug fix, or a good idea, please create an issue so that we can discuss it. It is not necessary to create an 5 | issue before sending a pull request but it may speed up the process if we can discuss your idea before 6 | you start implementing it. 7 | 8 | Because this project exposes a couple different public APIs, we must be very mindful of any potential breaking 9 | changes. Some contributions may not be accepted if they risk introducing breaking changes or if they 10 | don't match the goals of the project. The core maintainer team has the right of final approval over 11 | any contribution to this project. However, we are very happy to hear community feedback on any decision 12 | so that we can ensure we are solving the right problems in the right way. 13 | 14 | ## Ways to Contribute 15 | 16 | - File a bug or feature request as an [issue](https://github.com/SeeminglyScience/PSStringTemplate/issues) 17 | - Comment on existing issues to give your feedback on how they should be fixed/implemented 18 | - Contribute a bug fix or feature implementation by submitting a pull request 19 | - Contribute more unit tests for feature areas that lack good coverage 20 | - Review the pull requests that others submit to ensure they follow [established guidelines](#pull-request-guidelines) 21 | 22 | ## Code Contribution Guidelines 23 | 24 | Here's a high level list of guidelines to follow to ensure your code contribution is accepted: 25 | 26 | - Follow established guidelines for coding style and design 27 | - Follow established guidelines for commit hygiene 28 | - Write unit tests to validate new features and bug fixes 29 | - Ensure that the 'Release' build and unit tests pass locally 30 | - Respond to all review feedback and final commit cleanup 31 | 32 | ### Practice Good Commit Hygiene 33 | 34 | First of all, make sure you are practicing [good commit hygiene](http://blog.ericbmerritt.com/2011/09/21/commit-hygiene-and-git.html) 35 | so that your commits provide a good history of the changes you are making. To be more specific: 36 | 37 | - **Write good commit messages** 38 | 39 | Commit messages should be clearly written so that a person can look at the commit log and understand 40 | how and why a given change was made. Here is a great model commit message taken from a [blog post 41 | by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html): 42 | 43 | Capitalized, short (50 chars or less) summary 44 | 45 | More detailed explanatory text, if necessary. Wrap it to about 72 46 | characters or so. In some contexts, the first line is treated as the 47 | subject of an email and the rest of the text as the body. The blank 48 | line separating the summary from the body is critical (unless you omit 49 | the body entirely); tools like rebase can get confused if you run the 50 | two together. 51 | 52 | Write your commit message in the imperative: "Fix bug" and not "Fixed bug" 53 | or "Fixes bug." This convention matches up with commit messages generated 54 | by commands like git merge and git revert. 55 | 56 | Further paragraphs come after blank lines. 57 | 58 | - Bullet points are okay, too 59 | 60 | - Typically a hyphen or asterisk is used for the bullet, followed by a 61 | single space, with blank lines in between, but conventions vary here 62 | 63 | - Use a hanging indent 64 | 65 | A change that fixes a known bug with an issue filed should use the proper syntax so that the [issue 66 | is automatically closed](https://help.github.com/articles/closing-issues-via-commit-messages/) once 67 | your change is merged. Here's an example of what such a commit message should look like: 68 | 69 | Fix #3: Catch NullReferenceException from DoThing 70 | 71 | This change adds a try/catch block to catch a NullReferenceException that 72 | gets thrown by DoThing [...] 73 | 74 | - **Squash your commits** 75 | 76 | If you are introducing a new feature but have implemented it over multiple commits, 77 | please [squash those commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 78 | into a single commit that contains all the changes in one place. This especially applies to any "oops" 79 | commits where a file is forgotten or a typo is being fixed. Following this approach makes it a lot easier 80 | to pull those changes to other branches or roll back the change if necessary. 81 | 82 | - **Keep individual commits for larger changes** 83 | 84 | You can certainly maintain individual commits for different phases of a big change. For example, if 85 | you want to reorganize some files before adding new functionality, have your first commit contain all 86 | of the file move changes and then the following commit can have all of the feature additions. We 87 | highly recommend this approach so that larger commits don't turn into a jumbled mess. 88 | 89 | ### Build 'Release' Before Submitting 90 | 91 | Before you send out your pull request, make sure that you have run a Release configuration build of the 92 | project and that all new and existing tests are passing. The Release configuration build ensures that 93 | all public API interfaces have been documented correctly otherwise it throws an error. We have turned 94 | on this check so that our project will always have good generated documentation. 95 | ### Follow the Pull Request Process 96 | 97 | - **Create your pull request** 98 | 99 | Use the [typical process](https://help.github.com/articles/using-pull-requests/) to send a pull request 100 | from your fork of the project. In your pull request message, please give a high-level summary of the 101 | changes that you have made so that reviewers understand the intent of the changes. You should receive 102 | initial comments within a day or two, but please feel free to ping if things are taking longer than 103 | expected. 104 | 105 | - **Respond to code review feedback** 106 | 107 | If the reviewers ask you to make changes, make them as a new commit to your branch and push them so 108 | that they are made available for a final review pass. Do not rebase the fixes just yet so that the 109 | commit hash changes don't upset GitHub's pull request UI. 110 | 111 | - **If necessary, do a final rebase** 112 | 113 | Once your final changes have been accepted, we may ask you to do a final rebase to have your commits 114 | so that they follow our commit guidelines. If specific guidance is given, please follow it when 115 | rebasing your commits. Once you do your final push, we will merge your changes! 116 | 117 | -------------------------------------------------------------------------------- /docs/en-US/Invoke-StringTemplate.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSStringTemplate.dll-Help.xml 3 | online version: https://github.com/SeeminglyScience/PSStringTemplate/blob/master/docs/en-US/Invoke-StringTemplate.md 4 | schema: 2.0.0 5 | --- 6 | 7 | # Invoke-StringTemplate 8 | 9 | ## SYNOPSIS 10 | 11 | Renders a Template object. 12 | 13 | ## SYNTAX 14 | 15 | ### ByDefinition 16 | 17 | ```powershell 18 | Invoke-StringTemplate -Definition [[-Parameters] ] 19 | ``` 20 | 21 | ### ByGroup 22 | 23 | ```powershell 24 | Invoke-StringTemplate [-Group] [-Name ] [[-Parameters] ] 25 | ``` 26 | 27 | ## DESCRIPTION 28 | 29 | This cmdlet will take a Template object from a TemplateGroup, add specified parameters and 30 | render the template into a string. It can use an existing Template object (from the 31 | New-StringTemplateGroup cmdlet) or it can create a new Template using a string specified in 32 | the "Definition" parameter. 33 | 34 | ## EXAMPLES 35 | 36 | ### -------------------------- EXAMPLE 1 -------------------------- 37 | 38 | ```powershell 39 | PS> Invoke-StringTemplate -Definition ' is very !' -Parameters @{ 40 | language = 'PowerShell' 41 | adjective = 'cool' 42 | } 43 | ``` 44 | 45 | ```txt 46 | Output 47 | ------ 48 | 49 | PowerShell is very cool! 50 | ``` 51 | 52 | ### -------------------------- EXAMPLE 2 -------------------------- 53 | 54 | ```powershell 55 | PS> $definition = 'Name: <\n>Commands: ' 56 | PS> Invoke-StringTemplate -Definition $definition -Parameters (Get-Module PSReadLine) 57 | ``` 58 | 59 | ```txt 60 | Output 61 | ------ 62 | 63 | Name: PSReadline 64 | Commands: Get-PSReadlineKeyHandler, Get-PSReadlineOption, Remove-PSReadlineKeyHandler, Set-PSReadlineKeyHandler, Set-PSReadlineOption, PSConsoleHostReadline 65 | ``` 66 | 67 | ### -------------------------- EXAMPLE 3 -------------------------- 68 | 69 | ```powershell 70 | PS> $definition = @' 71 | Param(parameter) ::= "[] $" 72 | Method(member) ::= << 73 | [] static () { 74 | throw [NotImplementedException]::new() 75 | } 76 | >> 77 | Class(Name, DeclaredMethods) ::= << 78 | class MyClass : { 79 | 80 | } 81 | >> 82 | '@ 83 | PS> $group = New-StringTemplateGroup -Definition $definition 84 | PS> $group | Invoke-StringTemplate -Name Class -Parameters ([System.Runtime.InteropServices.ICustomMarshaler]) 85 | ``` 86 | 87 | ```txt 88 | Output 89 | ------ 90 | 91 | class MyClass : ICustomMarshaler { 92 | [Object] MarshalNativeToManaged ([IntPtr] $pNativeData) { 93 | throw [NotImplementedException]::new() 94 | } 95 | 96 | [IntPtr] MarshalManagedToNative ([Object] $ManagedObj) { 97 | throw [NotImplementedException]::new() 98 | } 99 | 100 | [Void] CleanUpNativeData ([IntPtr] $pNativeData) { 101 | throw [NotImplementedException]::new() 102 | } 103 | 104 | [Void] CleanUpManagedData ([Object] $ManagedObj) { 105 | throw [NotImplementedException]::new() 106 | } 107 | 108 | [Int32] GetNativeDataSize () { 109 | throw [NotImplementedException]::new() 110 | } 111 | } 112 | ``` 113 | 114 | ## PARAMETERS 115 | 116 | ### -Definition 117 | 118 | Specifies a Template definition string to create a new Template to be rendered. 119 | 120 | ```yaml 121 | Type: String 122 | Parameter Sets: ByDefinition 123 | Aliases: 124 | 125 | Required: True 126 | Position: Named 127 | Default value: None 128 | Accept pipeline input: False 129 | Accept wildcard characters: False 130 | ``` 131 | 132 | ### -Group 133 | 134 | Specifies an existing TemplateGroup to retrieve a template from. 135 | 136 | ```yaml 137 | Type: TemplateGroupInfo 138 | Parameter Sets: ByGroup 139 | Aliases: 140 | 141 | Required: True 142 | Position: 0 143 | Default value: None 144 | Accept pipeline input: True (ByPropertyName, ByValue) 145 | Accept wildcard characters: False 146 | ``` 147 | 148 | ### -Name 149 | 150 | Specifies the name of the Template within the TemplateGroup. 151 | 152 | ```yaml 153 | Type: String 154 | Parameter Sets: ByGroup 155 | Aliases: Name 156 | 157 | Required: False 158 | Position: Named 159 | Default value: None 160 | Accept pipeline input: True (ByPropertyName) 161 | Accept wildcard characters: False 162 | ``` 163 | 164 | ### -Parameters 165 | 166 | Specifies the arguments to pass to the template. If a IDictionary object (such as a hashtable) is 167 | specified, the entries will be used as arguments. All other objects will use the objects properties 168 | as arguments. 169 | 170 | Some methods will also be added as arguments if they meet the following criteria: 171 | 172 | - Has a return value 173 | - Has a parameterless overload 174 | - The name follows the format "GetSomething" 175 | - Does not have a matching property with value 176 | 177 | As an example, the method RuntimeMethodInfo.GetParameters() would be added as an argument with the name 178 | "Parameters". This is to stay consistent with StringTemplate4 behavior. See documentation for the 179 | StringTemplate4 template engine more information. 180 | 181 | ```yaml 182 | Type: PSObject 183 | Parameter Sets: (All) 184 | Aliases: 185 | 186 | Required: False 187 | Position: 0 188 | Default value: None 189 | Accept pipeline input: False 190 | Accept wildcard characters: False 191 | ``` 192 | 193 | ## INPUTS 194 | 195 | ### PSStringTemplate.TemplateGroupInfo 196 | 197 | You can pass template groups from New-StringTemplateGroup to this cmdlet. You can also pass objects with a property 198 | named "Group". 199 | 200 | ### System.String 201 | 202 | You can pass objects with a property named "Name" as template names to this cmdlet. 203 | 204 | 205 | ## OUTPUTS 206 | 207 | ### System.String 208 | 209 | The rendered template will be returned as a string. 210 | 211 | ## NOTES 212 | 213 | ## RELATED LINKS 214 | -------------------------------------------------------------------------------- /docs/en-US/New-StringTemplateGroup.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: PSStringTemplate.dll-Help.xml 3 | online version: https://github.com/SeeminglyScience/PSStringTemplate/blob/master/docs/en-US/New-StringTemplateGroup.md 4 | schema: 2.0.0 5 | --- 6 | 7 | # New-StringTemplateGroup 8 | 9 | ## SYNOPSIS 10 | 11 | Define a group of templates. 12 | 13 | ## SYNTAX 14 | 15 | ```powershell 16 | New-StringTemplateGroup -Definition 17 | ``` 18 | 19 | ## DESCRIPTION 20 | 21 | The New-StringTemplateGroup cmdlet allows you to create a group of 22 | templates using the group definition syntax. Templates within a group 23 | can reference other templates in the same group. 24 | 25 | You can use this to enumerate arrays, call different templates based on 26 | conditions, or just for better organization. 27 | 28 | ## EXAMPLES 29 | 30 | ### -------------------------- EXAMPLE 1 -------------------------- 31 | 32 | ```powershell 33 | PS> $group = New-StringTemplateGroup -Definition @' 34 | 35 | memberTemplate(Name, Parameters, ReturnType) ::= << 36 | () 37 | >> 38 | 39 | paramTemplate(param) ::= "$" 40 | '@ 41 | PS> $group | Invoke-StringTemplate -Name memberTemplate ([string].GetProperty('Length')) 42 | Length 43 | PS> $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('Clone')) 44 | Clone() 45 | PS> $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('IsNullOrWhiteSpace')) 46 | IsNullOrWhiteSpace($value) 47 | PS> $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('Insert')) 48 | Insert($startIndex, $value) 49 | ``` 50 | 51 | Create a template to generate different member expressions from MemberInfo objects. 52 | 53 | ## PARAMETERS 54 | 55 | ### -Definition 56 | 57 | The group definition string to use to compile a template group. 58 | 59 | ```yaml 60 | Type: String 61 | Parameter Sets: (All) 62 | Aliases: 63 | 64 | Required: True 65 | Position: Named 66 | Default value: None 67 | Accept pipeline input: False 68 | Accept wildcard characters: False 69 | ``` 70 | 71 | ## INPUTS 72 | 73 | ### None 74 | 75 | ## OUTPUTS 76 | 77 | ### PSStringTemplate.TemplateGroupInfo 78 | 79 | The compiled template group is returned to the pipeline. This can then be passed to 80 | the Invoke-StringTemplate cmdlet for rendering. 81 | 82 | ## NOTES 83 | 84 | ## RELATED LINKS 85 | -------------------------------------------------------------------------------- /docs/en-US/PSStringTemplate.md: -------------------------------------------------------------------------------- 1 | --- 2 | Module Name: PSStringTemplate 3 | Module Guid: f188d0cf-291f-41a1-ae0e-c770d980cf6e 4 | Download Help Link: 5 | Help Version: 1.0.0.5 6 | Locale: en-US 7 | --- 8 | 9 | # PSStringTemplate Module 10 | 11 | ## Description 12 | 13 | Create and render templates using the StringTemplate template engine. 14 | 15 | ## PSStringTemplate Cmdlets 16 | 17 | ### [Invoke-StringTemplate](Invoke-StringTemplate.md) 18 | 19 | This cmdlet will take a Template object from a TemplateGroup, add specified parameters and 20 | render the template into a string. It can use an existing Template object (from the 21 | New-StringTemplateGroup cmdlet) or it can create a new Template using a string specified in 22 | the "Definition" parameter. 23 | 24 | ### [New-StringTemplateGroup](New-StringTemplateGroup.md) 25 | 26 | The New-StringTemplateGroup cmdlet allows you to create a group of 27 | templates using the group definition syntax. Templates within a group 28 | can reference other templates in the same group. 29 | 30 | You can use this to enumerate arrays, call different templates based on 31 | conditions, or just for better organization. 32 | -------------------------------------------------------------------------------- /module/PSStringTemplate.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PSStringTemplate.TemplateGroupInfo 6 | 7 | PSStringTemplate.TemplateGroupInfo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | $_.Templates.Name -join ', ' 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PSStringTemplate.TemplateInfo 28 | 29 | PSStringTemplate.TemplateInfo 30 | 31 | 32 | 33 | 34 | 35 | 36 | Name 37 | 38 | 39 | 40 | $_.Parameters -join ', ' 41 | 42 | 43 | TemplateSource 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /module/PSStringTemplate.psd1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SeeminglyScience/PSStringTemplate/5fbb552a117759774d805d51fd014c87b58aba6f/module/PSStringTemplate.psd1 -------------------------------------------------------------------------------- /module/PSStringTemplate.psm1: -------------------------------------------------------------------------------- 1 | if (-not $PSVersionTable.PSEdition -or $PSVersionTable.PSEdition -eq 'Desktop') { 2 | Import-Module $PSScriptRoot/bin/Desktop/PSStringTemplate.dll 3 | } else { 4 | Import-Module $PSScriptRoot/bin/Core/PSStringTemplate.dll 5 | } 6 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/PSStringTemplate/AdapterUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace PSStringTemplate 4 | { 5 | /// 6 | /// Provides shared utility methods for adapters. 7 | /// 8 | internal static class AdapterUtil 9 | { 10 | /// 11 | /// Filter empty adapter results to allow PowerShell-like 12 | /// treatment of objects as true or false. 13 | /// 14 | /// The value to filter. 15 | /// Either if empty or the value. 16 | internal static object NullIfEmpty(object value) 17 | { 18 | return value == null 19 | ? null 20 | : LanguagePrimitives.IsTrue(value) 21 | ? value 22 | : null; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/PSStringTemplate/ErrorListener.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Globalization; 4 | using System.Management.Automation; 5 | using System.Management.Automation.Language; 6 | using Antlr4.StringTemplate; 7 | using Antlr4.StringTemplate.Misc; 8 | 9 | using Strings = PSStringTemplate.Properties.Resources; 10 | 11 | namespace PSStringTemplate 12 | { 13 | /// 14 | /// Intercepts errors generated by objects and creates 15 | /// terminating error records that a PowerShell user would expect. 16 | /// 17 | internal class ErrorListener : ITemplateErrorListener 18 | { 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The cmdlet invocation context to throw from. 23 | internal ErrorListener(Cmdlet terminatingErrorContext) 24 | { 25 | Debug.Assert(terminatingErrorContext != null, "terminatingErrorContext != null"); 26 | ErrorContext = terminatingErrorContext; 27 | } 28 | 29 | /// 30 | /// Gets the cmdlet context to use so invocation messages are correct. 31 | /// 32 | private Cmdlet ErrorContext { get; } 33 | 34 | /// 35 | /// Handles errors at template compile time, typically related to parsing issues. 36 | /// 37 | /// The message from Antlr. 38 | public void CompiletimeError(TemplateMessage msg) 39 | { 40 | if (msg == null) 41 | { 42 | ThrowUnhandledError("CompiletimeError", null); 43 | return; 44 | } 45 | 46 | var helper = new MessageHelper(msg); 47 | 48 | var errorDescription = 49 | string.Format( 50 | CultureInfo.CurrentCulture, 51 | Strings.CompiletimeExceptionWrapper, 52 | helper.ErrorDescription); 53 | 54 | var errors = new[] 55 | { 56 | new ParseError( 57 | helper.ErrorExtent, 58 | "CompiletimeError", 59 | errorDescription) 60 | }; 61 | 62 | ErrorContext.ThrowTerminatingError( 63 | new ErrorRecord( 64 | new ParseException(errors), 65 | "CompiletimeError", 66 | ErrorCategory.ParserError, 67 | msg.Self)); 68 | } 69 | 70 | /// 71 | /// Catch and rethrow unexpected internal errors. 72 | /// 73 | /// The from Antlr. 74 | public void InternalError(TemplateMessage msg) 75 | { 76 | ThrowUnhandledError("InternalError", msg); 77 | } 78 | 79 | /// 80 | /// Catch and rethrow unexpected IO errors. 81 | /// 82 | /// The from Antlr. 83 | public void IOError(TemplateMessage msg) 84 | { 85 | ThrowUnhandledError("IOError", msg); 86 | } 87 | 88 | /// 89 | /// Handles errors thrown while a template is rendering. 90 | /// 91 | /// The message from Antlr. 92 | public void RuntimeError(TemplateMessage msg) 93 | { 94 | if (msg == null) 95 | { 96 | ThrowUnhandledError("RuntimeError", null); 97 | return; 98 | } 99 | 100 | // Ignore missed parameters to be more consistent with how PowerShell handles 101 | // null values. Might be worth adding a "Strict" switch parameter. 102 | if (msg.Error.Message.Equals(@"attribute {0} isn't defined")) 103 | { 104 | ErrorContext.WriteDebug( 105 | string.Format( 106 | CultureInfo.CurrentCulture, 107 | Strings.DebugArgumentNotFound, 108 | msg.Arg as string, 109 | msg.Self.Name)); 110 | return; 111 | } 112 | 113 | if (msg.Error.Message.Equals(@"no such property or can't access: {0}")) 114 | { 115 | ErrorContext.WriteDebug( 116 | string.Format( 117 | CultureInfo.CurrentCulture, 118 | Strings.DebugPropertyNotFound, 119 | msg.Arg as string, 120 | msg.Self.Name)); 121 | return; 122 | } 123 | 124 | var fullMessage = 125 | string.Format( 126 | CultureInfo.CurrentCulture, 127 | Strings.RuntimeExceptionWrapper, 128 | msg.Self.Name, 129 | msg.ToString()); 130 | 131 | ErrorContext.ThrowTerminatingError( 132 | new ErrorRecord( 133 | new RuntimeException(fullMessage), 134 | "RuntimeError", 135 | ErrorCategory.InvalidOperation, 136 | msg)); 137 | } 138 | 139 | /// 140 | /// Throws a generate error record for any unanticipated exception. 141 | /// 142 | /// The ErrorId we want to show in the error record to help narrow down context. 143 | /// The message passed to the caller from Antlr. 144 | private void ThrowUnhandledError(string id, TemplateMessage msg) 145 | { 146 | var exceptionMessage = 147 | string.Format( 148 | CultureInfo.CurrentCulture, 149 | Strings.UnhandledErrorMessage, 150 | msg.ToString()); 151 | 152 | ErrorContext.ThrowTerminatingError( 153 | new ErrorRecord( 154 | new NotImplementedException(exceptionMessage), 155 | id, 156 | ErrorCategory.NotImplemented, 157 | msg)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/PSStringTemplate/InvokeStringTemplateCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Globalization; 7 | using System.Linq; 8 | using System.Management.Automation; 9 | using System.Reflection; 10 | using System.Text.RegularExpressions; 11 | 12 | using Strings = PSStringTemplate.Properties.Resources; 13 | 14 | namespace PSStringTemplate 15 | { 16 | /// 17 | /// The Invoke-StringTemplate cmdlet adds arguments to a template object 18 | /// (either existing or created by this cmdlet) and returns the rendered 19 | /// string. 20 | /// 21 | [Cmdlet(VerbsLifecycle.Invoke, "StringTemplate")] 22 | [OutputType(typeof(string))] 23 | public class InvokeStringTemplateCommand : Cmdlet 24 | { 25 | private TemplateInfo _currentTemplate; 26 | 27 | /// 28 | /// Gets or sets the string template definition to invoke. 29 | /// 30 | [Parameter(Mandatory = true, ParameterSetName = "ByDefinition")] 31 | [ValidateNotNullOrEmpty] 32 | public string Definition { get; set; } 33 | 34 | /// 35 | /// Gets or sets the target template group. 36 | /// 37 | [Parameter( 38 | Mandatory = true, 39 | Position = 0, 40 | ValueFromPipeline = true, 41 | ValueFromPipelineByPropertyName = true, 42 | ParameterSetName = "ByGroup")] 43 | [ValidateNotNullOrEmpty] 44 | public TemplateGroupInfo Group { get; set; } 45 | 46 | /// 47 | /// Gets or sets the name of the template to invoke. 48 | /// 49 | [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = "ByGroup")] 50 | [ValidateNotNullOrEmpty] 51 | public string Name { get; set; } 52 | 53 | /// 54 | /// Gets or sets the template parameters. 55 | /// 56 | [Parameter(Position = 0, ValueFromPipeline = true)] 57 | [ValidateNotNullOrEmpty] 58 | public PSObject Parameters { get; set; } 59 | 60 | /// 61 | /// ProcessRecord method. 62 | /// 63 | protected override void ProcessRecord() 64 | { 65 | TemplateGroupInfo group = null; 66 | if (Group != null) 67 | { 68 | group = Group; 69 | group.Bind(this); 70 | var templateNames = group.Templates.Select(t => t.Name); 71 | var safeTemplateNames = templateNames as string[] ?? templateNames.ToArray(); 72 | 73 | var name = Name ?? safeTemplateNames.FirstOrDefault(); 74 | 75 | if (!safeTemplateNames.Contains(name)) 76 | { 77 | var nameString = string.Join( 78 | CultureInfo.CurrentCulture.TextInfo.ListSeparator, 79 | safeTemplateNames); 80 | 81 | var msg = string.Format( 82 | CultureInfo.CurrentCulture, 83 | Strings.TemplateNotFound, 84 | name, 85 | nameString); 86 | 87 | var error = new ErrorRecord( 88 | new InvalidOperationException(msg), 89 | nameof(Strings.TemplateNotFound), 90 | ErrorCategory.ObjectNotFound, 91 | name); 92 | 93 | ThrowTerminatingError(error); 94 | } 95 | 96 | _currentTemplate = group.Templates 97 | .FirstOrDefault(t => t.Name == name); 98 | } 99 | 100 | if (Definition != null) 101 | { 102 | group = TemplateGroupInfo.CreateFromTemplateDefinition(this, Definition); 103 | _currentTemplate = group.Templates.FirstOrDefault(); 104 | } 105 | 106 | Debug.Assert(_currentTemplate != null, "_currentTemplate != null"); 107 | if (Parameters != null) 108 | { 109 | if (Parameters.BaseObject is IDictionary asDictionary) 110 | { 111 | foreach (DictionaryEntry keyValuePair in asDictionary) 112 | { 113 | AddTemplateArgument(keyValuePair.Key as string, keyValuePair.Value); 114 | } 115 | } 116 | else 117 | { 118 | ProcessObjectAsArguments(); 119 | } 120 | } 121 | 122 | var result = _currentTemplate.Instance.Render(CultureInfo.CurrentCulture); 123 | if (result != null) 124 | { 125 | WriteObject(result); 126 | } 127 | 128 | _currentTemplate.ResetInstance(); 129 | 130 | group.Unbind(); 131 | } 132 | 133 | /// 134 | /// Get the properties of the object in the "Parameter" input parameter and add them as 135 | /// template arguments. 136 | /// 137 | [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "The exception itself doesn't matter here, only that it's not accessible as a property.")] 138 | private void ProcessObjectAsArguments() 139 | { 140 | if (Parameters.BaseObject is Type type) 141 | { 142 | ProcessStaticProperties(type); 143 | } 144 | 145 | // Add any property with value as an argument. 146 | var propertyNames = new List(); 147 | foreach (var property in Parameters.Properties) 148 | { 149 | // Some properties throw exceptions depending on the object's state, or if not 150 | // implemented. (TypeInfo.DeclaringMethod for example) 151 | try 152 | { 153 | if (property.Value == null) continue; 154 | } 155 | catch 156 | { 157 | continue; 158 | } 159 | 160 | AddTemplateArgument(property.Name, property.Value); 161 | propertyNames.Add(property.Name); 162 | } 163 | 164 | // Add any method that: 165 | // 1. Starts with Get and any one upper case alpha character 166 | // 2. Has no parameters 167 | // 3. Returns something 168 | // 4. Doesn't have a matching properties 169 | foreach (var method in Parameters.Methods) 170 | { 171 | if (!Regex.IsMatch(method.Name, @"^Get[A-Z]")) continue; 172 | 173 | var nameAsProperty = Regex.Replace(method.Name, @"^Get", string.Empty); 174 | 175 | if (propertyNames.Contains(nameAsProperty) || 176 | !_currentTemplate.Parameters.Contains(nameAsProperty) || 177 | !method.OverloadDefinitions.First().Contains(@"()") || 178 | method.OverloadDefinitions.First().Contains("void")) continue; 179 | 180 | object result; 181 | try 182 | { 183 | result = method.Invoke(); 184 | } 185 | catch 186 | { 187 | WriteDebug( 188 | string.Format( 189 | CultureInfo.CurrentCulture, 190 | Strings.DebugInvokePropertyLikeMethodException, 191 | method.Name, 192 | nameAsProperty)); 193 | continue; 194 | } 195 | 196 | AddTemplateArgument(nameAsProperty, result); 197 | } 198 | } 199 | 200 | private void ProcessStaticProperties(Type type) 201 | { 202 | Array.ForEach( 203 | type.GetProperties(BindingFlags.Static | BindingFlags.Public), 204 | property => 205 | { 206 | AddTemplateArgument( 207 | property.Name, 208 | AdapterUtil.NullIfEmpty( 209 | property.GetValue(null))); 210 | }); 211 | } 212 | 213 | /// 214 | /// Adds an argument to a template with extra exception handling. 215 | /// 216 | /// The parameter name. 217 | /// The value to assign to the parameter. 218 | private void AddTemplateArgument(string name, object value) 219 | { 220 | try 221 | { 222 | _currentTemplate.Instance.Add(name, value); 223 | WriteVerbose( 224 | string.Format( 225 | CultureInfo.CurrentCulture, 226 | Strings.VerboseAddedParameter, 227 | name, 228 | _currentTemplate.Name)); 229 | } 230 | catch (ArgumentException exception) 231 | { 232 | if (exception.Message.StartsWith( 233 | @"no such attribute: ", 234 | StringComparison.CurrentCulture)) 235 | { 236 | WriteDebug( 237 | string.Format( 238 | CultureInfo.CurrentCulture, 239 | Strings.DebugAttributeNotFound, 240 | name, 241 | _currentTemplate.Name)); 242 | return; 243 | } 244 | 245 | throw; 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /src/PSStringTemplate/MessageHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Management.Automation.Language; 3 | using Antlr.Runtime; 4 | using Antlr4.StringTemplate.Misc; 5 | 6 | namespace PSStringTemplate 7 | { 8 | /// 9 | /// Helper class to get only the pieces of a TemplateMessage that we need to create 10 | /// an error record for PowerShell to consume. 11 | /// 12 | internal class MessageHelper 13 | { 14 | private readonly TemplateMessage _message; 15 | private readonly CommonToken _token; 16 | private readonly RecognitionException _cause; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The message passed by Antlr. 22 | internal MessageHelper(TemplateMessage msg) 23 | { 24 | _message = msg; 25 | _token = GetToken(); 26 | 27 | // TODO: Get the type of exception before casting and include that in the error. 28 | _cause = msg.Cause as RecognitionException; 29 | 30 | // TODO: Include the "Expecting" property that is sometimes included in the exception. 31 | ErrorDescription = GetDescription(); 32 | ErrorExtent = GetScriptExtent(); 33 | } 34 | 35 | /// 36 | /// Gets the summary of the error from the . 37 | /// 38 | internal string ErrorDescription { get; } 39 | 40 | /// 41 | /// Gets the approximate location of the invalid syntax. 42 | /// 43 | internal ScriptExtent ErrorExtent { get; } 44 | 45 | private ScriptExtent GetScriptExtent() 46 | { 47 | var startOffsetInLine = 48 | _cause?.CharPositionInLine 49 | ?? _token?.CharPositionInLine 50 | ?? 0; 51 | 52 | // Prefer the input stream from cause if possible, the token is more likely to 53 | // only be a portion of the source, which causes the extent to be off. 54 | var fullText = (_cause?.Input ?? _token?.InputStream)?.ToString() ?? string.Empty; 55 | var lines = fullText.Replace("\r", string.Empty).Split('\n'); 56 | 57 | // The line number in the template message isn't reliable, sometimes it's zero based, 58 | // sometimes it's one based. 59 | var lineNumber = fullText 60 | .Take(_cause?.Index ?? _token?.StartIndex ?? 0) 61 | .Count(c => c == '\n'); 62 | 63 | var line = lines.Length > lineNumber 64 | ? lines[lineNumber] 65 | : string.Empty; 66 | 67 | var start = new ScriptPosition( 68 | string.Empty, 69 | _cause?.Line ?? 0, 70 | startOffsetInLine + 1, 71 | line); 72 | 73 | return new ScriptExtent(start, start); 74 | } 75 | 76 | private string GetDescription() 77 | { 78 | // Lexer is the only message type that doesn't use the Arg property. 79 | return (_message as TemplateLexerMessage)?.Message 80 | ?? _message.Arg as string 81 | ?? string.Empty; 82 | } 83 | 84 | private CommonToken GetToken() 85 | { 86 | // Lexer again likes to break the mold. May remove the check for a token in cause, haven't 87 | // seen it used yet. 88 | var result = 89 | (_message as TemplateCompiletimeMessage)?.Token 90 | ?? (_message as TemplateLexerMessage)?.TemplateToken 91 | ?? (_message.Cause as RecognitionException)?.Token; 92 | return result as CommonToken; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PSStringTemplate/NewStringTemplateGroupCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | using Antlr4.StringTemplate; 4 | using Antlr4.StringTemplate.Misc; 5 | 6 | namespace PSStringTemplate 7 | { 8 | /// 9 | /// The New-StringTemplateGroup cmdlet creates a object 10 | /// from either a group or template definition string. 11 | /// 12 | [Cmdlet(VerbsCommon.New, "StringTemplateGroup", DefaultParameterSetName = "ByTemplateDefinition")] 13 | [OutputType(typeof(TemplateGroupInfo))] 14 | public class NewStringTemplateGroupCommand : Cmdlet 15 | { 16 | /// 17 | /// Gets or sets the string template group definition. 18 | /// 19 | [Parameter(Mandatory = true)] 20 | [ValidateNotNullOrEmpty] 21 | public string Definition { get; set; } 22 | 23 | /// 24 | /// EndProcessing method. 25 | /// 26 | protected override void EndProcessing() 27 | { 28 | var group = new TemplateGroupString(Definition) { Listener = new ErrorListener(this) }; 29 | 30 | var groupInfo = new TemplateGroupInfo(group); 31 | 32 | group.RegisterModelAdaptor(typeof(PSObject), new PSObjectAdaptor()); 33 | group.RegisterModelAdaptor(typeof(Type), new TypeAdapter()); 34 | group.RegisterRenderer(typeof(DateTime), new DateRenderer()); 35 | group.RegisterRenderer(typeof(DateTimeOffset), new DateRenderer()); 36 | group.Listener = ErrorManager.DefaultErrorListener; 37 | 38 | WriteObject(groupInfo); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PSStringTemplate/PSObjectAdaptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Management.Automation; 4 | using Antlr4.StringTemplate; 5 | using Antlr4.StringTemplate.Misc; 6 | 7 | namespace PSStringTemplate 8 | { 9 | /// 10 | /// Provides property binding for objects. 11 | /// 12 | public class PSObjectAdaptor : ObjectModelAdaptor 13 | { 14 | /// 15 | /// Gets the value of a property if it exists. 16 | /// 17 | /// The current interpreter passed by Antlr. 18 | /// The current frame passed by Antlr. 19 | /// The target of the property binding 20 | /// The property passed by Antlr. 21 | /// The target property name. 22 | /// The value of the property if it exists, otherwise . 23 | public override object GetProperty( 24 | Interpreter interpreter, 25 | TemplateFrame frame, 26 | object obj, 27 | object property, 28 | string propertyName) 29 | { 30 | var psObject = obj as PSObject; 31 | if (psObject == null) 32 | { 33 | return base.GetProperty(interpreter, frame, obj, property, propertyName); 34 | } 35 | 36 | // Check for static property matches if we're processing a type, 37 | // continue to instance properties if binding fails. 38 | if (psObject.BaseObject is Type type) 39 | { 40 | var typeResult = TypeAdapter.GetProperty(type, propertyName); 41 | if (typeResult != null) 42 | { 43 | return typeResult; 44 | } 45 | } 46 | 47 | var result = psObject.Properties.FirstOrDefault(p => p.Name == propertyName); 48 | 49 | if (result != null) 50 | { 51 | return AdapterUtil.NullIfEmpty(result.Value); 52 | } 53 | 54 | var method = psObject.Methods.FirstOrDefault( 55 | m => 56 | { 57 | return 58 | m.Name == string.Concat("Get", propertyName) && 59 | m.OverloadDefinitions.FirstOrDefault().Contains(@"()") && 60 | !m.OverloadDefinitions.FirstOrDefault().Contains("void"); 61 | }); 62 | 63 | return AdapterUtil.NullIfEmpty(method?.Invoke()); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PSStringTemplate/PSStringTemplate.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net452;netcoreapp2.0 5 | true 6 | PSStringTemplate.ruleset 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 6.0.0-beta.5 15 | 16 | 17 | 18 | true 19 | portable 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | true 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | false 37 | 38 | 39 | CoreCLR 40 | true 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/PSStringTemplate/PSStringTemplate.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/PSStringTemplate/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Resources; 4 | using System.Runtime.InteropServices; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyCopyright("Copyright © 2017 Patrick Meinecke")] 10 | [assembly: AssemblyTrademark("")] 11 | [assembly: AssemblyCulture("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("7ee9a67a-f825-4977-b13a-7d0b9adc69a2")] 20 | 21 | // Version information for an assembly consists of the following four values: 22 | // 23 | // Major Version 24 | // Minor Version 25 | // Build Number 26 | // Revision 27 | // 28 | // You can specify all the values or you can default the Build and Revision Numbers 29 | // by using the '*' as shown below: 30 | [assembly: NeutralResourcesLanguage("en-US")] 31 | [assembly: CLSCompliant(false)] 32 | -------------------------------------------------------------------------------- /src/PSStringTemplate/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace PSStringTemplate.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSStringTemplate.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Unable to compile template. Engine message: {0}. 65 | /// 66 | internal static string CompiletimeExceptionWrapper { 67 | get { 68 | return ResourceManager.GetString("CompiletimeExceptionWrapper", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Missing argument '{0}' in template '{1}', skipping.. 74 | /// 75 | internal static string DebugArgumentNotFound { 76 | get { 77 | return ResourceManager.GetString("DebugArgumentNotFound", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to No matching attribute found for parameter '{0}' in template '{1}'.. 83 | /// 84 | internal static string DebugAttributeNotFound { 85 | get { 86 | return ResourceManager.GetString("DebugAttributeNotFound", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to An exception was received while invoking method '{0}' for attribute '{1}'.. 92 | /// 93 | internal static string DebugInvokePropertyLikeMethodException { 94 | get { 95 | return ResourceManager.GetString("DebugInvokePropertyLikeMethodException", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Could not find property '{0}' while processing template '{1}'.. 101 | /// 102 | internal static string DebugPropertyNotFound { 103 | get { 104 | return ResourceManager.GetString("DebugPropertyNotFound", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to anonymous. 110 | /// 111 | internal static string DefaultTemplateName { 112 | get { 113 | return ResourceManager.GetString("DefaultTemplateName", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to An exception occurred while processing template "{0}". Engine message: {1}. 119 | /// 120 | internal static string RuntimeExceptionWrapper { 121 | get { 122 | return ResourceManager.GetString("RuntimeExceptionWrapper", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Cannot find template '{0}'. (Found: {1}). 128 | /// 129 | internal static string TemplateNotFound { 130 | get { 131 | return ResourceManager.GetString("TemplateNotFound", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Unhandled exception from StringTemplate, please file an issue on GitHub. Engine message: {0}. 137 | /// 138 | internal static string UnhandledErrorMessage { 139 | get { 140 | return ResourceManager.GetString("UnhandledErrorMessage", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to Added parameter '{0}' to template '{1}'.. 146 | /// 147 | internal static string VerboseAddedParameter { 148 | get { 149 | return ResourceManager.GetString("VerboseAddedParameter", resourceCulture); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/PSStringTemplate/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Unable to compile template. Engine message: {0} 122 | 123 | 124 | Missing argument '{0}' in template '{1}', skipping. 125 | 126 | 127 | No matching attribute found for parameter '{0}' in template '{1}'. 128 | 129 | 130 | An exception was received while invoking method '{0}' for attribute '{1}'. 131 | 132 | 133 | Could not find property '{0}' while processing template '{1}'. 134 | 135 | 136 | anonymous 137 | 138 | 139 | An exception occurred while processing template "{0}". Engine message: {1} 140 | 141 | 142 | Cannot find template '{0}'. (Found: {1}) 143 | 144 | 145 | Unhandled exception from StringTemplate, please file an issue on GitHub. Engine message: {0} 146 | 147 | 148 | Added parameter '{0}' to template '{1}'. 149 | 150 | -------------------------------------------------------------------------------- /src/PSStringTemplate/TemplateGroupInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.ObjectModel; 3 | using System.Management.Automation; 4 | using Antlr4.StringTemplate; 5 | using Antlr4.StringTemplate.Misc; 6 | 7 | using Strings = PSStringTemplate.Properties.Resources; 8 | 9 | namespace PSStringTemplate 10 | { 11 | /// 12 | /// Displays object information in a simpler format for use in PowerShell. 13 | /// 14 | public class TemplateGroupInfo 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The internal group instance. 20 | internal TemplateGroupInfo(TemplateGroup templateGroupInstance) 21 | { 22 | Instance = templateGroupInstance; 23 | var templates = new Collection(); 24 | var names = templateGroupInstance.GetTemplateNames(); 25 | foreach (var name in names) 26 | { 27 | templates.Add(new TemplateInfo(Instance.GetInstanceOf(name), this)); 28 | } 29 | 30 | Templates = new ReadOnlyCollection(templates); 31 | } 32 | 33 | /// 34 | /// Gets the objects that belong to this group, wrapped in 35 | /// objects. 36 | /// 37 | public ReadOnlyCollection Templates { get; } 38 | 39 | /// 40 | /// Gets the base that this object wraps. 41 | /// 42 | internal TemplateGroup Instance { get; } 43 | 44 | /// 45 | /// Create a from a template definition string. 46 | /// 47 | /// The error listener attached to the currently running cmdlet. 48 | /// The template source to use. 49 | /// The defined template. 50 | internal static TemplateGroupInfo CreateFromTemplateDefinition( 51 | Cmdlet context, 52 | string templateDefinition) 53 | { 54 | var group = new TemplateGroupString(string.Empty); 55 | Bind(group, context); 56 | group.DefineTemplate(Strings.DefaultTemplateName, templateDefinition); 57 | group.GetInstanceOf(Strings.DefaultTemplateName).impl.HasFormalArgs = false; 58 | return new TemplateGroupInfo(group); 59 | } 60 | 61 | /// 62 | /// Create a template group from a string group definition. 63 | /// 64 | /// The context to throw from. 65 | /// The string group definition. 66 | /// The compiled template group. 67 | internal static TemplateGroupInfo CreateFromGroupDefinition( 68 | Cmdlet context, 69 | string groupDefinition) 70 | { 71 | var group = new TemplateGroupString(groupDefinition); 72 | Bind(group, context); 73 | return new TemplateGroupInfo(group); 74 | } 75 | 76 | /// 77 | /// Attaches a template group to a running . 78 | /// 79 | /// The instance to attach to. 80 | internal void Bind(Cmdlet context) 81 | { 82 | Bind(Instance, context); 83 | } 84 | 85 | /// 86 | /// Removes the attached from a template group. 87 | /// 88 | internal void Unbind() 89 | { 90 | Unbind(Instance); 91 | } 92 | 93 | /// 94 | /// Attaches a template group to a running . 95 | /// 96 | /// The to attach. 97 | /// The instance to attach to. 98 | private static void Bind(TemplateGroup group, Cmdlet context) 99 | { 100 | group.Listener = new ErrorListener(context); 101 | group.RegisterModelAdaptor(typeof(PSObject), new PSObjectAdaptor()); 102 | group.RegisterModelAdaptor(typeof(Type), new TypeAdapter()); 103 | group.RegisterRenderer(typeof(DateTime), new DateRenderer()); 104 | group.RegisterRenderer(typeof(DateTimeOffset), new DateRenderer()); 105 | } 106 | 107 | private static void Unbind(TemplateGroup group) 108 | { 109 | group.Listener = ErrorManager.DefaultErrorListener; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/PSStringTemplate/TemplateInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Text.RegularExpressions; 5 | using Antlr.Runtime; 6 | using Antlr4.StringTemplate; 7 | 8 | namespace PSStringTemplate 9 | { 10 | /// 11 | /// Displays information in a simpler format for use in PowerShell. 12 | /// 13 | public class TemplateInfo 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The internal object. 19 | /// The group the template belongs to. 20 | internal TemplateInfo(Template templateInstance, TemplateGroupInfo groupInfo) 21 | { 22 | Instance = templateInstance; 23 | Group = groupInfo; 24 | } 25 | 26 | /// 27 | /// Gets the parameters defined in the wrapped . 28 | /// 29 | public IEnumerable Parameters 30 | { 31 | get 32 | { 33 | var tokens = this.Instance.impl.Tokens as BufferedTokenStream; 34 | return new ReadOnlyCollection( 35 | tokens 36 | ?.GetTokens() 37 | .Where(t => t.Type == 25) 38 | .Select(t => t.Text) 39 | .ToList() 40 | ?? new List()); 41 | } 42 | } 43 | 44 | /// 45 | /// Gets the name of the wrapped . 46 | /// 47 | public string Name => Regex.Replace(Instance.Name, @"^/", string.Empty); 48 | 49 | /// 50 | /// Gets the raw source string. 51 | /// 52 | public string TemplateSource => Instance.impl.TemplateSource; 53 | 54 | /// 55 | /// Gets the this template belongs to wrapped in a 56 | /// object. 57 | /// 58 | public TemplateGroupInfo Group { get; } 59 | 60 | /// 61 | /// Gets the base object. 62 | /// 63 | internal Template Instance { get; private set; } 64 | 65 | /// 66 | /// Recompile the wrapped to clear arguments. 67 | /// 68 | internal void ResetInstance() 69 | { 70 | var newTemplate = Group.Instance.GetInstanceOf(Name); 71 | Instance = newTemplate; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/PSStringTemplate/TypeAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Antlr4.StringTemplate; 4 | using Antlr4.StringTemplate.Misc; 5 | 6 | namespace PSStringTemplate 7 | { 8 | /// 9 | /// Provides static property binding for objects. 10 | /// 11 | public class TypeAdapter : ObjectModelAdaptor 12 | { 13 | /// 14 | /// Gets the value of a property if it exists. 15 | /// 16 | /// The current interpreter passed by Antlr. 17 | /// The current frame passed by Antlr. 18 | /// The target of the property binding 19 | /// The property passed by Antlr. 20 | /// The target property name. 21 | /// The value of the property if it exists, otherwise . 22 | public override object GetProperty( 23 | Interpreter interpreter, 24 | TemplateFrame frame, 25 | object o, 26 | object property, 27 | string propertyName) 28 | { 29 | return GetProperty(o as Type, propertyName) ?? 30 | base.GetProperty(interpreter, frame, o, property, propertyName); 31 | } 32 | 33 | /// 34 | /// Gets the value of a static property. 35 | /// 36 | /// The with the target property. 37 | /// The name of the property to retrieve. 38 | /// The value if the property exists, otherwise . 39 | internal static object GetProperty( 40 | Type type, 41 | string propertyName) 42 | { 43 | if (type == null) 44 | { 45 | return null; 46 | } 47 | 48 | PropertyInfo typeProp = null; 49 | try 50 | { 51 | typeProp = type 52 | .GetProperty( 53 | propertyName, 54 | BindingFlags.Static | BindingFlags.Public); 55 | } 56 | catch (AmbiguousMatchException) 57 | { 58 | // Treat ambiguous matches as if the property wasn't found 59 | } 60 | 61 | return AdapterUtil.NullIfEmpty(typeProp?.GetValue(null)); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/PSStringTemplate/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "indentationSize": 4, 6 | "tabSize": 4, 7 | "useTabs": false 8 | }, 9 | "orderingRules": { 10 | "elementOrder": [ 11 | "kind", 12 | "accessibility", 13 | "constant", 14 | "static", 15 | "readonly" 16 | ], 17 | "systemUsingDirectivesFirst": true, 18 | "usingDirectivesPlacement": "outsideNamespace", 19 | "blankLinesBetweenUsingGroups": "allow" 20 | }, 21 | "layoutRules": { 22 | "newlineAtEndOfFile": "require" 23 | }, 24 | "documentationRules": { 25 | "xmlHeader": false, 26 | "documentationCulture": "en-US", 27 | "documentExposedElements": true, 28 | "documentInternalElements": true 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Invoke-StringTemplate.Tests.ps1: -------------------------------------------------------------------------------- 1 | if (-not (Get-Module PSStringTemplate -ea 0)) { 2 | Import-Module $PSScriptRoot\..\Release\PSStringTemplate\*\PSStringTemplate.psd1 -Force 3 | } 4 | 5 | Describe 'Template invocation tests' { 6 | It 'can invoke a parameterless template' { 7 | Invoke-StringTemplate -Definition 'This is a pointless template.' | 8 | Should -Be 'This is a pointless template.' 9 | } 10 | It 'can map object properties as arguments' { 11 | Get-Command Get-ChildItem | 12 | Invoke-StringTemplate -Definition ' - ' | 13 | Should -Be 'Get-ChildItem - Cmdlet' 14 | } 15 | It 'can map many objects to a single template' { 16 | $result = (Get-Item ..\)[0] | 17 | Get-Member -MemberType Method | 18 | Where-Object Name -Match '^Enumerate' | 19 | Invoke-StringTemplate -Definition ' - - ' 20 | $result -join [Environment]::NewLine | Should -Be ( 21 | 'System.IO.DirectoryInfo - EnumerateDirectories - Method', 22 | 'System.IO.DirectoryInfo - EnumerateFiles - Method', 23 | 'System.IO.DirectoryInfo - EnumerateFileSystemInfos - Method' -join 24 | [Environment]::NewLine) 25 | } 26 | It 'can invoke multiple templates from the pipeline' { 27 | $group = New-StringTemplateGroup -Definition ' 28 | template1(Name) ::= "" 29 | template2(CommandType) ::= ""' 30 | 31 | $group.Templates | 32 | Invoke-StringTemplate (Get-Command Get-ChildItem) | 33 | Should -Be 'Get-ChildItem','Cmdlet' 34 | } 35 | It 'can invoke one template from a group from the pipeline' { 36 | $group = New-StringTemplateGroup -Definition ' 37 | template1(Name) ::= "" 38 | template2(CommandType) ::= ""' 39 | $group | 40 | Invoke-StringTemplate -Name template1 (Get-Command Get-ChildItem) | 41 | Should -Be 'Get-ChildItem' 42 | } 43 | It 'can map a dictionary as parameters' { 44 | Invoke-StringTemplate -Definition ', ' @{ One = '1'; Two = '2' } | 45 | Should -Be '1, 2' 46 | } 47 | It 'can map a "property-like" method as a parameter' { 48 | Invoke-StringTemplate -Definition '' ([scriptblock].GetMethod('Create')) | 49 | Should -Be 'System.String script' 50 | } 51 | It 'cannot map a method that does not start with Get' { 52 | $cmdlet = New-Object PSStringTemplate.NewStringTemplateGroupCommand -Property @{ Definition = 'Test' } 53 | Invoke-StringTemplate -Definition '' $cmdlet | Should -BeNullOrEmpty 54 | } 55 | It 'cannot map accessors as parameters' { 56 | $cmdlet = New-Object PSStringTemplate.NewStringTemplateGroupCommand -Property @{ Definition = 'Test' } 57 | Invoke-StringTemplate -Definition '<_Definition>' $cmdlet | Should -BeNullOrEmpty 58 | } 59 | It 'can map static properties when input is a type' { 60 | [DateTime] | 61 | Invoke-StringTemplate -Definition '' | 62 | Should -Not -BeNullOrEmpty 63 | } 64 | It 'adds instance properties of RuntimeType as well when bound by input' { 65 | [DateTime] | 66 | Invoke-StringTemplate -Definition '' | 67 | Should -Be True 68 | } 69 | It 'can map static properties using the type adapter' { 70 | Invoke-StringTemplate -Definition '' @{ 71 | r = [runspace] 72 | } | Should -Be 'Opened' 73 | } 74 | It 'adds instance properties as well as static using the adapter' { 75 | Invoke-StringTemplate -Definition '' @{ r = [runspace] } | 76 | Should -Be True 77 | } 78 | It 'can format date using the date renderer' { 79 | Invoke-StringTemplate -Definition '' @{ d = [DateTime]'1/1/90' } | 80 | Should -Be '1990.01.01' 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/New-StringTemplateGroup.Tests.ps1: -------------------------------------------------------------------------------- 1 | if (-not (Get-Module PSStringTemplate -ea 0)) { 2 | Import-Module $PSScriptRoot\..\Release\PSStringTemplate\*\PSStringTemplate.psd1 -Force 3 | } 4 | 5 | Describe 'Template Group Tests' { 6 | It 'can create a basic group' { 7 | $result = New-StringTemplateGroup -Definition 'template() ::= "Template"' 8 | $result.Templates.Name | Should -Be 'template' 9 | } 10 | It 'can create a group with attributes' { 11 | $result = New-StringTemplateGroup -Definition 'template(One, Two) ::= ": "' 12 | $result.Templates.Name | Should -Be 'template' 13 | $result.Templates.Parameters | Should -Be 'One','Two' 14 | } 15 | It 'can create multiple templates' { 16 | $result = New-StringTemplateGroup -Definition ' 17 | template1(One,Two) ::= ": " 18 | template2(Three) ::= "" 19 | ' 20 | $result.Templates.Name | Should -Be 'template1','template2' 21 | $result.Templates.Parameters | Should -Be 'One','Two','Three' 22 | } 23 | It 'can create multi-line templates' { 24 | $result = New-StringTemplateGroup -Definition ' 25 | template1() ::= << 26 | This is the first line 27 | This is the second 28 | >>' 29 | $result | Invoke-StringTemplate | Should -Be ( 30 | ' This is the first line', 31 | ' This is the second' -join [Environment]::NewLine) 32 | } 33 | } 34 | Describe 'Group exception handling tests' { 35 | # Verifies the parse message displays the correct code, highlights the right 36 | # character, and relays the correct message from the engine. 37 | function ShouldThrowParse { 38 | [CmdletBinding()] 39 | param( 40 | [string]$ContextMessage, 41 | [string]$Body, 42 | [single]$Offset = 0, 43 | [Parameter(ValueFromPipeline)][scriptblock]$InputObject 44 | ) 45 | process { 46 | $exceptionString = '(' +[regex]::Escape($ContextMessage) + ')' + 47 | ".*\+\s{$($ContextMessage.Length + $Offset)}~" 48 | if ($Body) { 49 | $exceptionString += '.*{0}' -f $Body 50 | } 51 | 52 | try { 53 | $null = & $InputObject 54 | } catch { 55 | $actualMessage = $_.Exception.Message 56 | } 57 | if ($actualMessage) { 58 | $actualMessage | Should -Match ('(?s)' + $exceptionString) 59 | } 60 | } 61 | } 62 | It 'displays TemplateGroupCompiletimeMessage' { 63 | $definition = 'a()::=' 64 | { New-StringTemplateGroup -Definition $definition } | 65 | ShouldThrowParse $definition -Body 'missing template at ''\' -Offset 1 66 | } 67 | It 'displays TemplateLexerMessage' { 68 | $definition = 'a(x)::= ""' 69 | { New-StringTemplateGroup -Definition $definition } | 70 | ShouldThrowParse '" 75 | b(x)::= "" 76 | c(x)::= "' 77 | { New-StringTemplateGroup -Definition $definition } | 78 | ShouldThrowParse '' -Body 'mismatched input '';'' expecting ID' -Offset -1 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/PSStringTemplate.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Manifest Validation' { 2 | $script:manifestPath = Resolve-Path "$PSScriptRoot\..\Release\PSStringTemplate\*\PSStringTemplate.psd1" 3 | It 'Passes Test-ModuleManifest' { 4 | # Ignore errors because FileList will be different if built in non-Windows platforms. 5 | $script:manifest = Test-ModuleManifest -Path $script:manifestPath -WarningAction 0 -ea 0 6 | } 7 | It 'Has the correct properties' { 8 | $manifest = $script:manifest 9 | $manifest.Name | Should -Be 'PSStringTemplate' 10 | $manifest.Guid | Should -Be 'f188d0cf-291f-41a1-ae0e-c770d980cf6e' 11 | $manifest.RootModule | Should -Be '.\PSStringTemplate.psm1' 12 | $manifest.PowerShellVersion | Should -Be '3.0' 13 | $manifest.DotNetFrameworkVersion | Should -Be '4.5' 14 | } 15 | } 16 | 17 | if (-not (Get-Module PSStringTemplate -ea 0)) { 18 | Import-Module $PSScriptRoot\..\Release\PSStringTemplate\*\PSStringTemplate.psd1 -Force 19 | } 20 | 21 | Describe 'Readme examples work as is' { 22 | It 'Anonymous template with dictionary parameters' { 23 | Invoke-StringTemplate -Definition ' is very !' -Parameters @{ 24 | language = 'PowerShell' 25 | adjective = 'cool' 26 | } | Should -Be 'PowerShell is very cool!' 27 | } 28 | It 'Anonymous template with object as parameters' { 29 | $definition = 'Name: <\n>Commands: ' 30 | 31 | $module = Get-Module -ListAvailable Microsoft.PowerShell.Host 32 | $result = Invoke-StringTemplate -Definition $definition -Parameters $module 33 | 34 | # Can't directly compare because the commands come out in a different order. 35 | $result.StartsWith('Name: Microsoft.PowerShell.Host') | Should -Be $true 36 | $module[0].ExportedCommands | 37 | ForEach-Object { $result -match $_.ToString() } 38 | } 39 | # TODO: Fix this test so it can work in environments where the members may vary. 40 | It 'TemplateGroup definition' -Skip { 41 | $definition = @' 42 | Param(parameter) ::= "[] $" 43 | Method(member) ::= << 44 | [] static () { 45 | throw [NotImplementedException]::new() 46 | } 47 | >> 48 | Class(Name, DeclaredMethods) ::= << 49 | class MyClass : { 50 | 51 | } 52 | >> 53 | '@ 54 | $group = New-StringTemplateGroup -Definition $definition 55 | $group | Invoke-StringTemplate -Name Class -Parameters ([System.Runtime.InteropServices.ICustomMarshaler]) | 56 | Should -Be 'class MyClass : ICustomMarshaler { 57 | [Object] MarshalNativeToManaged ([IntPtr] $pNativeData) { 58 | throw [NotImplementedException]::new() 59 | } 60 | 61 | [IntPtr] MarshalManagedToNative ([Object] $ManagedObj) { 62 | throw [NotImplementedException]::new() 63 | } 64 | 65 | [Void] CleanUpNativeData ([IntPtr] $pNativeData) { 66 | throw [NotImplementedException]::new() 67 | } 68 | 69 | [Void] CleanUpManagedData ([Object] $ManagedObj) { 70 | throw [NotImplementedException]::new() 71 | } 72 | 73 | [Int32] GetNativeDataSize () { 74 | throw [NotImplementedException]::new() 75 | } 76 | }' 77 | } 78 | It 'New-StringTemplateGroup example' { 79 | $group = New-StringTemplateGroup -Definition @' 80 | 81 | memberTemplate(Name, Parameters, ReturnType) ::= << 82 | () 83 | >> 84 | 85 | paramTemplate(param) ::= "$" 86 | '@ 87 | $group | Invoke-StringTemplate -Name memberTemplate ([string].GetProperty('Length')) | 88 | Should -Be 'Length' 89 | $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('Clone')) | 90 | Should -Be 'Clone()' 91 | $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('IsNullOrWhiteSpace')) | 92 | Should -Be 'IsNullOrWhiteSpace($value)' 93 | $group | Invoke-StringTemplate -Name memberTemplate ([string].GetMethod('Insert')) | 94 | Should -Be 'Insert($startIndex, $value)' 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tools/GetDotNet.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [string] $Version = '2.0.2', 4 | 5 | [switch] $Unix 6 | ) 7 | end { 8 | $TARGET_FOLDER = "$PSScriptRoot/dotnet" 9 | $TARGET_COMMAND = 'dotnet.exe' 10 | if ($Unix.IsPresent) { 11 | $TARGET_COMMAND = 'dotnet' 12 | } 13 | 14 | if (($dotnet = Get-Command dotnet -ea 0) -and (& $dotnet --version) -eq $Version) { 15 | return $dotnet 16 | } 17 | 18 | 19 | if ($dotnet = Get-Command $TARGET_FOLDER/$TARGET_COMMAND -ea 0) { 20 | if (($found = & $dotnet --version) -eq $Version) { 21 | return $dotnet 22 | } 23 | Write-Host -ForegroundColor Yellow Found dotnet $found but require $Version, replacing... 24 | Remove-Item $TARGET_FOLDER -Recurse 25 | $dotnet = $null 26 | } 27 | 28 | Write-Host -ForegroundColor Green Downloading dotnet version $Version 29 | 30 | if ($Unix.IsPresent) { 31 | $uri = "https://raw.githubusercontent.com/dotnet/cli/v2.0.0/scripts/obtain/dotnet-install.sh" 32 | $installerPath = [System.IO.Path]::GetTempPath() + 'dotnet-install.sh' 33 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri) 34 | Set-Content $installerPath -Value $scriptText -Encoding UTF8 35 | $installer = { param($Version, $InstallDir) & (Get-Command bash) $installerPath -Version $Version -InstallDir $InstallDir } 36 | } else { 37 | $uri = "https://raw.githubusercontent.com/dotnet/cli/v2.0.0/scripts/obtain/dotnet-install.ps1" 38 | $scriptText = [System.Net.WebClient]::new().DownloadString($uri) 39 | 40 | # Stop the official script from hard exiting at times... 41 | $safeScriptText = $scriptText -replace 'exit 0', 'return' 42 | $installer = [scriptblock]::Create($safeScriptText) 43 | } 44 | 45 | $null = & $installer -Version $Version -InstallDir $TARGET_FOLDER 46 | 47 | return Get-Command $TARGET_FOLDER/$TARGET_COMMAND 48 | } 49 | -------------------------------------------------------------------------------- /tools/GetOpenCover.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [string] $Version = '4.6.519' 4 | ) 5 | end { 6 | function GetVersionNumber { 7 | param([System.Management.Automation.CommandInfo] $Command) 8 | end { 9 | return (& $Command -version) ` 10 | -replace 'OpenCover version ' ` 11 | -replace '\.0$' 12 | } 13 | } 14 | 15 | $TARGET_FOLDER = "$PSScriptRoot\opencover" 16 | $TARGET_ARCHIVE = "$PSScriptRoot\opencover.zip" 17 | $TARGET_NAME = 'OpenCover.Console.exe' 18 | 19 | $ErrorActionPreference = 'Stop' 20 | 21 | if ($openCover = Get-Command $TARGET_FOLDER\$TARGET_NAME -ea 0) { 22 | if (($found = GetVersionNumber $openCover) -eq $Version) { 23 | return $openCover 24 | } 25 | 26 | Write-Host -ForegroundColor Yellow Found OpenCover $found but require $Version, replacing... 27 | Remove-Item $TARGET_FOLDER -Recurse 28 | } 29 | Write-Host -ForegroundColor Green Downloading OpenCover version $Version 30 | 31 | $url = "https://github.com/OpenCover/opencover/releases/download/$Version/opencover.$Version.zip" 32 | Invoke-WebRequest $url -OutFile $TARGET_ARCHIVE 33 | 34 | Expand-Archive $TARGET_ARCHIVE -DestinationPath $TARGET_FOLDER -Force 35 | Remove-Item $TARGET_ARCHIVE 36 | 37 | return Get-Command $TARGET_FOLDER\$TARGET_NAME 38 | 39 | } 40 | -------------------------------------------------------------------------------- /tools/InvokeCircleCI.ps1: -------------------------------------------------------------------------------- 1 | Install-Module Pester -RequiredVersion 4.0.6 -Scope CurrentUser -Force 2 | Install-Module InvokeBuild -RequiredVersion 3.2.1 -Scope CurrentUser -Force 3 | Install-Module platyPS -RequiredVersion 0.8.1 -Scope CurrentUser -Force 4 | 5 | Import-Module Pester 2> $null 6 | Import-Module InvokeBuild, platyPS 7 | 8 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 'true' 9 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 'true' 10 | 11 | Invoke-Build -File $PSScriptRoot/../PSStringTemplate.build.ps1 -Configuration Release -Task Prerelease 12 | 13 | $resultsFile = "$PSScriptRoot/../testresults/pester.xml" 14 | 15 | $passed = (Test-Path $resultsFile) -and 0 -eq ([int]([xml](Get-Content $resultsFile -Raw)).'test-results'.failures) 16 | 17 | 18 | if (-not $passed) { 19 | $Error | Format-List * -Force 20 | exit 1 21 | } 22 | --------------------------------------------------------------------------------