├── PSPrettifier.ps1.psm1 ├── PSPrettifier.Piecemeal.ps1 ├── CHANGELOG.md ├── docs ├── CHANGELOG.md ├── Pop-Indent.md ├── README.md ├── HangLongAssignments-Prettifier.md ├── IndentGroups-Prettifier.md ├── Push-Indent.md ├── PSPrettify.md ├── Expand-ScriptBlock.md └── Get-Prettifier.md ├── PSPrettifier.psm1 ├── PSPrettifier.tests.ps1 ├── PSPrettifier.GitHubWorkflow.PSDevOps.ps1 ├── Prettifiers ├── README.ps1.md ├── README.md ├── HangLongAssignments.prettify.ps1 └── IndentGroups.prettify.ps1 ├── Github └── Jobs │ └── BuildPSPrettifier.psd1 ├── Formatting └── Prettifier.format.ps1 ├── PSPrettifier.HelpOut.ps1 ├── README.ps1.md ├── PSPrettifier.psd1 ├── LICENSE ├── README.md ├── PSPrettifier.ezout.ps1 ├── Push-Indent.ps1 ├── Pop-Indent.ps1 ├── Expand-ScriptBlock.ps1 ├── PSPrettifier.format.ps1xml ├── .github └── workflows │ └── TestAndPublish.yml └── Get-Prettifier.ps1 /PSPrettifier.ps1.psm1: -------------------------------------------------------------------------------- 1 | [Include('*-*')]$psScriptRoot 2 | -------------------------------------------------------------------------------- /PSPrettifier.Piecemeal.ps1: -------------------------------------------------------------------------------- 1 | #require -Module Piecemeal 2 | Push-Location $PSScriptRoot 3 | 4 | Install-Piecemeal -ExtensionNoun 'Prettifier' -ExtensionPattern '\.(?>prettify|prettifier)\.ps1$'-ExtensionTypeName 'Prettifier' -OutputPath '.\Get-Prettifier.ps1' 5 | 6 | Pop-Location 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1: 2 | * Initial Release of PSPrettifier 3 | * Get-Prettifier will list prettifiers (Fixes #1) 4 | * Expand-ScriptBlock (aka PSPrettify) will prettify a scriptblock (Fixes #2) 5 | * Groups will be indented (Fixes #3) 6 | * Long Assignments should hang (Fixes #4) 7 | --- 8 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1: 2 | * Initial Release of PSPrettifier 3 | * Get-Prettifier will list prettifiers (Fixes #1) 4 | * Expand-ScriptBlock (aka PSPrettify) will prettify a scriptblock (Fixes #2) 5 | * Groups will be indented (Fixes #3) 6 | * Long Assignments should hang (Fixes #4) 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /PSPrettifier.psm1: -------------------------------------------------------------------------------- 1 | foreach ($file in (Get-ChildItem -Path "$psScriptRoot" -Filter "*-*" -Recurse)) { 2 | if ($file.Extension -ne '.ps1') { continue } # Skip if the extension is not .ps1 3 | if ($file.Name -match '\.[^\.]+\.ps1$') { continue } # Skip if the file is an unrelated file. 4 | . $file.FullName 5 | } 6 | 7 | -------------------------------------------------------------------------------- /PSPrettifier.tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module Pester 2 | 3 | describe PSPrettifier { 4 | it 'Makes PowerShell code a bit more pretty' { 5 | (Expand-ScriptBlock -ScriptBlock { 6 | "This is far too indented" 7 | }).ToString() | Should -Be ' 8 | "This is far too indented" 9 | ' 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PSPrettifier.GitHubWorkflow.PSDevOps.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module PSDevOps 2 | Import-BuildStep -ModuleName PSPrettifier 3 | New-GitHubWorkflow -Name "Analyze, Test, Tag, and Publish" -On Push, PullRequest, Demand -Job PowerShellStaticAnalysis, TestPowerShellOnLinux, TagReleaseAndPublish, BuildPSPrettifier -Environment @{ 4 | NoCoverage = $true 5 | }| 6 | Set-Content .\.github\workflows\TestAndPublish.yml -Encoding UTF8 -PassThru 7 | 8 | -------------------------------------------------------------------------------- /Prettifiers/README.ps1.md: -------------------------------------------------------------------------------- 1 | This directory contains Prettifiers. 2 | 3 | You can list prettifiers using: 4 | 5 | ~~~PowerShell 6 | Get-Prettifier 7 | ~~~ 8 | 9 | ~~~PipeScript{ 10 | Import-Module ../PSPrettifier.psd1 11 | 12 | [PSCustomObject]@{ 13 | Table = Get-Prettifier | 14 | .Name { 15 | "[$_.DisplayName]($($_.Name))" 16 | } .Synopsis 17 | } 18 | } 19 | ~~~ 20 | 21 | 22 | -------------------------------------------------------------------------------- /Github/Jobs/BuildPSPrettifier.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | "runs-on" = "ubuntu-latest" 3 | if = '${{ success() }}' 4 | steps = @( 5 | @{ 6 | name = 'Check out repository' 7 | uses = 'actions/checkout@v2' 8 | }, 9 | @{ 10 | name = 'Use Piecemeal' 11 | uses = 'StartAutomating/Piecemeal@main' 12 | id = 'Piecemeal' 13 | }, 14 | 'RunPipeScript', 15 | 'RunEZOut', 16 | 'RunHelpOut' 17 | ) 18 | } -------------------------------------------------------------------------------- /Prettifiers/README.md: -------------------------------------------------------------------------------- 1 | This directory contains Prettifiers. 2 | 3 | You can list prettifiers using: 4 | 5 | ~~~PowerShell 6 | Get-Prettifier 7 | ~~~ 8 | 9 | 10 | |Name |Synopsis | 11 | |--------------------------------------------------------------------------------|---------------------------------| 12 | |[HangLongAssignments.prettify.ps1.DisplayName](HangLongAssignments.prettify.ps1)|Hangs long assignment statements.| 13 | |[IndentGroups.prettify.ps1.DisplayName](IndentGroups.prettify.ps1) |Indents Groups | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Formatting/Prettifier.format.ps1: -------------------------------------------------------------------------------- 1 | Write-FormatView -TypeName Prettifier -Property DisplayName, Synopsis, Parameters -VirtualProperty @{ 2 | Parameters = { 3 | @(foreach ($kv in ([Management.Automation.CommandMetaData]$_).Parameters.GetEnumerator()) { 4 | @( 5 | Format-RichText -ForegroundColor Verbose -InputObject "[$($kv.Value.ParameterType)]" 6 | Format-RichText -ForegroundColor Warning -InputObject "`$$($kv.Key)" 7 | ) -join '' 8 | }) -join [Environment]::NewLine 9 | } 10 | Extends = { 11 | $_.Extends -join [Environment]::NewLine 12 | } 13 | } -Wrap -ColorProperty @{ 14 | "DisplayName" = {"Success"} 15 | } 16 | -------------------------------------------------------------------------------- /PSPrettifier.HelpOut.ps1: -------------------------------------------------------------------------------- 1 | #require -Module HelpOut 2 | Push-Location $PSScriptRoot 3 | $parentPath = $PSScriptRoot | Split-Path 4 | 5 | $PSPrettifierLoaded = Get-Module PSPrettifier 6 | if (-not $PSPrettifierLoaded) { 7 | $PSPrettifierLoaded = Get-ChildItem -Recurse -Filter "*.psd1" | Where-Object Name -like 'PSPrettifier*' | Import-Module -Name { $_.FullName } -Force -PassThru 8 | } 9 | if ($PSPrettifierLoaded) { 10 | "::notice title=ModuleLoaded::PSPrettifier Loaded" | Out-Host 11 | } else { 12 | "::error:: PSPrettifier not loaded" |Out-Host 13 | } 14 | if ($PSPrettifierLoaded) { 15 | Save-MarkdownHelp -Module $PSPrettifierLoaded.Name -ScriptPath Prettifiers -ReplaceScriptName '\.(?>pretty|prettify|prettifier)\.ps1$','^Prettifiers[\\/]' -ReplaceScriptNameWith "-Prettifier" -PassThru 16 | } 17 | 18 | Pop-Location -------------------------------------------------------------------------------- /README.ps1.md: -------------------------------------------------------------------------------- 1 | # PSPrettifier 2 | 3 | PSPrettifier is an extensible prettifier for PowerShell. 4 | 5 | You can make any script a little bit nicer by using PSPrettifier: 6 | 7 | ~~~PowerShell 8 | Install-Module PSPrettifier -Scope CurrentUser 9 | Import-Module PSPrettifier 10 | PSPrettify { 11 | "this script is a little more indented than it needs to be" 12 | if ($thereIsABlock) { 13 | "it should also be indented" 14 | } 15 | } 16 | ~~~ 17 | 18 | 19 | Prettification is done using a series of Prettifier scripts. 20 | 21 | You can list prettifiers using: 22 | 23 | ~~~PowerShell 24 | Get-Prettifier 25 | ~~~ 26 | 27 | ~~~PipeScript{ 28 | $psPrettifier = Import-Module ./PSPrettifier.psd1 29 | 30 | [PSCustomObject]@{ 31 | Table = Get-Prettifier | 32 | .Name { 33 | "[$_.DisplayName](Prettifiers/$($_.Name))" 34 | } .Synopsis 35 | } 36 | } 37 | ~~~ 38 | 39 | 40 | -------------------------------------------------------------------------------- /PSPrettifier.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Description = "Prettify your PowerShell" 3 | RootModule = 'PSPrettifier.psm1' 4 | ModuleVersion = '0.1' 5 | Guid = 'd7939a09-a7ab-4657-969c-71f715e052fe' 6 | Copyright = '2022 Start-Automating' 7 | Author = 'James Brundage' 8 | CompanyName = 'Start-Automating' 9 | FormatsToProcess = 'PSPrettifier.format.ps1xml' 10 | PrivateData = @{ 11 | PSData = @{ 12 | ProjectURI = 'https://github.com/StartAutomating/PSPrettifier' 13 | LicenseURI = 'https://github.com/StartAutomating/PSPrettifier/blob/main/LICENSE' 14 | 15 | Tags = 'Prettifier', 'PipeScript' 16 | ReleaseNotes = @' 17 | ## 0.1: 18 | * Initial Release of PSPrettifier 19 | * Get-Prettifier will list prettifiers (Fixes #1) 20 | * Expand-ScriptBlock (aka PSPrettify) will prettify a scriptblock (Fixes #2) 21 | * Groups will be indented (Fixes #3) 22 | * Long Assignments should hang (Fixes #4) 23 | --- 24 | '@ 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/Pop-Indent.md: -------------------------------------------------------------------------------- 1 | 2 | Pop-Indent 3 | ---------- 4 | ### Synopsis 5 | Pops text indentation. 6 | 7 | --- 8 | ### Description 9 | 10 | Pops text indentation. This removes indentation from text, based off of the first indented line. 11 | 12 | --- 13 | ### Examples 14 | #### EXAMPLE 1 15 | ```PowerShell 16 | Pop-Indent ' 17 | pop indent will trim initial indentation 18 | ' 19 | ``` 20 | 21 | --- 22 | ### Parameters 23 | #### **Text** 24 | 25 | The text to outdent 26 | 27 | 28 | 29 | > **Type**: ```[String]``` 30 | 31 | > **Required**: false 32 | 33 | > **Position**: 1 34 | 35 | > **PipelineInput**:false 36 | 37 | 38 | 39 | --- 40 | ### Syntax 41 | ```PowerShell 42 | Pop-Indent [[-Text] <String>] [<CommonParameters>] 43 | ``` 44 | --- 45 | ### Notes 46 | If the text has multiple lines, the first non-whitespace line will determine how much indentation is removed. 47 | 48 | This way, if there is nested indentation within the text, it will be unaffected. 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 James Brundage 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PSPrettifier 2 | 3 | PSPrettifier is an extensible prettifier for PowerShell. 4 | 5 | You can make any script a little bit nicer by using PSPrettifier: 6 | 7 | ~~~PowerShell 8 | Install-Module PSPrettifier -Scope CurrentUser 9 | Import-Module PSPrettifier 10 | PSPrettify { 11 | "this script is a little more indented than it needs to be" 12 | if ($thereIsABlock) { 13 | "it should also be indented" 14 | } 15 | } 16 | ~~~ 17 | 18 | 19 | Prettification is done using a series of Prettifier scripts. 20 | 21 | You can list prettifiers using: 22 | 23 | ~~~PowerShell 24 | Get-Prettifier 25 | ~~~ 26 | 27 | 28 | |Name |Synopsis | 29 | |--------------------------------------------------------------------------------------------|---------------------------------| 30 | |[HangLongAssignments.prettify.ps1.DisplayName](Prettifiers/HangLongAssignments.prettify.ps1)|Hangs long assignment statements.| 31 | |[IndentGroups.prettify.ps1.DisplayName](Prettifiers/IndentGroups.prettify.ps1) |Indents Groups | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PSPrettifier 2 | 3 | PSPrettifier is an extensible prettifier for PowerShell. 4 | 5 | You can make any script a little bit nicer by using PSPrettifier: 6 | 7 | ~~~PowerShell 8 | Install-Module PSPrettifier -Scope CurrentUser 9 | Import-Module PSPrettifier 10 | PSPrettify { 11 | "this script is a little more indented than it needs to be" 12 | if ($thereIsABlock) { 13 | "it should also be indented" 14 | } 15 | } 16 | ~~~ 17 | 18 | 19 | Prettification is done using a series of Prettifier scripts. 20 | 21 | You can list prettifiers using: 22 | 23 | ~~~PowerShell 24 | Get-Prettifier 25 | ~~~ 26 | 27 | 28 | |Name |Synopsis | 29 | |--------------------------------------------------------------------------------------------|---------------------------------| 30 | |[HangLongAssignments.prettify.ps1.DisplayName](Prettifiers/HangLongAssignments.prettify.ps1)|Hangs long assignment statements.| 31 | |[IndentGroups.prettify.ps1.DisplayName](Prettifiers/IndentGroups.prettify.ps1) |Indents Groups | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /PSPrettifier.ezout.ps1: -------------------------------------------------------------------------------- 1 | #requires -Module EZOut 2 | # Install-Module EZOut or https://github.com/StartAutomating/EZOut 3 | $myFile = $MyInvocation.MyCommand.ScriptBlock.File 4 | $myModuleName = 'PSPrettifier' 5 | $myRoot = $myFile | Split-Path 6 | Push-Location $myRoot 7 | $formatting = @( 8 | # Add your own Write-FormatView here, 9 | # or put them in a Formatting or Views directory 10 | foreach ($potentialDirectory in 'Formatting','Views') { 11 | Join-Path $myRoot $potentialDirectory | 12 | Get-ChildItem -ea ignore | 13 | Import-FormatView -FilePath {$_.Fullname} 14 | } 15 | ) 16 | 17 | $destinationRoot = $myRoot 18 | 19 | if ($formatting) { 20 | $myFormatFile = Join-Path $destinationRoot "$myModuleName.format.ps1xml" 21 | $formatting | Out-FormatData -Module $MyModuleName | Set-Content $myFormatFile -Encoding UTF8 22 | Get-Item $myFormatFile 23 | } 24 | 25 | $types = @( 26 | # Add your own Write-TypeView statements here 27 | # or declare them in the 'Types' directory 28 | Join-Path $myRoot Types | 29 | Get-Item -ea ignore | 30 | Import-TypeView 31 | 32 | ) 33 | 34 | if ($types) { 35 | $myTypesFile = Join-Path $destinationRoot "$myModuleName.types.ps1xml" 36 | $types | Out-TypeData | Set-Content $myTypesFile -Encoding UTF8 37 | Get-Item $myTypesFile 38 | } 39 | Pop-Location 40 | -------------------------------------------------------------------------------- /docs/HangLongAssignments-Prettifier.md: -------------------------------------------------------------------------------- 1 | 2 | Prettifiers/HangLongAssignments.prettify.ps1 3 | -------------------------------------------- 4 | ### Synopsis 5 | Hangs long assignment statements. 6 | 7 | --- 8 | ### Description 9 | 10 | Hangs long assignment statements. 11 | 12 | Any assignment statement whose 13 | 14 | --- 15 | ### Examples 16 | #### EXAMPLE 1 17 | ```PowerShell 18 | Expand-ScriptBlock { 19 | $x = "ThisIsASequenceOfStuff", 20 | "ThatTakesUpLotsOfSpace", 21 | "SoTheAssignmentShouldBeHanging" 22 | } 23 | ``` 24 | 25 | --- 26 | ### Parameters 27 | #### **ScriptBlock** 28 | 29 | The `[ScriptBlock]` to Prettify. 30 | 31 | 32 | 33 | > **Type**: ```[ScriptBlock]``` 34 | 35 | > **Required**: true 36 | 37 | > **Position**: 1 38 | 39 | > **PipelineInput**:false 40 | 41 | 42 | 43 | --- 44 | #### **MaximumAssignmentLength** 45 | 46 | The threshold of what makes an assignment 'too long'. 47 | By default, 60 characters. 48 | 49 | 50 | 51 | > **Type**: ```[Int32]``` 52 | 53 | > **Required**: false 54 | 55 | > **Position**: 2 56 | 57 | > **PipelineInput**:false 58 | 59 | 60 | 61 | --- 62 | #### **Indent** 63 | 64 | The indentation level. 65 | 66 | 67 | 68 | > **Type**: ```[Int32]``` 69 | 70 | > **Required**: false 71 | 72 | > **Position**: 3 73 | 74 | > **PipelineInput**:false 75 | 76 | 77 | 78 | --- 79 | ### Syntax 80 | ```PowerShell 81 | Prettifiers/HangLongAssignments.prettify.ps1 [-ScriptBlock] <ScriptBlock> [[-MaximumAssignmentLength] <Int32>] [[-Indent] <Int32>] [<CommonParameters>] 82 | ``` 83 | --- 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /docs/IndentGroups-Prettifier.md: -------------------------------------------------------------------------------- 1 | 2 | Prettifiers/IndentGroups.prettify.ps1 3 | ------------------------------------- 4 | ### Synopsis 5 | Indents Groups 6 | 7 | --- 8 | ### Description 9 | 10 | Prettifies a ScriptBlock by indenting the groups within it. 11 | 12 | --- 13 | ### Examples 14 | #### EXAMPLE 1 15 | ```PowerShell 16 | PSPrettify { 17 | these lines are indented more than they need to be. 18 | if (there was a statement) { 19 | it should be indented 20 | } 21 | } 22 | ``` 23 | 24 | --- 25 | ### Parameters 26 | #### **ScriptBlock** 27 | 28 | The [ScriptBlock] to prettify 29 | 30 | 31 | 32 | > **Type**: ```[ScriptBlock]``` 33 | 34 | > **Required**: true 35 | 36 | > **Position**: 1 37 | 38 | > **PipelineInput**:false 39 | 40 | 41 | 42 | --- 43 | #### **Indent** 44 | 45 | The amount of indentation to use for each group. By default, 4. 46 | 47 | 48 | 49 | > **Type**: ```[Int32]``` 50 | 51 | > **Required**: false 52 | 53 | > **Position**: 2 54 | 55 | > **PipelineInput**:false 56 | 57 | 58 | 59 | --- 60 | #### **Depth** 61 | 62 | The initial depth. By default, zero. 63 | 64 | 65 | 66 | > **Type**: ```[Int32]``` 67 | 68 | > **Required**: false 69 | 70 | > **Position**: 3 71 | 72 | > **PipelineInput**:false 73 | 74 | 75 | 76 | --- 77 | ### Syntax 78 | ```PowerShell 79 | Prettifiers/IndentGroups.prettify.ps1 [-ScriptBlock] <ScriptBlock> [[-Indent] <Int32>] [[-Depth] <Int32>] [<CommonParameters>] 80 | ``` 81 | --- 82 | ### Notes 83 | This Prettifier uses the `[PSParser]` 84 | 85 | 1. GroupStart/GroupEnd will be used to calculate depth. 86 | 2. Depth will be used to determine the desired indentation. 87 | 3. Each line between various points of depth should be indented to that point. 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /docs/Push-Indent.md: -------------------------------------------------------------------------------- 1 | 2 | Push-Indent 3 | ----------- 4 | ### Synopsis 5 | Pushes Indentation 6 | 7 | --- 8 | ### Description 9 | 10 | Pushes a block of text to a particular level of indentation. 11 | 12 | --- 13 | ### Examples 14 | #### EXAMPLE 1 15 | ```PowerShell 16 | Push-Indent -Indent 2 -Text { 17 | $a = $b 18 | } 19 | ``` 20 | 21 | --- 22 | ### Parameters 23 | #### **Text** 24 | 25 | The text to indent. 26 | 27 | 28 | 29 | > **Type**: ```[String]``` 30 | 31 | > **Required**: false 32 | 33 | > **Position**: 1 34 | 35 | > **PipelineInput**:false 36 | 37 | 38 | 39 | --- 40 | #### **Indent** 41 | 42 | The amount of indentation to apply to the text. 43 | 44 | 45 | 46 | > **Type**: ```[Int32]``` 47 | 48 | > **Required**: false 49 | 50 | > **Position**: 2 51 | 52 | > **PipelineInput**:false 53 | 54 | 55 | 56 | --- 57 | #### **HereDocStart** 58 | 59 | Many languages support heredocs or herestrings, which should not be indented. 60 | -HereDocStart describes the start of a herestring. 61 | By default, this will match PowerShell herestring starts. 62 | 63 | 64 | 65 | > **Type**: ```[Regex]``` 66 | 67 | > **Required**: false 68 | 69 | > **Position**: 3 70 | 71 | > **PipelineInput**:false 72 | 73 | 74 | 75 | --- 76 | #### **HereDocEnd** 77 | 78 | Many languages support heredocs or herestrings, which should not be indented. 79 | -HereDocEnd describes the start of a herestring. 80 | By default, this will match PowerShell herestring starts. 81 | 82 | 83 | 84 | > **Type**: ```[Regex]``` 85 | 86 | > **Required**: false 87 | 88 | > **Position**: 4 89 | 90 | > **PipelineInput**:false 91 | 92 | 93 | 94 | --- 95 | ### Syntax 96 | ```PowerShell 97 | Push-Indent [[-Text] <String>] [[-Indent] <Int32>] [[-HereDocStart] <Regex>] [[-HereDocEnd] <Regex>] [<CommonParameters>] 98 | ``` 99 | --- 100 | 101 | 102 | -------------------------------------------------------------------------------- /docs/PSPrettify.md: -------------------------------------------------------------------------------- 1 | 2 | Expand-ScriptBlock 3 | ------------------ 4 | ### Synopsis 5 | Prettifies a ScriptBlock. 6 | 7 | --- 8 | ### Description 9 | 10 | Expands a ScriptBlock into a prettified ScriptBlock. 11 | 12 | Prettification is accomplished using a series of prettifiers. 13 | 14 | --- 15 | ### Examples 16 | #### EXAMPLE 1 17 | ```PowerShell 18 | Expand-ScriptBlock -ScriptBlock { 19 | if (1) { 20 | if (2) { 21 | if (3) { 22 | ``` 23 | } 24 | } 25 | } 26 | } 27 | #### EXAMPLE 2 28 | ```PowerShell 29 | Expand-ScriptBlock -ScriptBlock { 30 | if (1) { 31 | $x = "ThisIsASequenceOfStuff", 32 | "ThatTakesUpLotsOfSpace", 33 | "SoTheAssignmentShouldBeHanging", $( 34 | if ($y) { 35 | "z" 36 | } 37 | ) 38 | } 39 | } 40 | ``` 41 | 42 | --- 43 | ### Parameters 44 | #### **ScriptBlock** 45 | 46 | The ScriptBlock that will be expanded 47 | 48 | 49 | 50 | > **Type**: ```[ScriptBlock]``` 51 | 52 | > **Required**: true 53 | 54 | > **Position**: 1 55 | 56 | > **PipelineInput**:true (ByValue, ByPropertyName) 57 | 58 | 59 | 60 | --- 61 | #### **Prettifier** 62 | 63 | A list of prettifiers. 64 | If not provided, all prettifiers will be run. 65 | 66 | 67 | 68 | > **Type**: ```[String[]]``` 69 | 70 | > **Required**: false 71 | 72 | > **Position**: 2 73 | 74 | > **PipelineInput**:false 75 | 76 | 77 | 78 | --- 79 | #### **Parameter** 80 | 81 | A collection of parameters. 82 | 83 | 84 | 85 | > **Type**: ```[IDictionary]``` 86 | 87 | > **Required**: false 88 | 89 | > **Position**: 3 90 | 91 | > **PipelineInput**:true (ByPropertyName) 92 | 93 | 94 | 95 | --- 96 | ### Syntax 97 | ```PowerShell 98 | Expand-ScriptBlock [-ScriptBlock] <ScriptBlock> [[-Prettifier] <String[]>] [[-Parameter] <IDictionary>] [<CommonParameters>] 99 | ``` 100 | --- 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/Expand-ScriptBlock.md: -------------------------------------------------------------------------------- 1 | 2 | Expand-ScriptBlock 3 | ------------------ 4 | ### Synopsis 5 | Prettifies a ScriptBlock. 6 | 7 | --- 8 | ### Description 9 | 10 | Expands a ScriptBlock into a prettified ScriptBlock. 11 | 12 | Prettification is accomplished using a series of prettifiers. 13 | 14 | --- 15 | ### Examples 16 | #### EXAMPLE 1 17 | ```PowerShell 18 | Expand-ScriptBlock -ScriptBlock { 19 | if (1) { 20 | if (2) { 21 | if (3) { 22 | ``` 23 | } 24 | } 25 | } 26 | } 27 | #### EXAMPLE 2 28 | ```PowerShell 29 | Expand-ScriptBlock -ScriptBlock { 30 | if (1) { 31 | $x = "ThisIsASequenceOfStuff", 32 | "ThatTakesUpLotsOfSpace", 33 | "SoTheAssignmentShouldBeHanging", $( 34 | if ($y) { 35 | "z" 36 | } 37 | ) 38 | } 39 | } 40 | ``` 41 | 42 | --- 43 | ### Parameters 44 | #### **ScriptBlock** 45 | 46 | The ScriptBlock that will be expanded 47 | 48 | 49 | 50 | > **Type**: ```[ScriptBlock]``` 51 | 52 | > **Required**: true 53 | 54 | > **Position**: 1 55 | 56 | > **PipelineInput**:true (ByValue, ByPropertyName) 57 | 58 | 59 | 60 | --- 61 | #### **Prettifier** 62 | 63 | A list of prettifiers. 64 | If not provided, all prettifiers will be run. 65 | 66 | 67 | 68 | > **Type**: ```[String[]]``` 69 | 70 | > **Required**: false 71 | 72 | > **Position**: 2 73 | 74 | > **PipelineInput**:false 75 | 76 | 77 | 78 | --- 79 | #### **Parameter** 80 | 81 | A collection of parameters. 82 | 83 | 84 | 85 | > **Type**: ```[IDictionary]``` 86 | 87 | > **Required**: false 88 | 89 | > **Position**: 3 90 | 91 | > **PipelineInput**:true (ByPropertyName) 92 | 93 | 94 | 95 | --- 96 | ### Syntax 97 | ```PowerShell 98 | Expand-ScriptBlock [-ScriptBlock] <ScriptBlock> [[-Prettifier] <String[]>] [[-Parameter] <IDictionary>] [<CommonParameters>] 99 | ``` 100 | --- 101 | 102 | 103 | -------------------------------------------------------------------------------- /Push-Indent.ps1: -------------------------------------------------------------------------------- 1 | function Push-Indent 2 | { 3 | <# 4 | .SYNOPSIS 5 | Pushes Indentation 6 | .DESCRIPTION 7 | Pushes a block of text to a particular level of indentation. 8 | .EXAMPLE 9 | Push-Indent -Indent 2 -Text { 10 | $a = $b 11 | } 12 | #> 13 | param( 14 | # The text to indent. 15 | [string] 16 | $Text, 17 | 18 | # The amount of indentation to apply to the text. 19 | [int] 20 | $Indent, 21 | 22 | # Many languages support heredocs or herestrings, which should not be indented. 23 | # -HereDocStart describes the start of a herestring. 24 | # By default, this will match PowerShell herestring starts. 25 | [Alias('HereStringStart')] 26 | [Regex] 27 | $HereDocStart = '@["'']$', 28 | 29 | # Many languages support heredocs or herestrings, which should not be indented. 30 | # -HereDocEnd describes the start of a herestring. 31 | # By default, this will match PowerShell herestring starts. 32 | [Alias('HereStringEnd')] 33 | [Regex] 34 | $HereDocEnd = '^["'']@' 35 | ) 36 | 37 | process { 38 | $poppedText = Pop-Indent -Text $text 39 | $text = $poppedText 40 | $textLines =@($text -split '[\r\n]+') 41 | if ($textLines.Length -eq 1) { 42 | return $text 43 | } 44 | 45 | $InHereDoc = $false 46 | $newText = 47 | @(foreach ($textLine in $textLines) { 48 | if (-not $InHereDoc) { 49 | if ($textLine -notmatch '^\s{0,}$') { 50 | (' ' * $Indent) + $textLine 51 | } else { 52 | '' 53 | } 54 | } else { 55 | $textLine 56 | } 57 | 58 | if ($textLine -match $HereDocStart) { 59 | $InHereDoc = $true 60 | } elseif ($textLine -match $HereDocEnd) { 61 | $InHereDoc = $false 62 | } 63 | }) -join [Environment]::NewLine 64 | 65 | 66 | $newText 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /Pop-Indent.ps1: -------------------------------------------------------------------------------- 1 | function Pop-Indent 2 | { 3 | <# 4 | .SYNOPSIS 5 | Pops text indentation. 6 | .DESCRIPTION 7 | Pops text indentation. This removes indentation from text, based off of the first indented line. 8 | .NOTES 9 | If the text has multiple lines, the first non-whitespace line will determine how much indentation is removed. 10 | 11 | This way, if there is nested indentation within the text, it will be unaffected. 12 | .EXAMPLE 13 | Pop-Indent ' 14 | pop indent will trim initial indentation 15 | ' 16 | #> 17 | param( 18 | # The text to outdent 19 | [string] 20 | $Text 21 | ) 22 | 23 | process { 24 | # Split the text into lines 25 | $textLines =@($text -split '(?>\r\n|\n)') 26 | # If there was only one line, change nothing 27 | if ($textLines.Length -eq 1) { return $text } 28 | # Set the target indentation to zero. 29 | $targetIndent = 0 30 | 31 | 32 | $newlines = # Walk over each line and adjust it's space. 33 | foreach ($textLine in $textLines) { 34 | if ($textLine -match '^\s{0}$') { # If the line was only whitespace 35 | '' # return a blank line. 36 | } 37 | else { 38 | # If we have the target indentation, 39 | if ($targetIndent) { 40 | # remove that many leading whitespace characters. 41 | $textLine -replace "^\s{$targetIndent}" 42 | } 43 | # Otherwise 44 | else { 45 | $null = $textLine -match '^(?\s{0,})' 46 | # determine the target indentation. 47 | $targetIndent = $matches.i.Length 48 | # and replace all of the leading whitespacee. 49 | $textLine -replace '^\s{0,}' 50 | } 51 | } 52 | } 53 | 54 | # Join all of the new lines together and we have our outdented text. 55 | $newlines -join [Environment]::NewLine 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Expand-ScriptBlock.ps1: -------------------------------------------------------------------------------- 1 | function Expand-ScriptBlock { 2 | <# 3 | .Synopsis 4 | Prettifies a ScriptBlock. 5 | .Description 6 | Expands a ScriptBlock into a prettified ScriptBlock. 7 | 8 | Prettification is accomplished using a series of prettifiers. 9 | .EXAMPLE 10 | Expand-ScriptBlock -ScriptBlock { 11 | if (1) { 12 | if (2) { 13 | if (3) { 14 | 15 | } 16 | } 17 | } 18 | } 19 | .Example 20 | Expand-ScriptBlock -ScriptBlock { 21 | if (1) { 22 | $x = "ThisIsASequenceOfStuff", 23 | "ThatTakesUpLotsOfSpace", 24 | "SoTheAssignmentShouldBeHanging", $( 25 | if ($y) { 26 | "z" 27 | } 28 | ) 29 | } 30 | } 31 | #> 32 | [Alias('PSPrettify')] 33 | param( 34 | # The ScriptBlock that will be expanded 35 | [Parameter(Mandatory,ValueFromPipelineByPropertyName,ValueFromPipeline)] 36 | [ScriptBlock] 37 | $ScriptBlock, 38 | 39 | # A list of prettifiers. 40 | # If not provided, all prettifiers will be run. 41 | [Alias('Prettifiers')] 42 | [string[]] 43 | $Prettifier, 44 | 45 | # A collection of parameters. 46 | [Parameter(ValueFromPipelineByPropertyName)] 47 | [Collections.IDictionary] 48 | $Parameter = @{} 49 | ) 50 | 51 | begin { 52 | $prettifiers = 53 | if ($Prettifier) { 54 | Get-Prettifier -PrettifierName "(?>$($Prettifier -join '|')" -Match 55 | } else { 56 | Get-Prettifier 57 | } 58 | } 59 | 60 | process { 61 | $justScriptBlock = @{ScriptBlock=$ScriptBlock} 62 | $allParameters = $justScriptBlock + $Parameter 63 | 64 | foreach ($prettifyCommand in $prettifiers) { 65 | $allParameters['ScriptBlock'] = $ScriptBlock 66 | $couldRun = $prettifyCommand.CouldRun($allParameters) 67 | if (-not $couldRun.Count) { continue } 68 | $updatedScriptBlock = & $prettifyCommand @couldRun 69 | if ($updatedScriptBlock -is [ScriptBlock]) { 70 | $scriptBlock = $updatedScriptBlock 71 | } 72 | } 73 | 74 | $ScriptBlock 75 | } 76 | 77 | end { 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Prettifiers/HangLongAssignments.prettify.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Hangs long assignment statements. 4 | .DESCRIPTION 5 | Hangs long assignment statements. 6 | 7 | Any assignment statement whose 8 | .EXAMPLE 9 | Expand-ScriptBlock { 10 | $x = "ThisIsASequenceOfStuff", 11 | "ThatTakesUpLotsOfSpace", 12 | "SoTheAssignmentShouldBeHanging" 13 | } 14 | #> 15 | param( 16 | # The `[ScriptBlock]` to Prettify. 17 | [Parameter(Mandatory)] 18 | [scriptblock] 19 | $ScriptBlock, 20 | 21 | # The threshold of what makes an assignment 'too long'. 22 | # By default, 60 characters. 23 | [Alias('TooLong')] 24 | [int] 25 | $MaximumAssignmentLength = 60, 26 | 27 | # The indentation level. 28 | [int] 29 | $Indent = 4 30 | ) 31 | 32 | process { 33 | # Find all of the long assignments. 34 | $longAssignments = $ScriptBlock.Ast.FindAll({ 35 | param($ast) 36 | $ast -is [Management.Automation.Language.AssignmentStatementAst] -and 37 | $ast.Right.Extent.ToString().Length -gt $MaximumAssignmentLength 38 | }, $true) 39 | 40 | # Determine the start offset and stringify the script. 41 | $startOffset = $ScriptBlock.Ast.Extent.StartOffset 42 | $text = "$scriptBlock" 43 | $index = 0 44 | 45 | # To create the new script we need to walk over each long assignment. 46 | $newScript = 47 | @(foreach ($longAssignment in $longAssignments) { 48 | # and determine it's offset 49 | $assignmentOffset = $longAssignment.Extent.StartOffset - $startOffset 50 | # and indent. 51 | $longAssignmentIndent = [Regex]::Match($text.Substring(0, $assignmentOffset), 52 | "^(?\s{0,})", 'RightToLeft').Groups["i"].Length 53 | # If there's code between assignments 54 | if ($assignmentOffset -gt $index) { 55 | # include it as is 56 | $text.Substring($index, $assignmentOffset - $index - 1) 57 | } 58 | 59 | # Otherwise, indent the left side 60 | (Push-Indent -Text ($longAssignment.Left.Extent.ToString() + 61 | ' =' + 62 | [Environment]::NewLine # add a newline 63 | ) -Indent $longAssignmentIndent) + 64 | # and indent the right side by -Indent. 65 | (Push-Indent -Text $longAssignment.Right.Extent.ToString() -Indent ($longAssignmentIndent + $Indent)) 66 | 67 | $index = $longAssignment.Extent.EndOffset - $startOffset 68 | } 69 | 70 | # If there is any remaining content 71 | if ($index -lt $text.Length) { 72 | $text.Substring($index) # include it as-is. 73 | }) -join [Environment]::NewLine 74 | 75 | [ScriptBlock]::Create($newScript) 76 | } 77 | -------------------------------------------------------------------------------- /Prettifiers/IndentGroups.prettify.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Indents Groups 4 | .DESCRIPTION 5 | Prettifies a ScriptBlock by indenting the groups within it. 6 | 7 | .NOTES 8 | This Prettifier uses the `[PSParser]` 9 | 10 | 1. GroupStart/GroupEnd will be used to calculate depth. 11 | 2. Depth will be used to determine the desired indentation. 12 | 3. Each line between various points of depth should be indented to that point. 13 | .EXAMPLE 14 | PSPrettify { 15 | these lines are indented more than they need to be. 16 | if (there was a statement) { 17 | it should be indented 18 | } 19 | } 20 | 21 | #> 22 | param( 23 | # The [ScriptBlock] to prettify 24 | [Parameter(Mandatory)] 25 | [Scriptblock] 26 | $ScriptBlock, 27 | 28 | # The amount of indentation to use for each group. By default, 4. 29 | [int] 30 | $Indent = 4, 31 | 32 | # The initial depth. By default, zero. 33 | [int] 34 | $Depth = 0 35 | ) 36 | 37 | process { 38 | # Get all of the tokens in the script 39 | $tokens = [Management.Automation.PSParser]::Tokenize($ScriptBlock, [ref]$null) 40 | 41 | # Determine all of the group indeces 42 | $groupIndeces = 43 | @(for ($tn = 0 ; $tn -lt $tokens.Count; $tn++) { 44 | if ($tokens[$tn].Type -in 'GroupStart', 'GroupEnd') { 45 | $tn 46 | } 47 | elseif ($tokens[$tn].Type -eq 'Operator' -and 48 | $tokens[$tn].Content -in '[', ']') { 49 | $tn 50 | } 51 | }) 52 | 53 | # Create a new stringbuilder 54 | $stringBuilder = [Text.StringBuilder]::new() 55 | # and stringify the scriptblock 56 | $text = "$scriptBlock" 57 | $index = 0 58 | # Now, walk over each group index 59 | foreach ($groupIndex in $groupIndeces) { 60 | $groupToken = $tokens[$groupIndex] 61 | # Determine if it is a start or an end. 62 | $isGroupStart = 63 | $groupToken.Type -eq 'GroupStart' -or $groupToken.Content -eq '[' 64 | $isGroupEnd = 65 | $groupToken.Type -eq 'GroupEnd' -or $groupToken.Content -eq ']' 66 | 67 | # find all of the text between now and the last token 68 | $TextBetween = 69 | $text.Substring($index, $groupToken.Start - $index) 70 | 71 | # indent that text based off the current $depth 72 | $null = 73 | $stringBuilder.Append((Push-Indent -Text $TextBetween -Indent ($indent * $depth))) 74 | 75 | 76 | # If the group was at the start of the line 77 | $null = if ($tokens[$groupIndex - 1].Type -eq 'Newline') { 78 | if ($isGroupStart) { 79 | # indent group starts by the current depth 80 | $stringBuilder.Append(' ' * ($indent * $depth)) 81 | } elseif ($isGroupEnd) { 82 | # or indent group ends by the current minus one. 83 | $stringBuilder.Append(' ' * ($indent * ($depth - 1))) 84 | } 85 | } 86 | # add the grouping token. 87 | $null = $stringBuilder.Append($groupToken.Content) 88 | 89 | # If the token was a group start 90 | if ($isGroupStart) { 91 | $depth++ # increment depth 92 | } else { # otherwise 93 | $depth-- # decrement depth. 94 | } 95 | $index = $groupToken.Start + $groupToken.Length 96 | } 97 | 98 | # If there was any remaining text 99 | if ($index -lt $text.Length) { 100 | # include it with the current -Indent level. 101 | $TextBetween = 102 | $text.Substring($index) 103 | $null = 104 | $stringBuilder.Append((Push-Indent -Text $TextBetween -Indent ($indent * $depth))) 105 | } 106 | 107 | [ScriptBlock]::Create("$stringBuilder") 108 | } 109 | -------------------------------------------------------------------------------- /docs/Get-Prettifier.md: -------------------------------------------------------------------------------- 1 | 2 | Get-Prettifier 3 | -------------- 4 | ### Synopsis 5 | Gets Extensions 6 | 7 | --- 8 | ### Description 9 | 10 | Gets Extensions. 11 | 12 | Prettifier can be found in: 13 | 14 | * Any module that includes -PrettifierModuleName in it's tags. 15 | * The directory specified in -PrettifierPath 16 | 17 | --- 18 | ### Examples 19 | #### EXAMPLE 1 20 | ```PowerShell 21 | Get-Prettifier 22 | ``` 23 | 24 | --- 25 | ### Parameters 26 | #### **PrettifierPath** 27 | 28 | If provided, will look beneath a specific path for extensions. 29 | 30 | 31 | 32 | > **Type**: ```[String]``` 33 | 34 | > **Required**: false 35 | 36 | > **Position**: 1 37 | 38 | > **PipelineInput**:true (ByPropertyName) 39 | 40 | 41 | 42 | --- 43 | #### **Force** 44 | 45 | If set, will clear caches of extensions, forcing a refresh. 46 | 47 | 48 | 49 | > **Type**: ```[Switch]``` 50 | 51 | > **Required**: false 52 | 53 | > **Position**: named 54 | 55 | > **PipelineInput**:false 56 | 57 | 58 | 59 | --- 60 | #### **CommandName** 61 | 62 | If provided, will get Prettifier that extend a given command 63 | 64 | 65 | 66 | > **Type**: ```[String[]]``` 67 | 68 | > **Required**: false 69 | 70 | > **Position**: 2 71 | 72 | > **PipelineInput**:true (ByPropertyName) 73 | 74 | 75 | 76 | --- 77 | #### **PrettifierName** 78 | 79 | The name of an extension. 80 | By default, this will match any extension command whose name, displayname, or aliases exactly match the name. 81 | 82 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 83 | 84 | 85 | 86 | > **Type**: ```[String[]]``` 87 | 88 | > **Required**: false 89 | 90 | > **Position**: 3 91 | 92 | > **PipelineInput**:true (ByPropertyName) 93 | 94 | 95 | 96 | --- 97 | #### **Like** 98 | 99 | If provided, will treat -PrettifierName as a wildcard. 100 | This will return any extension whose name, displayname, or aliases are like the -PrettifierName. 101 | 102 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 103 | 104 | 105 | 106 | > **Type**: ```[Switch]``` 107 | 108 | > **Required**: false 109 | 110 | > **Position**: named 111 | 112 | > **PipelineInput**:true (ByPropertyName) 113 | 114 | 115 | 116 | --- 117 | #### **Match** 118 | 119 | If provided, will treat -PrettifierName as a regular expression. 120 | This will return any extension whose name, displayname, or aliases match the -PrettifierName. 121 | 122 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 123 | 124 | 125 | 126 | > **Type**: ```[Switch]``` 127 | 128 | > **Required**: false 129 | 130 | > **Position**: named 131 | 132 | > **PipelineInput**:true (ByPropertyName) 133 | 134 | 135 | 136 | --- 137 | #### **DynamicParameter** 138 | 139 | If set, will return the dynamic parameters object of all the Prettifier for a given command. 140 | 141 | 142 | 143 | > **Type**: ```[Switch]``` 144 | 145 | > **Required**: false 146 | 147 | > **Position**: named 148 | 149 | > **PipelineInput**:true (ByPropertyName) 150 | 151 | 152 | 153 | --- 154 | #### **CouldRun** 155 | 156 | If set, will return if the extension could run. 157 | 158 | 159 | 160 | > **Type**: ```[Switch]``` 161 | 162 | > **Required**: false 163 | 164 | > **Position**: named 165 | 166 | > **PipelineInput**:true (ByPropertyName) 167 | 168 | 169 | 170 | --- 171 | #### **CouldPipe** 172 | 173 | If set, will return if the extension could accept this input from the pipeline. 174 | 175 | 176 | 177 | > **Type**: ```[PSObject]``` 178 | 179 | > **Required**: false 180 | 181 | > **Position**: 4 182 | 183 | > **PipelineInput**:false 184 | 185 | 186 | 187 | --- 188 | #### **Run** 189 | 190 | If set, will run the extension. If -Stream is passed, results will be directly returned. 191 | By default, extension results are wrapped in a return object. 192 | 193 | 194 | 195 | > **Type**: ```[Switch]``` 196 | 197 | > **Required**: false 198 | 199 | > **Position**: named 200 | 201 | > **PipelineInput**:true (ByPropertyName) 202 | 203 | 204 | 205 | --- 206 | #### **Stream** 207 | 208 | If set, will stream output from running the extension. 209 | By default, extension results are wrapped in a return object. 210 | 211 | 212 | 213 | > **Type**: ```[Switch]``` 214 | 215 | > **Required**: false 216 | 217 | > **Position**: named 218 | 219 | > **PipelineInput**:true (ByPropertyName) 220 | 221 | 222 | 223 | --- 224 | #### **DynamicParameterSetName** 225 | 226 | If set, will return the dynamic parameters of all Prettifier for a given command, using the provided DynamicParameterSetName. 227 | Implies -DynamicParameter. 228 | 229 | 230 | 231 | > **Type**: ```[String]``` 232 | 233 | > **Required**: false 234 | 235 | > **Position**: 5 236 | 237 | > **PipelineInput**:true (ByPropertyName) 238 | 239 | 240 | 241 | --- 242 | #### **DynamicParameterPositionOffset** 243 | 244 | If provided, will return the dynamic parameters of all Prettifier for a given command, with all positional parameters offset. 245 | Implies -DynamicParameter. 246 | 247 | 248 | 249 | > **Type**: ```[Int32]``` 250 | 251 | > **Required**: false 252 | 253 | > **Position**: 6 254 | 255 | > **PipelineInput**:true (ByPropertyName) 256 | 257 | 258 | 259 | --- 260 | #### **NoMandatoryDynamicParameter** 261 | 262 | If set, will return the dynamic parameters of all Prettifier for a given command, with all mandatory parameters marked as optional. 263 | Implies -DynamicParameter. Does not actually prevent the parameter from being Mandatory on the Extension. 264 | 265 | 266 | 267 | > **Type**: ```[Switch]``` 268 | 269 | > **Required**: false 270 | 271 | > **Position**: named 272 | 273 | > **PipelineInput**:true (ByPropertyName) 274 | 275 | 276 | 277 | --- 278 | #### **RequirePrettifierAttribute** 279 | 280 | If set, will require a [Runtime.CompilerServices.Extension()] attribute to be considered an extension. 281 | 282 | 283 | 284 | > **Type**: ```[Switch]``` 285 | 286 | > **Required**: false 287 | 288 | > **Position**: named 289 | 290 | > **PipelineInput**:true (ByPropertyName) 291 | 292 | 293 | 294 | --- 295 | #### **ValidateInput** 296 | 297 | If set, will validate this input against [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. 298 | 299 | 300 | 301 | > **Type**: ```[PSObject]``` 302 | 303 | > **Required**: false 304 | 305 | > **Position**: 7 306 | 307 | > **PipelineInput**:true (ByPropertyName) 308 | 309 | 310 | 311 | --- 312 | #### **AllValid** 313 | 314 | If set, will validate this input against all [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. 315 | By default, if any validation attribute returned true, the extension is considered validated. 316 | 317 | 318 | 319 | > **Type**: ```[Switch]``` 320 | 321 | > **Required**: false 322 | 323 | > **Position**: named 324 | 325 | > **PipelineInput**:false 326 | 327 | 328 | 329 | --- 330 | #### **ParameterSetName** 331 | 332 | The name of the parameter set. This is used by -CouldRun and -Run to enforce a single specific parameter set. 333 | 334 | 335 | 336 | > **Type**: ```[String]``` 337 | 338 | > **Required**: false 339 | 340 | > **Position**: 8 341 | 342 | > **PipelineInput**:true (ByPropertyName) 343 | 344 | 345 | 346 | --- 347 | #### **Parameter** 348 | 349 | The parameters to the extension. Only used when determining if the extension -CouldRun. 350 | 351 | 352 | 353 | > **Type**: ```[IDictionary]``` 354 | 355 | > **Required**: false 356 | 357 | > **Position**: 9 358 | 359 | > **PipelineInput**:true (ByPropertyName) 360 | 361 | 362 | 363 | --- 364 | #### **SteppablePipeline** 365 | 366 | If set, will output a steppable pipeline for the extension. 367 | Steppable pipelines allow you to control how begin, process, and end are executed in an extension. 368 | This allows for the execution of more than one extension at a time. 369 | 370 | 371 | 372 | > **Type**: ```[Switch]``` 373 | 374 | > **Required**: false 375 | 376 | > **Position**: named 377 | 378 | > **PipelineInput**:false 379 | 380 | 381 | 382 | --- 383 | #### **Help** 384 | 385 | If set, will output the help for the extensions 386 | 387 | 388 | 389 | > **Type**: ```[Switch]``` 390 | 391 | > **Required**: false 392 | 393 | > **Position**: named 394 | 395 | > **PipelineInput**:false 396 | 397 | 398 | 399 | --- 400 | ### Outputs 401 | Extension 402 | 403 | 404 | --- 405 | ### Syntax 406 | ```PowerShell 407 | Get-Prettifier [[-PrettifierPath] <String>] [-Force] [[-CommandName] <String[]>] [[-PrettifierName] <String[]>] [-Like] [-Match] [-DynamicParameter] [-CouldRun] [[-CouldPipe] <PSObject>] [-Run] [-Stream] [[-DynamicParameterSetName] <String>] [[-DynamicParameterPositionOffset] <Int32>] [-NoMandatoryDynamicParameter] [-RequirePrettifierAttribute] [[-ValidateInput] <PSObject>] [-AllValid] [[-ParameterSetName] <String>] [[-Parameter] <IDictionary>] [-SteppablePipeline] [-Help] [<CommonParameters>] 408 | ``` 409 | --- 410 | 411 | 412 | -------------------------------------------------------------------------------- /PSPrettifier.format.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prettifier 7 | 8 | Prettifier 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | $moduleName = 'PSPrettifier' 28 | 29 | do { 30 | $lm = Get-Module -Name $moduleName -ErrorAction Ignore 31 | if (-not $lm) { continue } 32 | if ($lm.FormatPartsLoaded) { break } 33 | $wholeScript = @(foreach ($formatFilePath in $lm.exportedFormatFiles) { 34 | foreach ($partNodeName in Select-Xml -LiteralPath $formatFilePath -XPath "/Configuration/Controls/Control/Name[starts-with(., '$')]") { 35 | $ParentNode = $partNodeName.Node.ParentNode 36 | "$($ParentNode.Name)={ 37 | $($ParentNode.CustomControl.CustomEntries.CustomEntry.CustomItem.ExpressionBinding.ScriptBlock)}" 38 | } 39 | }) -join [Environment]::NewLine 40 | New-Module -Name "${ModuleName}.format.ps1xml" -ScriptBlock ([ScriptBlock]::Create(($wholeScript + ';Export-ModuleMember -Variable *'))) | 41 | Import-Module -Global 42 | $onRemove = [ScriptBlock]::Create("Remove-Module '${ModuleName}.format.ps1xml'") 43 | 44 | if (-not $lm.OnRemove) { 45 | $lm.OnRemove = $onRemove 46 | } else { 47 | $lm.OnRemove = [ScriptBlock]::Create($onRemove.ToString() + '' + [Environment]::NewLine + $lm.OnRemove) 48 | } 49 | $lm | Add-Member NoteProperty FormatPartsLoaded $true -Force 50 | 51 | } while ($false) 52 | 53 | 54 | 55 | $__ = $_ 56 | $ci = . {"Success"} 57 | $_ = $__ 58 | if ($ci -is [string]) { 59 | $ci = & ${PSPrettifier_Format-RichText} -NoClear -ForegroundColor $ci 60 | } else { 61 | $ci = & ${PSPrettifier_Format-RichText} -NoClear @ci 62 | } 63 | $output = . {$_.'DisplayName'} 64 | @($ci; $output; & ${PSPrettifier_Format-RichText}) -join "" 65 | 66 | 67 | 68 | Synopsis 69 | 70 | 71 | 72 | @(foreach ($kv in ([Management.Automation.CommandMetaData]$_).Parameters.GetEnumerator()) { 73 | @( 74 | & ${PSPrettifier_Format-RichText} -ForegroundColor Verbose -InputObject "[$($kv.Value.ParameterType)]" 75 | & ${PSPrettifier_Format-RichText} -ForegroundColor Warning -InputObject "`$$($kv.Key)" 76 | ) -join '' 77 | }) -join [Environment]::NewLine 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ${PSPrettifier_Format-RichText} 89 | 90 | 91 | 92 | 93 | 94 | 95 | <# 96 | .Synopsis 97 | Formats the text color of output 98 | .Description 99 | Formats the text color of output 100 | 101 | * ForegroundColor 102 | * BackgroundColor 103 | * Bold 104 | * Underline 105 | .Notes 106 | Stylized Output works in two contexts at present: 107 | * Rich consoles (Windows Terminal, PowerShell.exe, Pwsh.exe) (when $host.UI.SupportsVirtualTerminal) 108 | * Web pages (Based off the presence of a $Request variable, or when $host.UI.SupportsHTML (you must add this property to $host.UI)) 109 | #> 110 | [Management.Automation.Cmdlet("Format","Object")] 111 | [ValidateScript({ 112 | $canUseANSI = $host.UI.SupportsVirtualTerminal 113 | $canUseHTML = $Request -or $host.UI.SupportsHTML -or $OutputMode -eq 'HTML' 114 | if (-not ($canUseANSI -or $canUseHTML)) { return $false} 115 | return $true 116 | })] 117 | param( 118 | # The input object 119 | [Parameter(ValueFromPipeline)] 120 | [PSObject] 121 | $InputObject, 122 | 123 | # The foreground color 124 | [string]$ForegroundColor, 125 | 126 | # The background color 127 | [string]$BackgroundColor, 128 | 129 | # If set, will render as bold 130 | [switch]$Bold, 131 | 132 | # If set, will render as italic. 133 | [switch]$Italic, 134 | 135 | # If set, will render as faint 136 | [switch]$Faint, 137 | 138 | # If set, will render as hidden text. 139 | [switch]$Hide, 140 | 141 | # If set, will render as blinking (not supported in all terminals or HTML) 142 | [switch]$Blink, 143 | 144 | # If set, will render as strikethru 145 | [Alias('Strikethrough', 'Crossout')] 146 | [switch]$Strikethru, 147 | 148 | # If set, will underline text 149 | [switch]$Underline, 150 | 151 | # If set, will double underline text. 152 | [switch]$DoubleUnderline, 153 | 154 | # If set, will invert text 155 | [switch]$Invert, 156 | # If set, will not clear formatting 157 | [switch]$NoClear 158 | ) 159 | 160 | begin { 161 | $canUseANSI = $host.UI.SupportsVirtualTerminal 162 | $canUseHTML = $Request -or $host.UI.SupportsHTML -or $OutputMode -eq 'HTML' 163 | $knownStreams = @{ 164 | Output='';Error='BrightRed';Warning='BrightYellow'; 165 | Verbose='BrightCyan';Debug='Yellow';Progress='Cyan'; 166 | Success='BrightGreen';Failure='Red';Default=''} 167 | $esc = [char]0x1b 168 | $standardColors = 'Black', 'Red', 'Green', 'Yellow', 'Blue','Magenta', 'Cyan', 'White' 169 | $brightColors = 'BrightBlack', 'BrightRed', 'BrightGreen', 'BrightYellow', 'BrightBlue','BrightMagenta', 'BrightCyan', 'BrightWhite' 170 | 171 | $n =0 172 | $cssClasses = @() 173 | $styleAttributes = 174 | @(:nextColor foreach ($hc in $ForegroundColor,$BackgroundColor) { 175 | $n++ 176 | if (-not $hc) { continue } 177 | if ($hc[0] -eq $esc) { 178 | if ($canUseANSI) { 179 | $hc; continue 180 | } 181 | } 182 | 183 | $ansiStartPoint = if ($n -eq 1) { 30 } else { 40 } 184 | if ($knownStreams.ContainsKey($hc)) { 185 | $i = $brightColors.IndexOf($knownStreams[$hc]) 186 | if ($canUseHTML) { 187 | $cssClasses += $hc 188 | } else { 189 | if ($i -ge 0 -and $canUseANSI) { 190 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 191 | } else { 192 | $i = $standardColors.IndexOf($knownStreams[$hc]) 193 | if ($i -ge 0 -and $canUseANSI) { 194 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 195 | } elseif ($i -le 0 -and $canUseANSI) { 196 | '' + $esc + "[$($ansistartpoint + 8):5m" 197 | } 198 | } 199 | } 200 | continue nextColor 201 | } 202 | elseif ($standardColors -contains $hc) { 203 | for ($i = 0; $i -lt $standardColors.Count;$i++) { 204 | if ($standardColors[$i] -eq $hc) { 205 | if ($canUseANSI -and -not $canUseHTML) { 206 | '' + $esc + "[$($ansiStartPoint + $i)m" 207 | } else { 208 | $cssClasses += $standardColors[$i] 209 | } 210 | continue nextColor 211 | } 212 | } 213 | } elseif ($brightColors -contains $hc) { 214 | for ($i = 0; $i -lt $brightColors.Count;$i++) { 215 | if ($brightColors[$i] -eq $hc) { 216 | if ($canUseANSI -and -not $canUseHTML) { 217 | '' + $esc + "[1;$($ansiStartPoint + $i)m" 218 | } else { 219 | $cssClasses += $standardColors[$i] 220 | } 221 | continue nextColor 222 | } 223 | } 224 | } 225 | elseif ($psStyle -and $psStyle.Formatting.$hc -and 226 | $psStyle.Formatting.$hc -match '^\e') { 227 | if ($canUseANSI -and -not $canUseHTML) { 228 | $psStyle.Formatting.$hc 229 | } else { 230 | $cssClasses += "formatting-$hc" 231 | } 232 | } 233 | elseif (-not $n -and $psStyle -and $psStyle.Foreground.$hc -and 234 | $psStyle.Foreground.$hc -match '^\e' ) { 235 | if ($canUseANSI -and -not $canUseHTML) { 236 | $psStyle.Foreground.$hc 237 | } else { 238 | $cssClasses += "foreground-$hc" 239 | } 240 | } 241 | elseif ($n -and $psStyle -and $psStyle.Background.$hc -and 242 | $psStyle.Background.$hc -match '^\e') { 243 | if ($canUseANSI -and -not $canUseHTML) { 244 | $psStyle.Background.$hc 245 | } else { 246 | $cssClasses += "background-$hc" 247 | } 248 | } 249 | 250 | 251 | 252 | if ($hc -and $hc -notmatch '^[\#\e]') { 253 | $placesToLook= 254 | @(if ($hc.Contains('.')) { 255 | $module, $setting = $hc -split '\.', 2 256 | $theModule = Get-Module $module 257 | $theModule.PrivateData.Color, 258 | $theModule.PrivateData.Colors, 259 | $theModule.PrivateData.Colour, 260 | $theModule.PrivateData.Colours, 261 | $theModule.PrivateData.EZOut, 262 | $global:PSColors, 263 | $global:PSColours 264 | } else { 265 | $setting = $hc 266 | $moduleColorSetting = $theModule.PrivateData.PSColors.$setting 267 | }) 268 | 269 | foreach ($place in $placesToLook) { 270 | if (-not $place) { continue } 271 | foreach ($propName in $setting -split '\.') { 272 | $place = $place.$propName 273 | if (-not $place) { break } 274 | } 275 | if ($place -and "$place".StartsWith('#') -and 4,7 -contains "$place".Length) { 276 | $hc = $place 277 | continue 278 | } 279 | } 280 | if (-not $hc.StartsWith -or -not $hc.StartsWith('#')) { 281 | continue 282 | } 283 | } 284 | $r,$g,$b = if ($hc.Length -eq 7) { 285 | [int]::Parse($hc[1..2]-join'', 'HexNumber') 286 | [int]::Parse($hc[3..4]-join '', 'HexNumber') 287 | [int]::Parse($hc[5..6] -join'', 'HexNumber') 288 | }elseif ($hc.Length -eq 4) { 289 | [int]::Parse($hc[1], 'HexNumber') * 16 290 | [int]::Parse($hc[2], 'HexNumber') * 16 291 | [int]::Parse($hc[3], 'HexNumber') * 16 292 | } 293 | 294 | if ($canUseHTML) { 295 | if ($n -eq 1) { "color:$hc" } 296 | elseif ($n -eq 2) { "background-color:$hc"} 297 | } 298 | elseif ($canUseANSI) { 299 | if ($n -eq 1) { $esc+"[38;2;$r;$g;${b}m" } 300 | elseif ($n -eq 2) { $esc+"[48;2;$r;$g;${b}m" } 301 | } 302 | 303 | }) 304 | 305 | 306 | $styleAttributes += @( 307 | if ($Bold) { 308 | if ($canUseHTML) {"font-weight:bold"} 309 | elseif ($canUseANSI) { '' + $esc + "[1m" } 310 | } 311 | if ($Faint) { 312 | if ($canUseHTML) { "opacity:.5" } 313 | elseif ($canUseANSI) { '' + $esc + "[2m" } 314 | } 315 | if ($Italic) { 316 | if ($canUseHTML) { "font-weight:bold" } 317 | elseif ($canUseANSI) {'' + $esc + "[3m" } 318 | } 319 | 320 | if ($Underline -and -not $doubleUnderline) { 321 | if ($canUseHTML) { "text-decoration:underline"} 322 | elseif ($canUseANSI) {'' +$esc + "[4m" } 323 | } 324 | 325 | if ($Blink) { 326 | if ($canUseANSI) { '' +$esc + "[5m" } 327 | } 328 | 329 | if ($invert) { 330 | if ($canUseHTML) {"filter:invert(100%)"} 331 | elseif ($canUseANSI) { '' + $esc + "[7m"} 332 | } 333 | 334 | if ($hide) { 335 | if ($canUseHTML) {"opacity:0"} 336 | elseif ($canUseANSI) { '' + $esc + "[8m"} 337 | } 338 | 339 | if ($Strikethru) { 340 | if ($canUseHTML) {"text-decoration: line-through"} 341 | elseif ($canUseANSI) { '' +$esc + "[9m" } 342 | } 343 | 344 | if ($DoubleUnderline) { 345 | if ($canUseHTML) { "border-bottom: 3px double;"} 346 | elseif ($canUseANSI) {'' +$esc + "[21m" } 347 | } 348 | 349 | ) 350 | 351 | $header = 352 | if ($canUseHTML) { 353 | "<span$( 354 | if ($styleAttributes) { " style='$($styleAttributes -join ';')'"} 355 | )$( 356 | if ($cssClasses) { " class='$($cssClasses -join ' ')'"} 357 | )>" 358 | } elseif ($canUseANSI) { 359 | $styleAttributes -join '' 360 | } 361 | } 362 | 363 | process { 364 | if ($header) { 365 | "$header" + "$(if ($inputObject) { $inputObject | Out-String})".Trim() 366 | } 367 | elseif ($inputObject) { 368 | ($inputObject | Out-String).Trim() 369 | } 370 | } 371 | 372 | end { 373 | 374 | if (-not $NoClear) { 375 | if ($canUseHTML) { 376 | "</span>" 377 | } 378 | elseif ($canUseANSI) { 379 | if ($Bold -or $Faint) { 380 | "$esc[22m" 381 | } 382 | if ($Italic) { 383 | "$esc[23m" 384 | } 385 | if ($Underline -or $doubleUnderline) { 386 | "$esc[24m" 387 | } 388 | if ($Blink) { 389 | "$esc[25m" 390 | } 391 | if ($Invert) { 392 | "$esc[27m" 393 | } 394 | if ($hide) { 395 | "$esc[28m" 396 | } 397 | if ($Strikethru) { 398 | "$esc[29m" 399 | } 400 | if ($ForegroundColor) { 401 | "$esc[39m" 402 | } 403 | if ($BackgroundColor) { 404 | "$esc[49m" 405 | } 406 | 407 | if (-not ($Underline -or $Bold -or $Invert -or $ForegroundColor -or $BackgroundColor)) { 408 | '' + $esc + '[0m' 409 | } 410 | } 411 | } 412 | } 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | -------------------------------------------------------------------------------- /.github/workflows/TestAndPublish.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Analyze, Test, Tag, and Publish 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | jobs: 8 | PowerShellStaticAnalysis: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: InstallScriptCop 12 | id: InstallScriptCop 13 | shell: pwsh 14 | run: | 15 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 16 | Install-Module -Name ScriptCop -Repository PSGallery -Force -Scope CurrentUser 17 | Import-Module ScriptCop -Force -PassThru 18 | - name: InstallPSScriptAnalyzer 19 | id: InstallPSScriptAnalyzer 20 | shell: pwsh 21 | run: | 22 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 23 | Install-Module -Name PSScriptAnalyzer -Repository PSGallery -Force -Scope CurrentUser 24 | Import-Module PSScriptAnalyzer -Force -PassThru 25 | - name: InstallPSDevOps 26 | id: InstallPSDevOps 27 | shell: pwsh 28 | run: | 29 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 30 | Install-Module -Name PSDevOps -Repository PSGallery -Force -Scope CurrentUser 31 | Import-Module PSDevOps -Force -PassThru 32 | - name: Check out repository 33 | uses: actions/checkout@v2 34 | - name: RunScriptCop 35 | id: RunScriptCop 36 | shell: pwsh 37 | run: | 38 | $Parameters = @{} 39 | $Parameters.ModulePath = ${env:ModulePath} 40 | foreach ($k in @($parameters.Keys)) { 41 | if ([String]::IsNullOrEmpty($parameters[$k])) { 42 | $parameters.Remove($k) 43 | } 44 | } 45 | Write-Host "::debug:: RunScriptCop $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 46 | & {param([string]$ModulePath) 47 | Import-Module ScriptCop, PSDevOps -PassThru | Out-Host 48 | 49 | if (-not $ModulePath) { 50 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 51 | $ModulePath = ".\$moduleName.psd1" 52 | } 53 | if ($ModulePath -like '*PSDevOps*') { 54 | Remove-Module PSDeVOps # If running ScriptCop on PSDeVOps, we need to remove the global module first. 55 | } 56 | 57 | 58 | $importedModule =Import-Module $ModulePath -Force -PassThru 59 | 60 | $importedModule | Out-Host 61 | 62 | $importedModule | 63 | Test-Command | 64 | Tee-Object -Variable scriptCopIssues | 65 | Out-Host 66 | 67 | foreach ($issue in $scriptCopIssues) { 68 | Write-GitHubWarning -Message "$($issue.ItemWithProblem): $($issue.Problem)" 69 | } 70 | } @Parameters 71 | - name: RunPSScriptAnalyzer 72 | id: RunPSScriptAnalyzer 73 | shell: pwsh 74 | run: | 75 | Import-Module PSScriptAnalyzer, PSDevOps -PassThru | Out-Host 76 | $invokeScriptAnalyzerSplat = @{Path='.\'} 77 | if ($ENV:PSScriptAnalyzer_Recurse) { 78 | $invokeScriptAnalyzerSplat.Recurse = $true 79 | } 80 | $result = Invoke-ScriptAnalyzer @invokeScriptAnalyzerSplat 81 | 82 | foreach ($r in $result) { 83 | if ('information', 'warning' -contains $r.Severity) { 84 | Write-GitHubWarning -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 85 | } 86 | elseif ($r.Severity -eq 'Error') { 87 | Write-GitHubError -Message "$($r.RuleName) : $($r.Message)" -SourcePath $r.ScriptPath -LineNumber $r.Line -ColumnNumber $r.Column 88 | } 89 | } 90 | TestPowerShellOnLinux: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: InstallPester 94 | id: InstallPester 95 | shell: pwsh 96 | run: | 97 | $Parameters = @{} 98 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 99 | foreach ($k in @($parameters.Keys)) { 100 | if ([String]::IsNullOrEmpty($parameters[$k])) { 101 | $parameters.Remove($k) 102 | } 103 | } 104 | Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 105 | & {<# 106 | .Synopsis 107 | Installs Pester 108 | .Description 109 | Installs Pester 110 | #> 111 | param( 112 | # The maximum pester version. Defaults to 4.99.99. 113 | [string] 114 | $PesterMaxVersion = '4.99.99' 115 | ) 116 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 117 | Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber 118 | Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters 119 | - name: Check out repository 120 | uses: actions/checkout@v2 121 | - name: RunPester 122 | id: RunPester 123 | shell: pwsh 124 | run: | 125 | $Parameters = @{} 126 | $Parameters.ModulePath = ${env:ModulePath} 127 | $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} 128 | $Parameters.NoCoverage = ${env:NoCoverage} 129 | $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; 130 | foreach ($k in @($parameters.Keys)) { 131 | if ([String]::IsNullOrEmpty($parameters[$k])) { 132 | $parameters.Remove($k) 133 | } 134 | } 135 | Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 136 | & {<# 137 | .Synopsis 138 | Runs Pester 139 | .Description 140 | Runs Pester tests after importing a PowerShell module 141 | #> 142 | param( 143 | # The module path. If not provided, will default to the second half of the repository ID. 144 | [string] 145 | $ModulePath, 146 | # The Pester max version. By default, this is pinned to 4.99.99. 147 | [string] 148 | $PesterMaxVersion = '4.99.99', 149 | 150 | # If set, will not collect code coverage. 151 | [switch] 152 | $NoCoverage 153 | ) 154 | 155 | $global:ErrorActionPreference = 'continue' 156 | $global:ProgressPreference = 'silentlycontinue' 157 | 158 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 159 | if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } 160 | $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion 161 | $importedModule = Import-Module $ModulePath -Force -PassThru 162 | $importedPester, $importedModule | Out-Host 163 | 164 | $codeCoverageParameters = @{ 165 | CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" 166 | CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" 167 | } 168 | 169 | if ($NoCoverage) { 170 | $codeCoverageParameters = @{} 171 | } 172 | 173 | 174 | $result = 175 | Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters 176 | 177 | "::set-output name=TotalCount::$($result.TotalCount)", 178 | "::set-output name=PassedCount::$($result.PassedCount)", 179 | "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host 180 | if ($result.FailedCount -gt 0) { 181 | "::debug:: $($result.FailedCount) tests failed" 182 | foreach ($r in $result.TestResult) { 183 | if (-not $r.Passed) { 184 | "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" 185 | } 186 | } 187 | throw "::error:: $($result.FailedCount) tests failed" 188 | } 189 | } @Parameters 190 | - name: PublishTestResults 191 | uses: actions/upload-artifact@v2 192 | with: 193 | name: PesterResults 194 | path: '**.TestResults.xml' 195 | if: ${{always()}} 196 | TagReleaseAndPublish: 197 | runs-on: ubuntu-latest 198 | if: ${{ success() }} 199 | steps: 200 | - name: Check out repository 201 | uses: actions/checkout@v2 202 | - name: TagModuleVersion 203 | id: TagModuleVersion 204 | shell: pwsh 205 | run: | 206 | $Parameters = @{} 207 | $Parameters.ModulePath = ${env:ModulePath} 208 | $Parameters.UserEmail = ${env:UserEmail} 209 | $Parameters.UserName = ${env:UserName} 210 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 211 | $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} 212 | foreach ($k in @($parameters.Keys)) { 213 | if ([String]::IsNullOrEmpty($parameters[$k])) { 214 | $parameters.Remove($k) 215 | } 216 | } 217 | Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 218 | & {param( 219 | [string] 220 | $ModulePath, 221 | 222 | # The user email associated with a git commit. 223 | [string] 224 | $UserEmail, 225 | 226 | # The user name associated with a git commit. 227 | [string] 228 | $UserName, 229 | 230 | # The tag version format (default value: 'v$(imported.Version)') 231 | # This can expand variables. $imported will contain the imported module. 232 | [string] 233 | $TagVersionFormat = 'v$($imported.Version)', 234 | 235 | # The tag version format (default value: '$($imported.Name) $(imported.Version)') 236 | # This can expand variables. $imported will contain the imported module. 237 | [string] 238 | $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' 239 | ) 240 | 241 | 242 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 243 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 244 | } else { $null } 245 | 246 | 247 | @" 248 | ::group::GitHubEvent 249 | $($gitHubEvent | ConvertTo-Json -Depth 100) 250 | ::endgroup:: 251 | "@ | Out-Host 252 | 253 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 254 | (-not $gitHubEvent.psobject.properties['inputs'])) { 255 | "::warning::Pull Request has not merged, skipping Tagging" | Out-Host 256 | return 257 | } 258 | 259 | 260 | 261 | $imported = 262 | if (-not $ModulePath) { 263 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 264 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 265 | } else { 266 | Import-Module $modulePath -Force -PassThru -Global 267 | } 268 | 269 | if (-not $imported) { return } 270 | 271 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 272 | $existingTags = git tag --list 273 | 274 | @" 275 | Target Version: $targetVersion 276 | 277 | Existing Tags: 278 | $($existingTags -join [Environment]::NewLine) 279 | "@ | Out-Host 280 | 281 | $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } 282 | 283 | if ($versionTagExists) { 284 | "::warning::Version $($versionTagExists)" 285 | return 286 | } 287 | 288 | if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } 289 | if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } 290 | git config --global user.email $UserEmail 291 | git config --global user.name $UserName 292 | 293 | git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) 294 | git push origin --tags 295 | 296 | if ($env:GITHUB_ACTOR) { 297 | exit 0 298 | }} @Parameters 299 | - name: ReleaseModule 300 | id: ReleaseModule 301 | shell: pwsh 302 | run: | 303 | $Parameters = @{} 304 | $Parameters.ModulePath = ${env:ModulePath} 305 | $Parameters.UserEmail = ${env:UserEmail} 306 | $Parameters.UserName = ${env:UserName} 307 | $Parameters.TagVersionFormat = ${env:TagVersionFormat} 308 | $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} 309 | foreach ($k in @($parameters.Keys)) { 310 | if ([String]::IsNullOrEmpty($parameters[$k])) { 311 | $parameters.Remove($k) 312 | } 313 | } 314 | Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 315 | & {param( 316 | [string] 317 | $ModulePath, 318 | 319 | # The user email associated with a git commit. 320 | [string] 321 | $UserEmail, 322 | 323 | # The user name associated with a git commit. 324 | [string] 325 | $UserName, 326 | 327 | # The tag version format (default value: 'v$(imported.Version)') 328 | # This can expand variables. $imported will contain the imported module. 329 | [string] 330 | $TagVersionFormat = 'v$($imported.Version)', 331 | 332 | # The release name format (default value: '$($imported.Name) $($imported.Version)') 333 | [string] 334 | $ReleaseNameFormat = '$($imported.Name) $($imported.Version)' 335 | ) 336 | 337 | 338 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 339 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 340 | } else { $null } 341 | 342 | 343 | @" 344 | ::group::GitHubEvent 345 | $($gitHubEvent | ConvertTo-Json -Depth 100) 346 | ::endgroup:: 347 | "@ | Out-Host 348 | 349 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 350 | (-not $gitHubEvent.psobject.properties['inputs'])) { 351 | "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host 352 | return 353 | } 354 | 355 | 356 | 357 | $imported = 358 | if (-not $ModulePath) { 359 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 360 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 361 | } else { 362 | Import-Module $modulePath -Force -PassThru -Global 363 | } 364 | 365 | if (-not $imported) { return } 366 | 367 | $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) 368 | $targetReleaseName = $targetVersion 369 | $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' 370 | "Release URL: $releasesURL" | Out-Host 371 | $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ 372 | "Accept" = "application/vnd.github.v3+json" 373 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 374 | } 375 | 376 | $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion 377 | 378 | if ($releaseExists) { 379 | "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host 380 | return 381 | } 382 | 383 | 384 | Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( 385 | [Ordered]@{ 386 | owner = '${{github.owner}}' 387 | repo = '${{github.repository}}' 388 | tag_name = $targetVersion 389 | name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) 390 | body = 391 | if ($env:RELEASENOTES) { 392 | $env:RELEASENOTES 393 | } elseif ($imported.PrivateData.PSData.ReleaseNotes) { 394 | $imported.PrivateData.PSData.ReleaseNotes 395 | } else { 396 | "$($imported.Name) $targetVersion" 397 | } 398 | draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } 399 | prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } 400 | } | ConvertTo-Json 401 | ) -Headers @{ 402 | "Accept" = "application/vnd.github.v3+json" 403 | "Content-type" = "application/json" 404 | "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' 405 | } 406 | } @Parameters 407 | - name: PublishPowerShellGallery 408 | id: PublishPowerShellGallery 409 | shell: pwsh 410 | run: | 411 | $Parameters = @{} 412 | $Parameters.ModulePath = ${env:ModulePath} 413 | $Parameters.Exclude = ${env:Exclude} 414 | $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' 415 | foreach ($k in @($parameters.Keys)) { 416 | if ([String]::IsNullOrEmpty($parameters[$k])) { 417 | $parameters.Remove($k) 418 | } 419 | } 420 | Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" 421 | & {param( 422 | [string] 423 | $ModulePath, 424 | 425 | [string[]] 426 | $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') 427 | ) 428 | 429 | $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { 430 | [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json 431 | } else { $null } 432 | 433 | if (-not $Exclude) { 434 | $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') 435 | } 436 | 437 | 438 | @" 439 | ::group::GitHubEvent 440 | $($gitHubEvent | ConvertTo-Json -Depth 100) 441 | ::endgroup:: 442 | "@ | Out-Host 443 | 444 | @" 445 | ::group::PSBoundParameters 446 | $($PSBoundParameters | ConvertTo-Json -Depth 100) 447 | ::endgroup:: 448 | "@ | Out-Host 449 | 450 | if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and 451 | (-not $gitHubEvent.psobject.properties['inputs'])) { 452 | "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host 453 | return 454 | } 455 | 456 | 457 | $imported = 458 | if (-not $ModulePath) { 459 | $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" 460 | Import-Module ".\$moduleName.psd1" -Force -PassThru -Global 461 | } else { 462 | Import-Module $modulePath -Force -PassThru -Global 463 | } 464 | 465 | if (-not $imported) { return } 466 | 467 | $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} 468 | 469 | if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { 470 | "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host 471 | } else { 472 | 473 | $gk = '${{secrets.GALLERYKEY}}' 474 | 475 | $rn = Get-Random 476 | $moduleTempFolder = Join-Path $pwd "$rn" 477 | $moduleTempPath = Join-Path $moduleTempFolder $moduleName 478 | New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host 479 | 480 | Write-Host "Staging Directory: $ModuleTempPath" 481 | 482 | $imported | Split-Path | 483 | Get-ChildItem -Force | 484 | Where-Object Name -NE $rn | 485 | Copy-Item -Destination $moduleTempPath -Recurse 486 | 487 | $moduleGitPath = Join-Path $moduleTempPath '.git' 488 | Write-Host "Removing .git directory" 489 | if (Test-Path $moduleGitPath) { 490 | Remove-Item -Recurse -Force $moduleGitPath 491 | } 492 | 493 | if ($Exclude) { 494 | "::notice::Attempting to Exlcude $exclude" | Out-Host 495 | Get-ChildItem $moduleTempPath -Recurse | 496 | Where-Object { 497 | foreach ($ex in $exclude) { 498 | if ($_.FullName -like $ex) { 499 | "::notice::Excluding $($_.FullName)" | Out-Host 500 | return $true 501 | } 502 | } 503 | } | 504 | Remove-Item 505 | } 506 | 507 | Write-Host "Module Files:" 508 | Get-ChildItem $moduleTempPath -Recurse 509 | Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" 510 | Publish-Module -Path $moduleTempPath -NuGetApiKey $gk 511 | if ($?) { 512 | Write-Host "Published to Gallery" 513 | } else { 514 | Write-Host "Gallery Publish Failed" 515 | exit 1 516 | } 517 | } 518 | } @Parameters 519 | BuildPSPrettifier: 520 | runs-on: ubuntu-latest 521 | if: ${{ success() }} 522 | steps: 523 | - name: Check out repository 524 | uses: actions/checkout@v2 525 | - name: Use Piecemeal 526 | uses: StartAutomating/Piecemeal@main 527 | id: Piecemeal 528 | - name: BuildPipeScript 529 | uses: StartAutomating/PipeScript@main 530 | - name: UseEZOut 531 | uses: StartAutomating/EZOut@master 532 | - name: UseHelpOut 533 | uses: StartAutomating/HelpOut@master 534 | env: 535 | NoCoverage: true 536 | -------------------------------------------------------------------------------- /Get-Prettifier.ps1: -------------------------------------------------------------------------------- 1 | #region Piecemeal [ 0.3.5 ] : Easy Extensible Plugins for PowerShell 2 | # Install-Module Piecemeal -Scope CurrentUser 3 | # Import-Module Piecemeal -Force 4 | # Install-Piecemeal -ExtensionNoun 'Prettifier' -ExtensionPattern '\.(?>prettify|prettifier)\.ps1$' -ExtensionTypeName 'Prettifier' -OutputPath '.\Get-Prettifier.ps1' 5 | function Get-Prettifier 6 | { 7 | <# 8 | .Synopsis 9 | Gets Extensions 10 | .Description 11 | Gets Extensions. 12 | 13 | Prettifier can be found in: 14 | 15 | * Any module that includes -PrettifierModuleName in it's tags. 16 | * The directory specified in -PrettifierPath 17 | .Example 18 | Get-Prettifier 19 | #> 20 | [OutputType('Extension')] 21 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "", Justification="PSScriptAnalyzer cannot handle nested scoping")] 22 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidAssignmentToAutomaticVariable", "", Justification="Desired for scenario")] 23 | param( 24 | # If provided, will look beneath a specific path for extensions. 25 | [Parameter(ValueFromPipelineByPropertyName)] 26 | [Alias('Fullname')] 27 | [string] 28 | $PrettifierPath, 29 | 30 | # If set, will clear caches of extensions, forcing a refresh. 31 | [switch] 32 | $Force, 33 | 34 | # If provided, will get Prettifier that extend a given command 35 | [Parameter(ValueFromPipelineByPropertyName)] 36 | [Alias('ThatExtends', 'For')] 37 | [string[]] 38 | $CommandName, 39 | 40 | <# 41 | 42 | The name of an extension. 43 | By default, this will match any extension command whose name, displayname, or aliases exactly match the name. 44 | 45 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 46 | #> 47 | [Parameter(ValueFromPipelineByPropertyName)] 48 | [ValidateNotNullOrEmpty()] 49 | [string[]] 50 | $PrettifierName, 51 | 52 | <# 53 | 54 | If provided, will treat -PrettifierName as a wildcard. 55 | This will return any extension whose name, displayname, or aliases are like the -PrettifierName. 56 | 57 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 58 | #> 59 | [Parameter(ValueFromPipelineByPropertyName)] 60 | [switch] 61 | $Like, 62 | 63 | <# 64 | 65 | If provided, will treat -PrettifierName as a regular expression. 66 | This will return any extension whose name, displayname, or aliases match the -PrettifierName. 67 | 68 | If the extension has an Alias with a regular expression literal (```'/Expression/'```) then the -PrettifierName will be valid if that regular expression matches. 69 | #> 70 | [Parameter(ValueFromPipelineByPropertyName)] 71 | [switch] 72 | $Match, 73 | 74 | # If set, will return the dynamic parameters object of all the Prettifier for a given command. 75 | [Parameter(ValueFromPipelineByPropertyName)] 76 | [switch] 77 | $DynamicParameter, 78 | 79 | # If set, will return if the extension could run. 80 | [Parameter(ValueFromPipelineByPropertyName)] 81 | [Alias('CanRun')] 82 | [switch] 83 | $CouldRun, 84 | 85 | # If set, will return if the extension could accept this input from the pipeline. 86 | [Alias('CanPipe')] 87 | [PSObject] 88 | $CouldPipe, 89 | 90 | # If set, will run the extension. If -Stream is passed, results will be directly returned. 91 | # By default, extension results are wrapped in a return object. 92 | [Parameter(ValueFromPipelineByPropertyName)] 93 | [switch] 94 | $Run, 95 | 96 | # If set, will stream output from running the extension. 97 | # By default, extension results are wrapped in a return object. 98 | [Parameter(ValueFromPipelineByPropertyName)] 99 | [switch] 100 | $Stream, 101 | 102 | # If set, will return the dynamic parameters of all Prettifier for a given command, using the provided DynamicParameterSetName. 103 | # Implies -DynamicParameter. 104 | [Parameter(ValueFromPipelineByPropertyName)] 105 | [string] 106 | $DynamicParameterSetName, 107 | 108 | 109 | # If provided, will return the dynamic parameters of all Prettifier for a given command, with all positional parameters offset. 110 | # Implies -DynamicParameter. 111 | [Parameter(ValueFromPipelineByPropertyName)] 112 | [int] 113 | $DynamicParameterPositionOffset = 0, 114 | 115 | # If set, will return the dynamic parameters of all Prettifier for a given command, with all mandatory parameters marked as optional. 116 | # Implies -DynamicParameter. Does not actually prevent the parameter from being Mandatory on the Extension. 117 | [Parameter(ValueFromPipelineByPropertyName)] 118 | [Alias('NoMandatoryDynamicParameters')] 119 | [switch] 120 | $NoMandatoryDynamicParameter, 121 | 122 | # If set, will require a [Runtime.CompilerServices.Extension()] attribute to be considered an extension. 123 | [Parameter(ValueFromPipelineByPropertyName)] 124 | [switch] 125 | $RequirePrettifierAttribute, 126 | 127 | # If set, will validate this input against [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. 128 | [Parameter(ValueFromPipelineByPropertyName)] 129 | [PSObject] 130 | $ValidateInput, 131 | 132 | # If set, will validate this input against all [ValidateScript], [ValidatePattern], [ValidateSet], and [ValidateRange] attributes found on an extension. 133 | # By default, if any validation attribute returned true, the extension is considered validated. 134 | [switch] 135 | $AllValid, 136 | 137 | # The name of the parameter set. This is used by -CouldRun and -Run to enforce a single specific parameter set. 138 | [Parameter(ValueFromPipelineByPropertyName)] 139 | [string] 140 | $ParameterSetName, 141 | 142 | # The parameters to the extension. Only used when determining if the extension -CouldRun. 143 | [Parameter(ValueFromPipelineByPropertyName)] 144 | [Collections.IDictionary] 145 | [Alias('Parameters','ExtensionParameter','ExtensionParameters')] 146 | $Parameter = [Ordered]@{}, 147 | 148 | # If set, will output a steppable pipeline for the extension. 149 | # Steppable pipelines allow you to control how begin, process, and end are executed in an extension. 150 | # This allows for the execution of more than one extension at a time. 151 | [switch] 152 | $SteppablePipeline, 153 | 154 | # If set, will output the help for the extensions 155 | [switch] 156 | $Help 157 | ) 158 | 159 | begin { 160 | $PrettifierPattern = '\.(?>prettify|prettifier)\.ps1$' 161 | $PrettifierTypeName = 'Prettifier' 162 | #region Define Inner Functions 163 | function WhereExtends { 164 | param( 165 | [Parameter(Position=0)] 166 | [string[]] 167 | $Command, 168 | 169 | [Parameter(ValueFromPipeline)] 170 | [PSObject] 171 | $ExtensionCommand 172 | ) 173 | 174 | process { 175 | if ($PrettifierName) { 176 | $ExtensionCommandAliases = @($ExtensionCommand.Attributes.AliasNames) 177 | $ExtensionCommandAliasRegexes = @($ExtensionCommandAliases -match '^/' -match '/$') 178 | if ($ExtensionCommandAliasRegexes) { 179 | $ExtensionCommandAliases = @($ExtensionCommandAliases -notmatch '^/' -match '/$') 180 | } 181 | :CheckExtensionName do { 182 | foreach ($exn in $PrettifierName) { 183 | if ($like) { 184 | if (($extensionCommand -like $exn) -or 185 | ($extensionCommand.DisplayName -like $exn) -or 186 | ($ExtensionCommandAliases -like $exn)) { break CheckExtensionName } 187 | } 188 | elseif ($match) { 189 | if (($ExtensionCommand -match $exn) -or 190 | ($extensionCommand.DisplayName -match $exn) -or 191 | ($ExtensionCommandAliases -match $exn)) { break CheckExtensionName } 192 | } 193 | elseif (($ExtensionCommand -eq $exn) -or 194 | ($ExtensionCommand.DisplayName -eq $exn) -or 195 | ($ExtensionCommandAliases -eq $exn)) { break CheckExtensionName } 196 | elseif ($ExtensionCommandAliasRegexes) { 197 | foreach ($extensionAliasRegex in $ExtensionCommandAliasRegexes) { 198 | $extensionAliasRegex = [Regex]::New($extensionAliasRegex -replace '^/' -replace '/$', 'IgnoreCase,IgnorePatternWhitespace') 199 | if ($extensionAliasRegex -and $extensionAliasRegex.IsMatch($exn)) { 200 | break CheckExtensionName 201 | } 202 | } 203 | } 204 | } 205 | 206 | 207 | return 208 | } while ($false) 209 | } 210 | if ($Command -and $ExtensionCommand.Extends -contains $command) { 211 | $commandExtended = $ext 212 | return $ExtensionCommand 213 | } 214 | elseif (-not $command) { 215 | return $ExtensionCommand 216 | } 217 | } 218 | } 219 | filter ConvertToExtension { 220 | $in = $_ 221 | $extCmd = 222 | if ($in -is [Management.Automation.CommandInfo]) { 223 | $in 224 | } 225 | elseif ($in -is [IO.FileInfo]) { 226 | $ExecutionContext.SessionState.InvokeCommand.GetCommand($in.fullname, 'ExternalScript,Application') 227 | } 228 | else { 229 | $ExecutionContext.SessionState.InvokeCommand.GetCommand($in, 'Function,ExternalScript,Application') 230 | } 231 | 232 | $extCmd.PSObject.Methods.Add([psscriptmethod]::new('GetExtendedCommands', { 233 | param([Management.Automation.CommandInfo[]]$CommandList) 234 | $extendedCommandNames = @( 235 | foreach ($attr in $this.ScriptBlock.Attributes) { 236 | if ($attr -isnot [Management.Automation.CmdletAttribute]) { continue } 237 | ( 238 | ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s') 239 | ) -replace '^\-' -replace '\-$' 240 | } 241 | ) 242 | if (-not $extendedCommandNames) { 243 | $this | Add-Member NoteProperty Extends @() -Force 244 | $this | Add-Member NoteProperty ExtensionCommands @() -Force 245 | return 246 | } 247 | if (-not $CommandList) { 248 | $commandList = $ExecutionContext.SessionState.InvokeCommand.GetCommands('*','Function,Alias,Cmdlet', $true) 249 | } 250 | $extends = @{} 251 | :nextCommand foreach ($loadedCmd in $commandList) { 252 | foreach ($extensionCommandName in $extendedCommandNames) { 253 | if ($extensionCommandName -and $loadedCmd.Name -match $extensionCommandName) { 254 | $loadedCmd 255 | $extends[$loadedCmd.Name] = $loadedCmd 256 | continue nextCommand 257 | } 258 | } 259 | } 260 | 261 | if (-not $extends.Count) { 262 | $extends = $null 263 | } 264 | 265 | $this | Add-Member NoteProperty Extends $extends.Keys -Force 266 | $this | Add-Member NoteProperty ExtensionCommands $extends.Values -Force 267 | })) 268 | 269 | if (-not $script:AllCommands) { 270 | $script:AllCommands = $ExecutionContext.SessionState.InvokeCommand.GetCommands('*','Function,Alias,Cmdlet', $true) 271 | } 272 | 273 | 274 | $null = $extCmd.GetExtendedCommands($script:AllCommands) 275 | 276 | $inheritanceLevel = [ComponentModel.InheritanceLevel]::Inherited 277 | 278 | $extCmd.PSObject.Properties.Add([psscriptproperty]::New('BlockComments', { 279 | [Regex]::New(" 280 | \<\# # The opening tag 281 | (? 282 | (?:.|\s)+?(?=\z|\#>) # anything until the closing tag 283 | ) 284 | \#\> # the closing tag 285 | ", 'IgnoreCase,IgnorePatternWhitespace', '00:00:01').Matches($this.ScriptBlock) 286 | })) 287 | 288 | $extCmd.PSObject.Methods.Add([psscriptmethod]::New('GetHelpField', { 289 | param([Parameter(Mandatory)]$Field) 290 | $fieldNames = 'synopsis','description','link','example','inputs', 'outputs', 'parameter', 'notes' 291 | foreach ($block in $this.BlockComments) { 292 | foreach ($match in [Regex]::new(" 293 | \.(?$Field) # Field Start 294 | [\s-[\r\n]]{0,} # Optional Whitespace 295 | [\r\n]+ # newline 296 | (?(?:.|\s)+?(?= 297 | ( 298 | [\r\n]{0,}\s{0,}\.(?>$($fieldNames -join '|'))| 299 | \#\>| 300 | \z 301 | ))) # Anything until the next .field or end of the comment block 302 | ", 'IgnoreCase,IgnorePatternWhitespace', [Timespan]::FromSeconds(1)).Matches( 303 | $block.Value 304 | )) { 305 | $match.Groups["Content"].Value -replace '[\s\r\n]+$' 306 | } 307 | } 308 | })) 309 | 310 | $extCmd.PSObject.Properties.Add([PSNoteProperty]::new('InheritanceLevel', $inheritanceLevel)) 311 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 312 | 'DisplayName', [ScriptBlock]::Create("`$this.Name -replace '$extensionFullRegex'") 313 | )) 314 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 315 | 'Attributes', {$this.ScriptBlock.Attributes} 316 | )) 317 | 318 | 319 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 320 | 'Category', { 321 | foreach ($attr in $this.ScriptBlock.Attributes) { 322 | if ($attr -is [Reflection.AssemblyMetaDataAttribute] -and 323 | $attr.Key -eq 'Category') { 324 | $attr.Value 325 | } 326 | elseif ($attr -is [ComponentModel.CategoryAttribute]) { 327 | $attr.Category 328 | } 329 | } 330 | 331 | } 332 | )) 333 | 334 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 335 | 'Rank', { 336 | foreach ($attr in $this.ScriptBlock.Attributes) { 337 | if ($attr -is [Reflection.AssemblyMetaDataAttribute] -and 338 | $attr.Key -in 'Order', 'Rank') { 339 | return $attr.Value -as [int] 340 | } 341 | } 342 | return 0 343 | } 344 | )) 345 | 346 | $extCmd.PSObject.Properties.Add([psscriptproperty]::new( 347 | 'Metadata', { 348 | $Metadata = [Ordered]@{} 349 | foreach ($attr in $this.ScriptBlock.Attributes) { 350 | if ($attr -is [Reflection.AssemblyMetaDataAttribute]) { 351 | if ($Metadata[$attr.Key]) { 352 | $Metadata[$attr.Key] = @($Metadata[$attr.Key]) + $attr.Value 353 | } else { 354 | $Metadata[$attr.Key] = $attr.Value 355 | } 356 | } 357 | } 358 | return $Metadata 359 | } 360 | )) 361 | 362 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 363 | 'Description', { @($this.GetHelpField("Description"))[0] -replace '^\s+' } 364 | )) 365 | 366 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 367 | 'Synopsis', { @($this.GetHelpField("Synopsis"))[0] -replace '^\s+' })) 368 | 369 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 370 | 'Examples', { $this.GetHelpField("Example") })) 371 | 372 | $extCmd.PSObject.Properties.Add([PSScriptProperty]::new( 373 | 'Links', { $this.GetHelpField("Link") })) 374 | 375 | $extCmd.PSObject.Methods.Add([psscriptmethod]::new('Validate', { 376 | param( 377 | # input being validated 378 | [PSObject]$ValidateInput, 379 | # If set, will require all [Validate] attributes to be valid. 380 | # If not set, any input will be valid. 381 | [switch]$AllValid 382 | ) 383 | 384 | foreach ($attr in $this.ScriptBlock.Attributes) { 385 | if ($attr -is [Management.Automation.ValidateScriptAttribute]) { 386 | try { 387 | $_ = $this = $psItem = $ValidateInput 388 | $isValidInput = . $attr.ScriptBlock 389 | if ($isValidInput -and -not $AllValid) { return $true} 390 | if (-not $isValidInput -and $AllValid) { 391 | if ($ErrorActionPreference -eq 'ignore') { 392 | return $false 393 | } elseif ($AllValid) { 394 | throw "'$ValidateInput' is not a valid value." 395 | } 396 | } 397 | } catch { 398 | if ($AllValid) { 399 | if ($ErrorActionPreference -eq 'ignore') { 400 | return $false 401 | } else { 402 | throw 403 | } 404 | } 405 | } 406 | } 407 | elseif ($attr -is [Management.Automation.ValidateSetAttribute]) { 408 | if ($ValidateInput -notin $attr.ValidValues) { 409 | if ($AllValid) { 410 | if ($ErrorActionPreference -eq 'ignore') { 411 | return $false 412 | } else { 413 | throw "'$ValidateInput' is not a valid value. Valid values are '$(@($attr.ValidValues) -join "','")'" 414 | } 415 | } 416 | } elseif (-not $AllValid) { 417 | return $true 418 | } 419 | } 420 | elseif ($attr -is [Management.Automation.ValidatePatternAttribute]) { 421 | $matched = [Regex]::new($attr.RegexPattern, $attr.Options, [Timespan]::FromSeconds(1)).Match($ValidateInput) 422 | if (-not $matched.Success) { 423 | if ($allValid) { 424 | if ($ErrorActionPreference -eq 'ignore') { 425 | return $false 426 | } else { 427 | throw "'$ValidateInput' is not a valid value. Valid values must match the pattern '$($attr.RegexPattern)'" 428 | } 429 | } 430 | } elseif (-not $AllValid) { 431 | return $true 432 | } 433 | } 434 | elseif ($attr -is [Management.Automation.ValidateRangeAttribute]) { 435 | if ($null -ne $attr.MinRange -and $validateInput -lt $attr.MinRange) { 436 | if ($AllValid) { 437 | if ($ErrorActionPreference -eq 'ignore') { 438 | return $false 439 | } else { 440 | throw "'$ValidateInput' is below the minimum range [ $($attr.MinRange)-$($attr.MaxRange) ]" 441 | } 442 | } 443 | } 444 | elseif ($null -ne $attr.MaxRange -and $validateInput -gt $attr.MaxRange) { 445 | if ($AllValid) { 446 | if ($ErrorActionPreference -eq 'ignore') { 447 | return $false 448 | } else { 449 | throw "'$ValidateInput' is above the maximum range [ $($attr.MinRange)-$($attr.MaxRange) ]" 450 | } 451 | } 452 | } 453 | elseif (-not $AllValid) { 454 | return $true 455 | } 456 | } 457 | } 458 | 459 | if ($AllValid) { 460 | return $true 461 | } else { 462 | return $false 463 | } 464 | })) 465 | 466 | $extCmd.PSObject.Methods.Add([PSScriptMethod]::new('GetDynamicParameters', { 467 | param( 468 | [string] 469 | $ParameterSetName, 470 | 471 | [int] 472 | $PositionOffset, 473 | 474 | [switch] 475 | $NoMandatory, 476 | 477 | [string[]] 478 | $commandList 479 | ) 480 | 481 | $ExtensionDynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() 482 | $Extension = $this 483 | 484 | :nextDynamicParameter foreach ($in in @(([Management.Automation.CommandMetaData]$Extension).Parameters.Keys)) { 485 | $attrList = [Collections.Generic.List[Attribute]]::new() 486 | $validCommandNames = @() 487 | foreach ($attr in $extension.Parameters[$in].attributes) { 488 | if ($attr -isnot [Management.Automation.ParameterAttribute]) { 489 | # we can passthru any non-parameter attributes 490 | $attrList.Add($attr) 491 | if ($attr -is [Management.Automation.CmdletAttribute] -and $commandList) { 492 | $validCommandNames += ( 493 | ($attr.VerbName -replace '\s') + '-' + ($attr.NounName -replace '\s') 494 | ) -replace '^\-' -replace '\-$' 495 | } 496 | } else { 497 | # but parameter attributes need to copied. 498 | $attrCopy = [Management.Automation.ParameterAttribute]::new() 499 | # (Side note: without a .Clone, copying is tedious.) 500 | foreach ($prop in $attrCopy.GetType().GetProperties('Instance,Public')) { 501 | if (-not $prop.CanWrite) { continue } 502 | if ($null -ne $attr.($prop.Name)) { 503 | $attrCopy.($prop.Name) = $attr.($prop.Name) 504 | } 505 | } 506 | 507 | $attrCopy.ParameterSetName = 508 | if ($ParameterSetName) { 509 | $ParameterSetName 510 | } 511 | else { 512 | $defaultParamSetName = 513 | foreach ($extAttr in $Extension.ScriptBlock.Attributes) { 514 | if ($extAttr.DefaultParameterSetName) { 515 | $extAttr.DefaultParameterSetName 516 | break 517 | } 518 | } 519 | if ($attrCopy.ParameterSetName -ne '__AllParameterSets') { 520 | $attrCopy.ParameterSetName 521 | } 522 | elseif ($defaultParamSetName) { 523 | $defaultParamSetName 524 | } 525 | elseif ($this -is [Management.Automation.FunctionInfo]) { 526 | $this.Name 527 | } elseif ($this -is [Management.Automation.ExternalScriptInfo]) { 528 | $this.Source 529 | } 530 | } 531 | 532 | if ($NoMandatory -and $attrCopy.Mandatory) { 533 | $attrCopy.Mandatory = $false 534 | } 535 | 536 | if ($PositionOffset -and $attr.Position -ge 0) { 537 | $attrCopy.Position += $PositionOffset 538 | } 539 | $attrList.Add($attrCopy) 540 | } 541 | } 542 | 543 | 544 | if ($commandList -and $validCommandNames) { 545 | :CheckCommandValidity do { 546 | foreach ($vc in $validCommandNames) { 547 | if ($commandList -match $vc) { break CheckCommandValidity } 548 | } 549 | continue nextDynamicParameter 550 | } while ($false) 551 | } 552 | $ExtensionDynamicParameters.Add($in, [Management.Automation.RuntimeDefinedParameter]::new( 553 | $Extension.Parameters[$in].Name, 554 | $Extension.Parameters[$in].ParameterType, 555 | $attrList 556 | )) 557 | } 558 | 559 | $ExtensionDynamicParameters 560 | 561 | })) 562 | 563 | 564 | $extCmd.PSObject.Methods.Add([PSScriptMethod]::new('IsParameterValid', { 565 | param([Parameter(Mandatory)]$ParameterName, [PSObject]$Value) 566 | 567 | if ($this.Parameters.Count -ge 0 -and 568 | $this.Parameters[$parameterName].Attributes 569 | ) { 570 | foreach ($attr in $this.Parameters[$parameterName].Attributes) { 571 | $_ = $value 572 | if ($attr -is [Management.Automation.ValidateScriptAttribute]) { 573 | $result = try { . $attr.ScriptBlock } catch { $null } 574 | if ($result -ne $true) { 575 | return $false 576 | } 577 | } 578 | elseif ($attr -is [Management.Automation.ValidatePatternAttribute] -and 579 | (-not [Regex]::new($attr.RegexPattern, $attr.Options, '00:00:05').IsMatch($value)) 580 | ) { 581 | return $false 582 | } 583 | elseif ($attr -is [Management.Automation.ValidateSetAttribute] -and 584 | $attr.ValidValues -notcontains $value) { 585 | return $false 586 | } 587 | elseif ($attr -is [Management.Automation.ValidateRangeAttribute] -and ( 588 | ($value -gt $attr.MaxRange) -or ($value -lt $attr.MinRange) 589 | )) { 590 | return $false 591 | } 592 | } 593 | } 594 | return $true 595 | })) 596 | 597 | $extCmd.PSObject.Methods.Add([PSScriptMethod]::new('CouldPipe', { 598 | param([PSObject]$InputObject) 599 | 600 | :nextParameterSet foreach ($paramSet in $this.ParameterSets) { 601 | if ($ParameterSetName -and $paramSet.Name -ne $ParameterSetName) { continue } 602 | $params = @{} 603 | $mappedParams = [Ordered]@{} # Create a collection of mapped parameters 604 | # Walk thru each parameter of this command 605 | foreach ($myParam in $paramSet.Parameters) { 606 | # If the parameter is ValueFromPipeline 607 | if ($myParam.ValueFromPipeline) { 608 | # and we have an input object 609 | if ($null -ne $inputObject -and 610 | ( 611 | # of the exact type 612 | $myParam.ParameterType -eq $inputObject.GetType() -or 613 | # (or a subclass of that type) 614 | $inputObject.GetType().IsSubClassOf($myParam.ParameterType) -or 615 | # (or an inteface of that type) 616 | ($myParam.ParameterType.IsInterface -and $InputObject.GetType().GetInterface($myParam.ParameterType)) 617 | ) 618 | ) { 619 | # then map the parameter. 620 | $mappedParams[$myParam.Name] = $params[$myParam.Name] = $InputObject 621 | } 622 | } 623 | } 624 | # Check for parameter validity. 625 | foreach ($mappedParamName in @($mappedParams.Keys)) { 626 | if (-not $this.IsParameterValid($mappedParamName, $mappedParams[$mappedParamName])) { 627 | $mappedParams.Remove($mappedParamName) 628 | } 629 | } 630 | if ($mappedParams.Count -gt 0) { 631 | return $mappedParams 632 | } 633 | } 634 | })) 635 | 636 | $extCmd.PSObject.Methods.Add([PSScriptMethod]::new('CouldRun', { 637 | param([Collections.IDictionary]$params, [string]$ParameterSetName) 638 | 639 | :nextParameterSet foreach ($paramSet in $this.ParameterSets) { 640 | if ($ParameterSetName -and $paramSet.Name -ne $ParameterSetName) { continue } 641 | $mappedParams = [Ordered]@{} # Create a collection of mapped parameters 642 | $mandatories = # Walk thru each parameter of this command 643 | @(foreach ($myParam in $paramSet.Parameters) { 644 | if ($params.Contains($myParam.Name)) { # If this was in Params, 645 | $mappedParams[$myParam.Name] = $params[$myParam.Name] # then map it. 646 | } else { 647 | foreach ($paramAlias in $myParam.Aliases) { # Otherwise, check the aliases 648 | if ($params.Contains($paramAlias)) { # and map it if the parameters had the alias. 649 | $mappedParams[$myParam.Name] = $params[$paramAlias] 650 | break 651 | } 652 | } 653 | } 654 | if ($myParam.IsMandatory) { # If the parameter was mandatory, 655 | $myParam.Name # keep track of it. 656 | } 657 | }) 658 | 659 | # Check for parameter validity. 660 | foreach ($mappedParamName in @($mappedParams.Keys)) { 661 | if (-not $this.IsParameterValid($mappedParamName, $mappedParams[$mappedParamName])) { 662 | $mappedParams.Remove($mappedParamName) 663 | } 664 | } 665 | 666 | foreach ($mandatoryParam in $mandatories) { # Walk thru each mandatory parameter. 667 | if (-not $mappedParams.Contains($mandatoryParam)) { # If it wasn't in the parameters. 668 | continue nextParameterSet 669 | } 670 | } 671 | return $mappedParams 672 | } 673 | return $false 674 | })) 675 | 676 | $extCmd.pstypenames.clear() 677 | if ($PrettifierTypeName) { 678 | $extCmd.pstypenames.add($PrettifierTypeName) 679 | } else { 680 | $extCmd.pstypenames.add('Extension') 681 | } 682 | 683 | $extCmd 684 | } 685 | function OutputExtension { 686 | begin { 687 | $allDynamicParameters = [Management.Automation.RuntimeDefinedParameterDictionary]::new() 688 | } 689 | process { 690 | $extCmd = $_ 691 | 692 | # When we're outputting an extension, we start off assuming that it is valid. 693 | $IsValid = $true 694 | if ($ValidateInput) { # If we have a particular input we want to validate 695 | try { 696 | # Check if it is valid 697 | if (-not $extCmd.Validate($ValidateInput, $AllValid)) { 698 | $IsValid = $false # and then set IsValid if it is not. 699 | } 700 | } catch { 701 | Write-Error $_ # If we encountered an exception, write it out 702 | $IsValid = $false # and set is $IsValid to false. 703 | } 704 | } 705 | 706 | 707 | # If we're requesting dynamic parameters (and the extension is valid) 708 | if ($IsValid -and 709 | ($DynamicParameter -or $DynamicParameterSetName -or $DynamicParameterPositionOffset -or $NoMandatoryDynamicParameter)) { 710 | # Get what the dynamic parameters of the extension would be. 711 | $extensionParams = $extCmd.GetDynamicParameters($DynamicParameterSetName, 712 | $DynamicParameterPositionOffset, 713 | $NoMandatoryDynamicParameter, $CommandName) 714 | 715 | # Then, walk over each extension parameter. 716 | foreach ($kv in $extensionParams.GetEnumerator()) { 717 | # If the $CommandExtended had a built-in parameter, we cannot override it, so skip it. 718 | if ($commandExtended -and ([Management.Automation.CommandMetaData]$commandExtended).Parameters.$($kv.Key)) { 719 | continue 720 | } 721 | 722 | # If already have this dynamic parameter 723 | if ($allDynamicParameters.ContainsKey($kv.Key)) { 724 | 725 | # check it's type. 726 | if ($kv.Value.ParameterType -ne $allDynamicParameters[$kv.Key].ParameterType) { 727 | # If the types are different, make it a PSObject (so it could be either). 728 | Write-Verbose "Extension '$extCmd' Parameter '$($kv.Key)' Type Conflict, making type PSObject" 729 | $allDynamicParameters[$kv.Key].ParameterType = [PSObject] 730 | } 731 | 732 | 733 | foreach ($attr in $kv.Value.Attributes) { 734 | if ($allDynamicParameters[$kv.Key].Attributes.Contains($attr)) { 735 | continue 736 | } 737 | $allDynamicParameters[$kv.Key].Attributes.Add($attr) 738 | } 739 | } else { 740 | $allDynamicParameters[$kv.Key] = $kv.Value 741 | } 742 | } 743 | } 744 | elseif ($IsValid -and ($CouldPipe -or $CouldRun)) { 745 | if (-not $extCmd) { return } 746 | 747 | $extensionParams = [Ordered]@{} 748 | $pipelineParams = @() 749 | if ($CouldPipe) { 750 | $couldPipeExt = $extCmd.CouldPipe($CouldPipe) 751 | if (-not $couldPipeExt) { return } 752 | $pipelineParams += $couldPipeExt.Keys 753 | if (-not $CouldRun) { 754 | $extensionParams += $couldPipeExt 755 | } else { 756 | foreach ($kv in $couldPipeExt.GetEnumerator()) { 757 | $Parameter[$kv.Key] = $kv.Value 758 | } 759 | } 760 | } 761 | if ($CouldRun) { 762 | $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) 763 | if (-not $couldRunExt) { return } 764 | $extensionParams += $couldRunExt 765 | } 766 | 767 | [PSCustomObject][Ordered]@{ 768 | ExtensionCommand = $extCmd 769 | CommandName = $CommandName 770 | ExtensionInputObject = if ($CouldPipe) { $CouldPipe } else { $null } 771 | ExtensionParameter = $extensionParams 772 | PipelineParameters = $pipelineParams 773 | } 774 | } 775 | elseif ($IsValid -and $SteppablePipeline) { 776 | if (-not $extCmd) { return } 777 | if ($Parameter) { 778 | $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) 779 | if (-not $couldRunExt) { 780 | $sb = {& $extCmd } 781 | $sb.GetSteppablePipeline() | 782 | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | 783 | Add-Member NoteProperty ExtensionParameters $couldRunExt -Force -PassThru | 784 | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru 785 | } else { 786 | $sb = {& $extCmd @couldRunExt} 787 | $sb.GetSteppablePipeline() | 788 | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | 789 | Add-Member NoteProperty ExtensionParameters $couldRunExt -Force -PassThru | 790 | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru 791 | } 792 | } else { 793 | $sb = {& $extCmd } 794 | $sb.GetSteppablePipeline() | 795 | Add-Member NoteProperty ExtensionCommand $extCmd -Force -PassThru | 796 | Add-Member NoteProperty ExtensionParameters @{} -Force -PassThru | 797 | Add-Member NoteProperty ExtensionScriptBlock $sb -Force -PassThru 798 | } 799 | } 800 | elseif ($IsValid -and $Run) { 801 | if (-not $extCmd) { return } 802 | $couldRunExt = $extCmd.CouldRun($Parameter, $ParameterSetName) 803 | if (-not $couldRunExt) { return } 804 | if ($extCmd.InheritanceLevel -eq 'InheritedReadOnly') { return } 805 | if ($Stream) { 806 | & $extCmd @couldRunExt 807 | } else { 808 | [PSCustomObject][Ordered]@{ 809 | CommandName = $CommandName 810 | ExtensionCommand = $extCmd 811 | ExtensionOutput = & $extCmd @couldRunExt 812 | Done = $extCmd.InheritanceLevel -eq 'NotInherited' 813 | } 814 | } 815 | return 816 | } 817 | elseif ($IsValid -and $Help) { 818 | $getHelpSplat = @{Full=$true} 819 | 820 | if ($extCmd -is [Management.Automation.ExternalScriptInfo]) { 821 | Get-Help $extCmd.Source @getHelpSplat 822 | } elseif ($extCmd -is [Management.Automation.FunctionInfo]) { 823 | Get-Help $extCmd @getHelpSplat 824 | } 825 | } 826 | elseif ($IsValid) { 827 | return $extCmd 828 | } 829 | } 830 | end { 831 | if ($DynamicParameter) { 832 | return $allDynamicParameters 833 | } 834 | } 835 | } 836 | #endregion Define Inner Functions 837 | 838 | $extensionFullRegex = 839 | [Regex]::New($( 840 | if ($PrettifierModule) { 841 | "\.(?>$(@(@($PrettifierModule) + $PrettifierModuleAlias) -join '|'))\." + "(?>$($PrettifierPattern -join '|'))" 842 | } else { 843 | "(?>$($PrettifierPattern -join '|'))" 844 | } 845 | ), 'IgnoreCase,IgnorePatternWhitespace', '00:00:01') 846 | 847 | #region Find Extensions 848 | $loadedModules = @(Get-Module) 849 | $myInv = $MyInvocation 850 | $myModuleName = if ($PrettifierModule) { $PrettifierModule } else { $MyInvocation.MyCommand.Module.Name } 851 | if ($myInv.MyCommand.Module -and $loadedModules -notcontains $myInv.MyCommand.Module) { 852 | $loadedModules = @($myInv.MyCommand.Module) + $loadedModules 853 | } 854 | $getCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand 855 | 856 | if ($Force) { 857 | $script:Prettifiers = $null 858 | $script:AllCommands = @() 859 | } 860 | if (-not $script:Prettifiers) 861 | { 862 | $script:Prettifiers = 863 | @( 864 | #region Find Prettifier in Loaded Modules 865 | foreach ($loadedModule in $loadedModules) { # Walk over all modules. 866 | if ( # If the module has PrivateData keyed to this module 867 | $loadedModule.PrivateData.$myModuleName 868 | ) { 869 | # Determine the root of the module with private data. 870 | $thisModuleRoot = [IO.Path]::GetDirectoryName($loadedModule.Path) 871 | # and get the extension data 872 | $extensionData = $loadedModule.PrivateData.$myModuleName 873 | if ($extensionData -is [Hashtable]) { # If it was a hashtable 874 | foreach ($ed in $extensionData.GetEnumerator()) { # walk each key 875 | 876 | $extensionCmd = 877 | if ($ed.Value -like '*.ps1') { # If the key was a .ps1 file 878 | $getCmd.Invoke( # treat it as a relative path to the .ps1 879 | [IO.Path]::Combine($thisModuleRoot, $ed.Value), 880 | 'ExternalScript' 881 | ) 882 | } else { # Otherwise, treat it as the name of an exported command. 883 | $loadedModule.ExportedCommands[$ed.Value] 884 | } 885 | if ($extensionCmd) { # If we've found a valid extension command 886 | $extensionCmd | ConvertToExtension # return it as an extension. 887 | } 888 | } 889 | } 890 | } 891 | elseif ($loadedModule.PrivateData.PSData.Tags -contains $myModuleName -or $loadedModule.Name -eq $myModuleName) { 892 | $loadedModule | 893 | Split-Path | 894 | Get-ChildItem -Recurse | 895 | Where-Object { $_.Name -Match $extensionFullRegex } | 896 | ConvertToExtension 897 | } 898 | } 899 | #endregion Find Prettifier in Loaded Modules 900 | ) 901 | } 902 | #endregion Find Extensions 903 | } 904 | 905 | process { 906 | 907 | if ($PrettifierPath) { 908 | Get-ChildItem -Recurse -Path $PrettifierPath | 909 | Where-Object { $_.Name -Match $extensionFullRegex } | 910 | ConvertToExtension | 911 | . WhereExtends $CommandName | 912 | #region Install-Piecemeal -WhereObject 913 | # This section can be updated by using Install-Piecemeal -WhereObject 914 | #endregion Install-Piecemeal -WhereObject 915 | Sort-Object Rank, Name | 916 | OutputExtension 917 | #region Install-Piecemeal -ForeachObject 918 | # This section can be updated by using Install-Piecemeal -ForeachObject 919 | #endregion Install-Piecemeal -ForeachObject 920 | } else { 921 | $script:Prettifiers | 922 | . WhereExtends $CommandName | 923 | Sort-Object Rank, Name | 924 | OutputExtension 925 | } 926 | } 927 | } 928 | #endregion Piecemeal [ 0.3.5 ] : Easy Extensible Plugins for PowerShell 929 | 930 | --------------------------------------------------------------------------------