├── .github ├── stale.yml └── workflows │ └── ci.yml ├── ContinuousIntegration └── ContinuousIntegration.ps1 ├── InstallModule.ps1 ├── LICENCE ├── PSPx.psd1 ├── PSPx.psm1 ├── Private └── PSNotebookRunspace.ps1 ├── Public ├── Get-MarkdownCodeBlock.ps1 ├── Invoke-ExecuteMarkdown.ps1 ├── Invoke-ScriptAnalyzerMarkdown.ps1 ├── Invoke-ScriptFormatterMarkdown.ps1 └── Invoke-UpdateScriptFormat.ps1 ├── PublishToGallery.ps1 ├── README.md ├── __tests__ ├── GetMarkdownCodeBlock.tests.ps1 ├── Invoke-UpdateScriptFormat.tests.ps1 ├── InvokeExecuteMarkdown.tests.ps1 ├── InvokeScriptAnalyzerMarkdown.tests.ps1 ├── InvokeScriptFormatterMarkdown.tests.ps1 ├── testMarkdownFiles │ ├── FormatPSBlocks.md │ ├── PSBlocksPSSA-Issues.md │ └── basicPSBlocks.md └── testRawMDFiles │ ├── hashtable.md │ └── simple.md ├── changelog.md ├── examples └── markdown.md └── media ├── PSMarkdown.png └── PSOutput.png /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 30 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | pull_request: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | validate: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | # os: [windows-latest] 16 | # os: [windows-latest, ubuntu-18.04] 17 | os: [windows-latest, ubuntu-18.04, macos-latest] 18 | # os: [ubuntu-18.04] 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Run Continuous Integration 22 | run: ./ContinuousIntegration/ContinuousIntegration.ps1 23 | shell: pwsh 24 | -------------------------------------------------------------------------------- /ContinuousIntegration/ContinuousIntegration.ps1: -------------------------------------------------------------------------------- 1 | $PSVersionTable 2 | 3 | $modules = @("Pester", "PSScriptAnalyzer") 4 | 5 | foreach ($module in $modules) { 6 | Write-Host "Installing $module" -ForegroundColor Cyan 7 | Install-Module $module -Force -SkipPublisherCheck 8 | Import-Module $module -Force -PassThru 9 | } 10 | 11 | $pesterResults = Invoke-Pester -Output Detailed -PassThru 12 | 13 | if (!$pesterResults) { 14 | Throw "Tests failed" 15 | } 16 | else { 17 | if ($pesterResults.FailedCount -gt 0) { 18 | 19 | '[Progress] Pester Results Failed' 20 | $pesterResults.Failed | Out-String 21 | 22 | '[Progress] Pester Results FailedBlocks' 23 | $pesterResults.FailedBlocks | Out-String 24 | 25 | '[Progress] Pester Results FailedContainers' 26 | $pesterResults.FailedContainers | Out-String 27 | 28 | Throw "Tests failed" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /InstallModule.ps1: -------------------------------------------------------------------------------- 1 | param ($fullPath) 2 | 3 | if (-not $fullPath) { 4 | $fullpath = $env:PSModulePath -split ":(?!\\)|;|," | 5 | Where-Object { $_ -notlike ([System.Environment]::GetFolderPath("UserProfile") + "*") -and $_ -notlike "$pshome*" } | 6 | Select-Object -First 1 7 | $fullPath = Join-Path $fullPath -ChildPath "PSPx" 8 | } 9 | Push-location $PSScriptRoot 10 | Robocopy . $fullPath /mir /XD .vscode .git CI __tests__ data mdHelp /XF appveyor.yml azure-pipelines.yml .gitattributes .gitignore filelist.txt install.ps1 InstallModule.ps1 11 | Pop-Location -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Doug Finke 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 | -------------------------------------------------------------------------------- /PSPx.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # Assemblies that must be loaded prior to importing this module 3 | RequiredAssemblies = @() 4 | 5 | # Script module or binary module file associated with this manifest. 6 | RootModule = 'PSPx.psm1' 7 | 8 | # Version number of this module. 9 | ModuleVersion = '1.4.1' 10 | 11 | # ID used to uniquely identify this module 12 | GUID = '8ce136b4-c11b-4ede-a1f6-9822e3212c16' 13 | 14 | # Author of this module 15 | Author = 'Douglas Finke' 16 | 17 | # Company or vendor of this module 18 | CompanyName = 'Doug Finke' 19 | 20 | # Copyright statement for this module 21 | Copyright = 'c 2021 All rights reserved.' 22 | 23 | # Description of the functionality provided by this module 24 | Description = @' 25 | PowerShell module that can execute scripts written in markdown that can be accessed either locally or via a URL. 26 | '@ 27 | 28 | # Functions to export from this module 29 | FunctionsToExport = @( 30 | 'Get-MarkdownCodeBlock', 31 | 'Invoke-ExecuteMarkdown', 32 | 'Invoke-ScriptAnalyzerMarkdown', 33 | 'Invoke-ScriptFormatterMarkdown', 34 | 'Invoke-UpdateScriptFormat', 35 | 'Update-MarkdownCodeFormatting' 36 | ) 37 | 38 | # Aliases to export from this module 39 | AliasesToExport = @( 40 | 'px' 41 | ) 42 | 43 | # Cmdlets to export from this module 44 | CmdletsToExport = @() 45 | 46 | FileList = @() 47 | 48 | # Private data to pass to the module specified in RootModule/ModuleToProcess 49 | PrivateData = @{ 50 | # PSData is module packaging and gallery metadata embedded in PrivateData 51 | # It's for rebuilding PowerShellGet (and PoshCode) NuGet-style packages 52 | # We had to do this because it's the only place we're allowed to extend the manifest 53 | # https://connect.microsoft.com/PowerShell/feedback/details/421837 54 | PSData = @{ 55 | # The primary categorization of this module (from the TechNet Gallery tech tree). 56 | Category = "PowerShell Markdown" 57 | 58 | # Keyword tags to help users find this module via navigations and search. 59 | Tags = @("PowerShell", "Markdown", "Execute", "Scripts") 60 | 61 | # The web address of an icon which can be used in galleries to represent this module 62 | #IconUri = "http://pesterbdd.com/images/Pester.png" 63 | 64 | # The web address of this module's project or support homepage. 65 | ProjectUri = "https://github.com/dfinke/PSPx" 66 | 67 | # The web address of this module's license. Points to a page that's embeddable and linkable. 68 | LicenseUri = "https://github.com/dfinke/PSPx/blob/master/LICENSE" 69 | 70 | # Release notes for this particular version of the module 71 | #ReleaseNotes = $True 72 | 73 | # If true, the LicenseUrl points to an end-user license (not just a source license) which requires the user agreement before use. 74 | # RequireLicenseAcceptance = "" 75 | 76 | # Indicates this is a pre-release/testing version of the module. 77 | IsPrerelease = 'False' 78 | } 79 | } 80 | 81 | # Minimum version of the Windows PowerShell engine required by this module 82 | # PowerShellVersion = '' 83 | 84 | # Name of the Windows PowerShell host required by this module 85 | # PowerShellHostName = '' 86 | 87 | # Minimum version of the Windows PowerShell host required by this module 88 | # PowerShellHostVersion = '' 89 | 90 | # Minimum version of Microsoft .NET Framework required by this module 91 | # DotNetFrameworkVersion = '' 92 | 93 | # Minimum version of the common language runtime (CLR) required by this module 94 | # CLRVersion = '' 95 | 96 | # Processor architecture (None, X86, Amd64) required by this module 97 | # ProcessorArchitecture = '' 98 | 99 | # Modules that must be imported into the global environment prior to importing this module 100 | # RequiredModules = @() 101 | 102 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 103 | # ScriptsToProcess = @() 104 | 105 | # Type files (.ps1xml) to be loaded when importing this module 106 | # TypesToProcess = @() 107 | 108 | # Format files (.ps1xml) to be loaded when importing this module 109 | # FormatsToProcess = @() 110 | 111 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 112 | # NestedModules = @() 113 | 114 | # List of all modules packaged with this module 115 | # ModuleList = @() 116 | 117 | # List of all files packaged with this module 118 | # Variables to export from this module 119 | #VariablesToExport = '*' 120 | 121 | # HelpInfo URI of this module 122 | # HelpInfoURI = '' 123 | 124 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 125 | # DefaultCommandPrefix = '' 126 | 127 | } -------------------------------------------------------------------------------- /PSPx.psm1: -------------------------------------------------------------------------------- 1 | foreach ($directory in @('Private', 'Public')) { 2 | Get-ChildItem -Path "$PSScriptRoot\$directory\*.ps1" -ErrorAction SilentlyContinue | ForEach-Object { 3 | . $_.FullName 4 | } 5 | } 6 | 7 | Set-Alias px Invoke-ExecuteMarkdown 8 | -------------------------------------------------------------------------------- /Private/PSNotebookRunspace.ps1: -------------------------------------------------------------------------------- 1 | class PSNotebookRunspace { 2 | $Runspace 3 | $PowerShell 4 | 5 | PSNotebookRunspace() { 6 | $this.Runspace = [runspacefactory]::CreateRunspace() 7 | $this.PowerShell = [powershell]::Create() 8 | $this.PowerShell.runspace = $this.Runspace 9 | $this.Runspace.Open() 10 | } 11 | 12 | [object]Invoke($code) { 13 | $this.PowerShell.AddScript(($code -join "`r`n")) 14 | return $this.PowerShell.Invoke() 15 | } 16 | 17 | [void]Close() { 18 | $this.Runspace.Close() 19 | } 20 | } -------------------------------------------------------------------------------- /Public/Get-MarkdownCodeBlock.ps1: -------------------------------------------------------------------------------- 1 | function New-MardownEntry { 2 | param( 3 | [ValidateSet('markdown', 'psscript')] 4 | $type, 5 | $text 6 | ) 7 | 8 | [pscustomobject]@{Type = $type; Text = @($text) } 9 | } 10 | 11 | function Get-PSScript { 12 | param( 13 | $parsedMarkdown 14 | ) 15 | 16 | $parsedMarkdown | Where-Object type -eq 'psscript' | 17 | ForEach-Object { 18 | $end = $_.Text.Count - 2 19 | $_.Text[1..$end] 20 | } 21 | } 22 | 23 | function Update-MarkdownCodeFormatting { 24 | param( 25 | [Parameter(Mandatory)] 26 | $parsedMarkdown 27 | ) 28 | 29 | switch ($parsedMarkdown) { 30 | { $_.Type -eq 'markdown' } { 31 | continue 32 | } 33 | { $_.Type -eq 'psscript' } { 34 | $end = $_.Text.Count - 2 35 | $s = $_.Text[1..$end] -join "`n" 36 | $s = Invoke-Formatter -ScriptDefinition $s 37 | 38 | $_.Text = "{0}`n{1}`n{2}" -f $_.Text[0], $s, $_.Text[-1] 39 | continue 40 | } 41 | } 42 | 43 | $parsedMarkdown 44 | } 45 | 46 | function Invoke-ParseMarkdown { 47 | param( 48 | [string[]]$markdown, 49 | [Switch]$Raw 50 | ) 51 | 52 | $parsedMD = @() 53 | 54 | $newMarkdowEntry = $true 55 | $found = $false 56 | 57 | switch ($markdown) { 58 | { $_.Trim() -eq '```ps' -Or $_.Trim() -eq '```ps1' -Or $_.Trim() -eq '```powershell' } { 59 | $parsedMD += New-MardownEntry "PSScript" ("{0}" -f $_) 60 | $found = $true 61 | continue 62 | } 63 | 64 | { $_.StartsWith('```') } { 65 | $found = $false 66 | $parsedMD[-1].Text += "{0}" -f $_ 67 | $newMarkdowEntry = $true 68 | continue 69 | } 70 | 71 | default { 72 | if ($found -eq $true) { 73 | $parsedMD[-1].Text += "{0}" -f $_ 74 | } 75 | else { 76 | if ($newMarkdowEntry -eq $true) { 77 | $parsedMD += New-MardownEntry "Markdown" 78 | $newMarkdowEntry = $false 79 | } 80 | $parsedMD[-1].Text += "{0}" -f $_ 81 | } 82 | } 83 | } 84 | 85 | if ($Raw) { 86 | $parsedMD | Add-Member -PassThru -MemberType NoteProperty -Name Path -Value $Path 87 | } 88 | else { 89 | [PSCustomObject]@{ 90 | Path = $Path 91 | Script = (Get-PSScript $parsedMD) 92 | } 93 | } 94 | } 95 | 96 | function Get-MarkdownCodeBlock { 97 | <# 98 | .SYNOPSIS 99 | Extracts PowerShell code blocks from markdown 100 | 101 | .EXAMPLE 102 | Get-MarkdownCodeBlock -Path C:\temp\myfile.md 103 | 104 | .Example 105 | $url = 'https://private.url.com/test.md' 106 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 107 | Get-MarkdownCodeBlock $url $header 108 | #> 109 | param( 110 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 111 | [Alias('FullName')] 112 | $Path, 113 | $Headers, 114 | [Switch]$Raw 115 | ) 116 | 117 | Process { 118 | if (([System.Uri]::IsWellFormedUriString($Path, 'Absolute'))) { 119 | $InvokeParams = @{Uri = $Path } 120 | 121 | if ($Headers) { 122 | $InvokeParams["Headers"] = $Headers 123 | } 124 | 125 | try { 126 | $Error.Clear() 127 | $mdContent = Invoke-RestMethod @InvokeParams 128 | } 129 | catch { 130 | $err = [PSCustomObject]@{ 131 | Path = $Path 132 | Error = $_ 133 | } 134 | 135 | return $err 136 | } 137 | 138 | $mdContent = $mdContent -split "`n" 139 | } 140 | elseif (Test-Path $Path) { 141 | $Path = Resolve-Path $Path 142 | $mdContent = [System.IO.File]::ReadAllLines($Path) 143 | } 144 | elseif ($Path -is [string]) { 145 | $mdContent = $Path -split "`n" 146 | } 147 | 148 | Invoke-ParseMarkdown $mdContent -Raw:$Raw 149 | } 150 | } -------------------------------------------------------------------------------- /Public/Invoke-ExecuteMarkdown.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ExecuteMarkdown { 2 | <# 3 | .Synopsis 4 | Execute PowerShell written in markdown code blocks 5 | 6 | .Example 7 | px https://gist.githubusercontent.com/dfinke/610703acacd915a94afc1a4695fc6fce/raw/479e8a5edc62607ac5f753a4eb2a56ead43a841f/testErrors.md 8 | 9 | .Example 10 | $url = 'https://private.url.com/test.md' 11 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 12 | Invoke-ExecuteMarkdown $url $header 13 | #> 14 | param( 15 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 16 | [Alias('FullName')] 17 | $Path, 18 | $Headers 19 | ) 20 | 21 | Process { 22 | 23 | $mdCodeBlock = Get-MarkdownCodeBlock $Path -Headers $Headers 24 | if (!$mdCodeBlock.Error) { 25 | $PSNotebookRunspace = [PSNotebookRunspace]::new() 26 | $result = $null 27 | 28 | $invokeResult = $PSNotebookRunspace.Invoke($mdCodeBlock.script) 29 | 30 | if ($PSNotebookRunspace.PowerShell.Streams.Error.Count -gt 0) { 31 | $result = $PSNotebookRunspace.PowerShell.Streams.Error | Out-String 32 | } 33 | 34 | $result += $invokeResult 35 | 36 | $mdCodeBlock | 37 | Add-Member -PassThru -MemberType NoteProperty -Name Cmdlet -Value $MyInvocation.MyCommand | 38 | Add-Member -PassThru -MemberType NoteProperty -Name Result -Value $result 39 | 40 | $PSNotebookRunspace.Close() 41 | } 42 | else { 43 | $mdCodeBlock 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Public/Invoke-ScriptAnalyzerMarkdown.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ScriptAnalyzerMarkdown { 2 | <# 3 | .Synopsis 4 | Run PowerShell Script Analyzer on code blocks written in markdown 5 | 6 | .Example 7 | Invoke-ScriptAnalyzerMarkdown https://gist.githubusercontent.com/dfinke/610703acacd915a94afc1a4695fc6fce/raw/479e8a5edc62607ac5f753a4eb2a56ead43a841f/testErrors.md 8 | 9 | .Example 10 | $url = 'https://private.url.com/test.md' 11 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 12 | Invoke-ScriptAnalyzerMarkdown $url $header 13 | 14 | #> 15 | param( 16 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 17 | [Alias('FullName')] 18 | $Path, 19 | $Headers 20 | ) 21 | 22 | Process { 23 | $mdCodeBlock = Get-MarkdownCodeBlock -Path $Path -Headers $Headers 24 | 25 | if (!$mdCodeBlock.Error) { 26 | 27 | $result = Invoke-ScriptAnalyzer -ScriptDefinition ($mdCodeBlock.script -join "`n") 28 | 29 | $mdCodeBlock | 30 | Add-Member -PassThru -MemberType NoteProperty -Name Cmdlet -Value $MyInvocation.MyCommand | 31 | Add-Member -PassThru -MemberType NoteProperty -Name Result -Value $result 32 | } 33 | else { 34 | $mdCodeBlock 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Public/Invoke-ScriptFormatterMarkdown.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ScriptFormatterMarkdown { 2 | <# 3 | .Synopsis 4 | Run PowerShell Script Analyzer Formatter on code blocks written in markdown 5 | 6 | .Example 7 | Invoke-ScriptFormatterMarkdown https://gist.githubusercontent.com/dfinke/610703acacd915a94afc1a4695fc6fce/raw/479e8a5edc62607ac5f753a4eb2a56ead43a841f/testErrors.md 8 | 9 | .Example 10 | $url = 'https://private.url.com/test.md' 11 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 12 | Invoke-ScriptFormatterMarkdown $url $header 13 | 14 | #> 15 | param( 16 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 17 | [Alias('FullName')] 18 | $Path, 19 | $Headers 20 | ) 21 | 22 | Process { 23 | $mdCodeBlock = Get-MarkdownCodeBlock -Path $Path -Headers $Headers 24 | 25 | if (!$mdCodeBlock.Error) { 26 | 27 | $script = $mdCodeBlock.script -join "`n" 28 | $result = Invoke-Formatter -ScriptDefinition $script 29 | 30 | $mdCodeBlock | 31 | Add-Member -PassThru -MemberType NoteProperty -Name Cmdlet -Value $MyInvocation.MyCommand | 32 | Add-Member -PassThru -MemberType NoteProperty -Name Result -Value $result 33 | } 34 | else { 35 | $mdCodeBlock 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /Public/Invoke-UpdateScriptFormat.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-UpdateScriptFormat { 2 | <# 3 | .SYNOPSIS 4 | Run PowerShell Script Analyzer Formatter on code blocks, and return both markdown and formatted code blocks as a string. 5 | 6 | .EXAMPLE 7 | Invoke-UpdateScriptFormat 'https://gist.githubusercontent.com/dfinke/610703acacd915a94afc1a4695fc6fce/raw/479e8a5edc62607ac5f753a4eb2a56ead43a841f/testErrors.md' 8 | 9 | #> 10 | param( 11 | [Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline)] 12 | [Alias('FullName')] 13 | $Path, 14 | $Headers 15 | ) 16 | 17 | Process { 18 | $md = Get-MarkdownCodeBlock -Path $Path -Raw -Headers $Headers 19 | (Update-MarkdownCodeFormatting $md).Text 20 | } 21 | } -------------------------------------------------------------------------------- /PublishToGallery.ps1: -------------------------------------------------------------------------------- 1 | $p = @{ 2 | Name = "PSPx" 3 | NuGetApiKey = $NuGetApiKey 4 | } 5 | 6 | Publish-Module @p -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Why do this? 2 | 3 | - Do you have a very long installation documentation for our project and need a quick way of testing it? 4 | - It is general purpose tool that can be used for multiple purposes like executing a tutorial documentation, using docs as a script, etc. 5 | 6 | PSPx collects fenced code blocks from input markdown file and executes them in same order as they appear in the file. 7 | 8 | Example of fenced code block in markdown file 9 | 10 | ```ps1 11 | foreach($i in 1..10) { 12 | $i 13 | } 14 | ``` 15 | 16 | PSPx recognizes the tags `ps`, `ps1`, and `powershell`. 17 | 18 | PSPx collects all the code blocks and executes them as a single script. 19 | 20 |
21 | 22 | | Function | Description | 23 | | --- | --- | 24 | | `Invoke-ExecuteMarkdown` | Execute PowerShell written in markdown code blocks | 25 | | `Invoke-ScriptAnalyzerMarkdown` | Run PowerShell Script Analyzer on code blocks written in markdown | 26 | | `Invoke-ScriptFormatterMarkdown` | Install a module | 27 | | `Invoke-UpdateScriptFormat` | Run PowerShell Script Analyzer Formatter on code blocks, and return both markdown and formatted code blocks as a string. | 28 | | `Get-MarkdownCodeBlock` | Extracts PowerShell code blocks from markdown | 29 | 30 | ## Markdown scripts 31 | 32 | `px` can execute scripts written in markdown ([examples/markdown.md](examples/markdown.md)): 33 | 34 | ```powershell 35 | px examples/markdown.md 36 | ``` 37 | 38 | It can also execute scripts written in markdown from a `url`: 39 | 40 | ```powershell 41 | px https://gist.githubusercontent.com/dfinke/610703acacd915a94afc1a4695fc6fce/raw/479e8a5edc62607ac5f753a4eb2a56ead43a841f/testErrors.md | fl 42 | ``` 43 | 44 | # Run a markdown file 45 | Execute code blocks in input.md file 46 | 47 | ```powershell 48 | Invoke-ExecuteMarkdown input.md 49 | ``` 50 | 51 | # Run a markdown file from a Url 52 | 53 | ```powershell 54 | $url = 'https://raw.githubusercontent.com/dfinke/PSPx/master/__tests__/testMarkdownFiles/basicPSBlocks.md' 55 | 56 | Invoke-ExecuteMarkdown $url 57 | ``` 58 | 59 | ## The output 60 | ``` 61 | Path : https://raw.githubusercontent.com/dfinke/PSPx/master/__tests__/testMarkdownFiles/basicPSBlocks.md 62 | Script : {"Hello World", "Goodbye", $xs = 1, 2, 3, foreach ($x in $xs) {…} 63 | Cmdlet : Invoke-ExecuteMarkdown 64 | Result : {Hello World, Goodbye, 1, 2...} 65 | ``` 66 | 67 | # List Code Blocks 68 | 69 | Wouldn't it be great to be able to list all code blocks that are going to be executed before actually using run command? You can! 70 | There are a couple of ways to do this. 71 | 72 | This returns all of the code blocks as a single `Script`. 73 | 74 | ```powershell 75 | Get-MarkdownCodeBlock basicPSBlocks.md 76 | ``` 77 | 78 | ### Result 79 | ``` 80 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 81 | Script : {"Hello World", "Goodbye", $xs = 1, 2, 3, foreach ($x in $xs) {...} 82 | ``` 83 | 84 | ## Use `-Raw` Switch 85 | 86 | This returns both the markdown and code blocks from the target `.md` file. Each is tagged with a `Type` => `PSScript`|`Markdown`. 87 | 88 | ```powershell 89 | Get-MarkdownCodeBlock basicPSBlocks.md -Raw 90 | ``` 91 | 92 | ### Result 93 | 94 | ``` 95 | Type : PSScript 96 | Text : {```ps, "Hello World", ```} 97 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 98 | 99 | Type : Markdown 100 | Text : {$null, , # Another block, } 101 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 102 | 103 | Type : PSScript 104 | Text : {```ps, "Goodbye", ```} 105 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 106 | 107 | Type : Markdown 108 | Text : {$null, , # Add some numbers, } 109 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 110 | 111 | Type : PSScript 112 | Text : {```ps, $xs = 1, 2, 3, foreach ($x in $xs) {, $x...} 113 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 114 | 115 | Type : Markdown 116 | Text : {$null, , # Return a string, use a powershell block, } 117 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 118 | 119 | Type : PSScript 120 | Text : {```powershell, 'This is a powershell block', ```} 121 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 122 | 123 | Type : Markdown 124 | Text : {$null, , # Add Numbers, } 125 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 126 | 127 | Type : PSScript 128 | Text : {```ps1, 3+4+5, ```} 129 | Path : D:\mygit\PSPx\__tests__\testMarkdownFiles\basicPSBlocks.md 130 | ``` 131 | 132 | This gives you the ability to process the mardown file on your own. For example, you can extract the PowerShell from the `Text` parameter like this. 133 | 134 | ```powershell 135 | Get-MarkdownCodeBlock basicPSBlocks.md -Raw | 136 | Where Type -eq 'PSScript' | 137 | ForEach { $_.Text[1..($_.Text.Count - 2)] } 138 | ``` 139 | 140 | ### Markdown 141 | 142 | ![](/media/PSMarkdown.png) 143 | 144 | ### Output 145 | 146 | ![](/media/PSOutput.png) 147 | 148 | ## Access Tokens 149 | 150 | The `-Headers` parameter can be used to add access tokens to the request for markdown files behind a private Url. For example, if they are in a private GutHub repository, or Azure DevOps. 151 | 152 | ```powershell 153 | $url = 'https://private.url.com/test.md' 154 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 155 | Invoke-ExecuteMarkdown $url $header 156 | ``` -------------------------------------------------------------------------------- /__tests__/GetMarkdownCodeBlock.tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PSPx.psd1 -Force 2 | 3 | Describe "Test Get Markdown CodeBlock" -Tag "Get-MarkdownCodeBlock" { 4 | 5 | BeforeAll { 6 | $rootDir = Join-Path $PSScriptRoot 'testMarkdownFiles' 7 | } 8 | 9 | It "Should read a local markdown file" { 10 | $fileName = Join-Path $rootDir 'basicPSBlocks.md' 11 | 12 | $actual = Get-MarkdownCodeBlock $fileName 13 | $scriptAsLines = $actual[0].Script.Split("`n") 14 | 15 | $actual.Path | Should -BeExactly $fileName 16 | $scriptAsLines.Count | Should -Be 8 17 | } 18 | 19 | It "Should read a url" { 20 | $url = 'https://raw.githubusercontent.com/dfinke/PSPx/master/examples/markdown.md' 21 | 22 | $actual = Get-MarkdownCodeBlock $url 23 | 24 | $actual.Path | Should -BeExactly $url 25 | $actual.Script | Should -Not -BeNullOrEmpty 26 | 27 | $lines = $actual.Script -split "`n" 28 | $lines.Count | Should -Be 7 29 | 30 | $lines[0].Trim() | Should -BeExactly '$PSVersionTable' 31 | $lines[1].Trim() | Should -BeExactly '$pwd' 32 | $lines[2].Trim() | Should -BeExactly '$env:APPDATA' 33 | $lines[3].Trim() | Should -BeExactly 'function Get-Info {' 34 | $lines[4].Trim() | Should -BeExactly '"Test Info $(get-date)"' 35 | $lines[5].Trim() | Should -BeExactly '}' 36 | $lines[6].Trim() | Should -BeExactly 'Get-Info' 37 | } 38 | 39 | It "Should throw if no Headers param on the function" { 40 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 41 | { Get-MarkdownCodeBlock -Path $url -Headers @{"A" = 1 } } | Should -Not -Throw 42 | } 43 | 44 | It "Should get an error with a private url" { 45 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 46 | # $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 47 | $actual = Get-MarkdownCodeBlock $url 48 | 49 | $actual.Path | Should -BeExactly $url 50 | $actual.Error | Should -BeExactly "404: Not Found" 51 | } 52 | 53 | It "Should read each block of the markdown files" { 54 | $rootdir = Join-Path $PSScriptRoot 'testRawMDFiles' 55 | $fileName = Join-Path $rootDir 'simple.md' 56 | 57 | $actual = Get-MarkdownCodeBlock $fileName -Raw 58 | 59 | $actual.Count | Should -Be 2 60 | 61 | $actual[0].Path | Should -BeExactly $fileName 62 | $actual[0].Type | Should -BeExactly 'Markdown' 63 | $actual[0].Text.Count | Should -Be 3 64 | 65 | $actual[0].Text[0] | Should -BeNullOrEmpty 66 | $actual[0].Text[1] | Should -BeExactly '# This is a simple test' 67 | $actual[0].Text[2] | Should -BeNullOrEmpty 68 | 69 | $actual[1].Path | Should -BeExactly $fileName 70 | $actual[1].Type | Should -BeExactly 'PSScript' 71 | $actual[1].Text.Count | Should -Be 3 72 | 73 | $actual[1].Text[0] | Should -BeExactly '```powershell' 74 | $actual[1].Text[1] | Should -BeExactly '"Hello World"' 75 | $actual[1].Text[2] | Should -BeExactly '```' 76 | } 77 | } -------------------------------------------------------------------------------- /__tests__/Invoke-UpdateScriptFormat.tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PSPx.psd1 -Force 2 | 3 | Describe "Test Invoke Update Script Format" -Tag "Invoke-UpdateScriptFormat" { 4 | It "Should update PS code in place" { 5 | $rootdir = Join-Path $PSScriptRoot 'testRawMDFiles' 6 | $fileName = Join-Path $rootDir 'hashtable.md' 7 | 8 | $actual = Invoke-UpdateScriptFormat $fileName 9 | 10 | $actual.Count | Should -Be 4 11 | 12 | $actual[0] | Should -BeNullOrEmpty 13 | $actual[1] | Should -BeExactly '# a hashtable' 14 | $actual[2] | Should -BeNullOrEmpty 15 | 16 | $text = $actual[3] -split "`n" 17 | 18 | $text[0] | Should -BeExactly '```ps1' 19 | $text[1] | Should -BeExactly '@{' 20 | $text[2] | Should -BeExactly " 10 = 'a'" 21 | $text[3] | Should -BeExactly " 100 = 'a'" 22 | $text[4] | Should -BeExactly " 1000 = 'a'" 23 | $text[5] | Should -BeExactly '}' 24 | $text[6] | Should -BeExactly '```' 25 | } 26 | } -------------------------------------------------------------------------------- /__tests__/InvokeExecuteMarkdown.tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PSPx.psd1 -Force 2 | 3 | Describe "Test Invoke Execute Markdown" -Tag "Invoke-ExecuteMarkdown" { 4 | 5 | BeforeAll { 6 | $rootDir = "$PSScriptRoot/testMarkdownFiles" 7 | } 8 | 9 | It "Should have `px` alias" { 10 | Get-Alias px | Should -Not -BeNullOrEmpty 11 | } 12 | 13 | It "Should execute the markdown and return results" { 14 | $fileName = $rootDir + "/basicPSBlocks.md" 15 | 16 | $actual = Invoke-ExecuteMarkdown $fileName 17 | 18 | $actual | Should -Not -BeNullOrEmpty 19 | $actual.Result.Count | Should -Be 7 20 | $actual.Result[0] | Should -BeExactly 'Hello World' 21 | $actual.Result[1] | Should -BeExactly 'Goodbye' 22 | $actual.Result[2] | Should -BeExactly 1 23 | $actual.Result[3] | Should -BeExactly 2 24 | $actual.Result[4] | Should -BeExactly 3 25 | $actual.Result[5] | Should -BeExactly 'This is a powershell block' 26 | } 27 | 28 | It "Should execute the markdown from a URL" { 29 | $url = 'https://raw.githubusercontent.com/dfinke/PSPx/master/examples/markdown.md' 30 | 31 | $actual = Invoke-ExecuteMarkdown $url 32 | 33 | $actual | Should -Not -BeNullOrEmpty 34 | $actual.Result.Count | Should -Be 4 35 | $actual.Script | Should -Not -BeNullOrEmpty 36 | } 37 | 38 | It "Should execute mutiple markdown files" { 39 | $actual = Get-ChildItem $rootDir *.md | Invoke-ExecuteMarkdown 40 | 41 | $actual.Count | Should -Be 3 42 | 43 | $actual[0].Cmdlet | Should -BeExactly 'Invoke-ExecuteMarkdown' 44 | $actual[1].Cmdlet | Should -BeExactly 'Invoke-ExecuteMarkdown' 45 | 46 | # first file 47 | $scriptAsLines = $actual[0].Script.Split("`n") 48 | $scriptAsLines.Count | Should -Be 8 49 | 50 | # second file 51 | $scriptAsLines = $actual[1].Script.Split("`n") 52 | $scriptAsLines.Count | Should -Be 5 53 | } 54 | 55 | It "Should execute markdown strings directly" { 56 | $md = @' 57 | ```powershell 58 | "Hello World" 59 | ``` 60 | '@ 61 | $actual = Invoke-ExecuteMarkdown $md 62 | $scriptAsLines = $actual[0].Script.Split("`n") 63 | 64 | $scriptAsLines.Count | Should -Be 1 65 | 66 | } 67 | 68 | It "Should execute markdown strings piped" { 69 | $md = @' 70 | ```powershell 71 | "Hello World" 72 | ``` 73 | '@ 74 | $actual = $md | Invoke-ExecuteMarkdown 75 | $scriptAsLines = $actual[0].Script.Split("`n") 76 | 77 | $scriptAsLines.Count | Should -Be 1 78 | } 79 | 80 | It "Should throw if no Headers param on the function" { 81 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 82 | { Invoke-ExecuteMarkdown -Path $url -Headers @{"A" = 1 } } | Should -Not -Throw 83 | } 84 | 85 | It "Should get an error with a private url" { 86 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 87 | # $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 88 | $actual = Invoke-ExecuteMarkdown $url 89 | 90 | $actual.Path | Should -BeExactly $url 91 | $actual.Error | Should -BeExactly "404: Not Found" 92 | } 93 | } -------------------------------------------------------------------------------- /__tests__/InvokeScriptAnalyzerMarkdown.tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PSPx.psd1 -Force 2 | 3 | Describe "Test Invoke Script Analyzer Markdown" -Tag "Invoke-ScriptAnalyzerMarkdown" { 4 | 5 | BeforeAll { 6 | $rootDir = "$PSScriptRoot/testMarkdownFiles" 7 | } 8 | 9 | It "Should run PSSA on the markdown blocks" { 10 | $fileName = $rootDir + "/PSBlocksPSSA-Issues.md" 11 | 12 | $actual = Invoke-ScriptAnalyzerMarkdown $fileName 13 | 14 | $actual | Should -Not -BeNullOrEmpty 15 | $actual.Result.Count | Should -Be 2 16 | } 17 | 18 | It "Should execute mutiple markdown files" { 19 | $actual = Get-ChildItem $rootDir *.md | Invoke-ScriptAnalyzerMarkdown 20 | 21 | $actual.Count | Should -Be 3 22 | 23 | $actual[0].Cmdlet | Should -BeExactly 'Invoke-ScriptAnalyzerMarkdown' 24 | $actual[1].Cmdlet | Should -BeExactly 'Invoke-ScriptAnalyzerMarkdown' 25 | 26 | # first file 27 | $scriptAsLines = $actual[0].Script.Split("`n") 28 | $scriptAsLines.Count | Should -Be 8 29 | 30 | # second file 31 | $scriptAsLines = $actual[1].Script.Split("`n") 32 | $scriptAsLines.Count | Should -Be 5 33 | } 34 | It "Should analyze markdown strings directly" { 35 | $md = @' 36 | ```powershell 37 | "Hello World" 38 | ``` 39 | '@ 40 | $actual = Invoke-ScriptAnalyzerMarkdown $md 41 | $scriptAsLines = $actual[0].Script.Split("`n") 42 | 43 | $scriptAsLines.Count | Should -Be 1 44 | 45 | } 46 | 47 | It "Should analyze markdown strings piped" { 48 | $md = @' 49 | ```powershell 50 | "Hello World" 51 | ``` 52 | '@ 53 | $actual = $md | Invoke-ScriptAnalyzerMarkdown 54 | $scriptAsLines = $actual[0].Script.Split("`n") 55 | 56 | $scriptAsLines.Count | Should -Be 1 57 | } 58 | 59 | It "Should throw if no Headers param on the function" { 60 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 61 | { Invoke-ScriptAnalyzerMarkdown -Path $url -Headers @{"A" = 1 } } | Should -Not -Throw 62 | } 63 | 64 | It "Should get an error with a private url" { 65 | $url = 'https://raw.githubusercontent.com/dfinke/pstestX/main/test.md' 66 | # $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 67 | $actual = Invoke-ScriptAnalyzerMarkdown -Path $url 68 | 69 | $actual.Path | Should -BeExactly $url 70 | $actual.Error | Should -BeExactly "404: Not Found" 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /__tests__/InvokeScriptFormatterMarkdown.tests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot/../PSPx.psd1 -Force 2 | 3 | Describe "Test Invoke Script Formatter Markdown" -Tag "Invoke-ScriptFormatterMarkdown" { 4 | 5 | BeforeAll { 6 | $rootDir = "$PSScriptRoot/testMarkdownFiles" 7 | } 8 | 9 | It "Should handle formatting script blocks" { 10 | $fileName = $rootDir + "/FormatPSBlocks.md" 11 | 12 | $actual = Invoke-ScriptFormatterMarkdown $fileName 13 | $lines = $actual.Result -split "`n" 14 | 15 | $actual.Cmdlet | Should -BeExactly 'Invoke-ScriptFormatterMarkdown' 16 | 17 | $lines.Count | Should -Be 5 18 | $lines[0] | Should -BeExactly '@{' 19 | $lines[1] | Should -BeExactly " 1 ='a'" 20 | $lines[2] | Should -BeExactly " 10 ='b'" 21 | $lines[3] | Should -BeExactly " 100 ='c'" 22 | $lines[4] | Should -BeExactly '}' 23 | } 24 | } -------------------------------------------------------------------------------- /__tests__/testMarkdownFiles/FormatPSBlocks.md: -------------------------------------------------------------------------------- 1 | # Format the hashtable 2 | 3 | ```powershell 4 | @{ 5 | 1='a' 6 | 10='b' 7 | 100='c' 8 | } 9 | ``` -------------------------------------------------------------------------------- /__tests__/testMarkdownFiles/PSBlocksPSSA-Issues.md: -------------------------------------------------------------------------------- 1 | ```ps 2 | $a = 1 3 | ``` 4 | 5 | ```ps 6 | $b = 2 7 | "Hello World" 8 | ``` -------------------------------------------------------------------------------- /__tests__/testMarkdownFiles/basicPSBlocks.md: -------------------------------------------------------------------------------- 1 | ```ps 2 | "Hello World" 3 | ``` 4 | 5 | # Another block 6 | 7 | ```ps 8 | "Goodbye" 9 | ``` 10 | 11 | # Add some numbers 12 | 13 | ```ps 14 | $xs = 1, 2, 3 15 | foreach ($x in $xs) { 16 | $x 17 | } 18 | ``` 19 | 20 | # Return a string, use a powershell block 21 | 22 | ```powershell 23 | 'This is a powershell block' 24 | ``` 25 | 26 | # Add Numbers 27 | 28 | ```ps1 29 | 3+4+5 30 | ``` -------------------------------------------------------------------------------- /__tests__/testRawMDFiles/hashtable.md: -------------------------------------------------------------------------------- 1 | # a hashtable 2 | 3 | ```ps1 4 | @{ 5 | 10= 'a' 6 | 100= 'a' 7 | 1000= 'a' 8 | } 9 | ``` -------------------------------------------------------------------------------- /__tests__/testRawMDFiles/simple.md: -------------------------------------------------------------------------------- 1 | # This is a simple test 2 | 3 | ```powershell 4 | "Hello World" 5 | ``` -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # 1.4.0 2 | 3 | - Refactored `Get-MarkdownCodeBlock` to return markdown and code blocks 4 | - Update `Invoke-*` functions to work with new refactoring 5 | - Added `Invoke-UpdateScriptFormat`, uses the PSSA formatter to format each code block in place 6 | 7 | 8 | # 1.3.0 9 | 10 | - Added `Invoke-ScriptFormatterMarkdown`. Formats the PowerShell code block using ScriptAnalyzer 11 | 12 | # 1.2.1 13 | 14 | - Support ` ```ps1 ` code block 15 | 16 | # 1.2.0 17 | 18 | - Add -Headers parameter to `Get-MarkdownCodeBlock`, `Invoke-ExecuteMarkdown`, and `Invoke-ScriptAnalyzerMarkdown` 19 | 20 | ```powershell 21 | $url = 'https://private.url.com/test.md' 22 | $header = @{"Authorization"="token $($env:GITHUB_TOKEN)"} 23 | Invoke-ExecuteMarkdown $url $header 24 | ``` 25 | # 1.1.0 26 | 27 | - Added `Invoke-ScriptAnalyzerMarkdown` - Run PowerShell Script Analyzer on code blocks written in markdown 28 | - Refactored, created `Get-MarkdownCodeBlock` - Extracts PowerShell code blocks from markdown 29 | 30 | # 1.0.0 31 | - `Invoke-ExecuteMarkdown` - Execute PowerShell written in markdown code blocks -------------------------------------------------------------------------------- /examples/markdown.md: -------------------------------------------------------------------------------- 1 | # Markdown Scripts 2 | 3 | It's possible to write scripts using markdown. Only code blocks will be executed 4 | by `px`. Try to run `px .\examples\markdown.md | % result`. 5 | 6 | ```ps 7 | $PSVersionTable 8 | $pwd 9 | ``` 10 | 11 | Display the `APPDATA` environment variable: 12 | 13 | ```ps 14 | $env:APPDATA 15 | ``` 16 | 17 | We can create functions to be used in subsequent code blocks: 18 | 19 | ```ps 20 | function Get-Info { 21 | "Test Info $(get-date)" 22 | } 23 | ``` 24 | 25 | Use `Get-Info` here: 26 | 27 | ```ps 28 | Get-Info 29 | ``` 30 | 31 | Other code blocks are ignored: 32 | 33 | ```css 34 | body .hero { 35 | margin: 42px; 36 | } 37 | ``` -------------------------------------------------------------------------------- /media/PSMarkdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinke/PSPx/4a21d776d6316089e90cb5bdda0815b60b875fd6/media/PSMarkdown.png -------------------------------------------------------------------------------- /media/PSOutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinke/PSPx/4a21d776d6316089e90cb5bdda0815b60b875fd6/media/PSOutput.png --------------------------------------------------------------------------------