Commands/Parsers/Out-Parser.ps.ps1

function Out-Parser
{
    <#
    .Synopsis
        Outputs Parser to PowerShell
    .Description
        Outputs Parser as PowerShell Objects.

        Parser Output can be provided by any number of Commands to Out-Parser.

        Commands use two attributes to indicate if they should be run:

        ~~~PowerShell
        [Management.Automation.Cmdlet("Out","Parser")] # This signals that this is an Command for Out-Parser
        [ValidatePattern("RegularExpression")] # This is run on $ParserCommand to determine if the Command should run.
        ~~~
    .LINK
        Invoke-Parser
    .NOTES
        Out-Parser will generate two events upon completion. They will have the source identifiers of "Out-Parser" and "Out-Parser $ParserArgument"
    #>

    [CmdletBinding(PositionalBinding=$false)]
    param(
    # One or more output lines to parse.
    [Parameter(ValueFromPipeline)]
    [Alias('ParserOutputLines')]
    [string[]]
    $ParserOutputLine,

    # The command line that describes what is being parsed.
    [string]
    $CommandLine,
    
    # The pattern used to identify lines with errors
    [string]
    $ErrorPattern = "^(?:error|fatal):",

    # The pattern used to identify lines with warnings
    [string]
    $WarningPattern = '^hint:',

    # The timestamp. This can be used for tracking. Defaults to [DateTime]::Now
    [DateTime]
    $TimeStamp = [DateTime]::Now
    )

    begin {        
        if ((-not $CommandLine) -and ($MyInvocation.InvocationName -ne $MyInvocation.MyCommand.Name)) {
            $CommandLine = $MyInvocation.InvocationName
        }

        $ParserCommand = $CommandLine

        # Now we need to see if we have have a cached for Command mapping.
        if (-not $script:ParserCommandMappingCache) {
            $script:ParserCommandMappingCache = @{} # If we don't, create one.
        }

        if (-not $script:ParserCommandMappingCache[$ParserCommand]) { # If we don't have a cached Command list
            $script:ParserCommandMappingCache[$ParserCommand] = $PSParser.ForCommand($ParserCommand)            
        }
        # If there was already an Command cached, we can skip the previous steps and just reuse the cached Commands.
        $ParserOutputCommands = $script:ParserCommandMappingCache[$ParserCommand]
    
        # Next we want to create a collection of SteppablePipelines.
        # These allow us to run the begin/process/end blocks of each Command.
        $steppablePipelines =
            [Collections.ArrayList]::new(@(if ($ParserOutputCommands) {
                foreach ($ext in $ParserOutputCommands) {
                    $scriptCmd = {& $ext}
                    $scriptCmd.GetSteppablePipeline()
                }
            }))


        # Next we need to start any steppable pipelines.
        # Each Command can break, continue in it's begin block to indicate it should not be processed.
        $spi = 0
        $spiToRemove = @()
        $beginIsRunning = $false
        # Walk over each steppable pipeline.
        :beginParser foreach ($steppable in $steppablePipelines) {
            if ($beginIsRunning) { # If beginIsRunning is set, then the last steppable pipeline continued
                $spiToRemove+=$steppablePipelines[$spi] # so mark it to be removed.
            }
            $beginIsRunning = $true      # Note that beginIsRunning=$false,
            try {
                $steppable.Begin($true) # then try to run begin
            } catch {
                $PSCmdlet.WriteError($_) # Write any exceptions as errors
            }
            $beginIsRunning = $false     # Note that beginIsRunning=$false
            $spi++                       # and increment the index.
        }

        # If this is still true, an extenion used 'break', which signals to stop processing of it any subsequent pipelines.
        if ($beginIsRunning) {
            $spiToRemove += @(for (; $spi -lt $steppablePipelines.Count; $spi++) {
                $steppablePipelines[$spi]
            })
        }

        # Remove all of the steppable pipelines that signaled they no longer wish to run.
        foreach ($tr in $spiToRemove) {
            $steppablePipelines.Remove($tr)
        }

        $AllParserOutput    = [Collections.Queue]::new()
        $ProcessedOutput = [Collections.Queue]::new()
        $OutputLineCount = 0
    }

    process {
        # Walk over each output.

        :NextOutputLine foreach ($out in $ParserOutputLine) {
            $OutputLineCount++
            # If the out was a literal string of 'System.Management.Automation.RemoteException',
            if ("$out" -eq "System.Management.Automation.RemoteException") {
                # ignore it and continue (these things happen with some exes from time to time).
                continue
            }

            try {
                $AllParserOutput.Enqueue($out)
                
                # Wrap the output in a PSObject
                $ParserOut = [PSObject]::new($out)
            } catch {
                Write-Error "Line $outputLineCount : $_"
                continue
            }
            # Next, clear it's typenames and determine an automatic typename.
            $ParserOut.pstypenames.clear()            
            $ParserOut.pstypenames.add('Parser.output')

            # All ParserOutput should attach the original output line, as well as the command that produced that line.
            $ParserOut.psobject.properties.add([PSNoteProperty]::new('ParserOutput',"$out"))
            $ParserOut.psobject.properties.add([PSNoteProperty]::new('ParserCommand',$CommandLine))

            # If the output started with "error" or "fatal"
            if ("$out" -match $ErrorPattern) {
                $exception = [Exception]::new($("$out" -replace $ErrorPattern)) # Create an exception
                $PSCmdlet.WriteError( # and write an error using $psCmdlet (this simplifies the displayed callstack).
                    [Management.Automation.ErrorRecord]::new($exception,"$ParserCommand", 'NotSpecified',$ParserOut)
                )
                # If there was an error, cancel all steppable pipelines (thus stopping any Commands)
                $steppablePipelines = @()
                continue # then move onto the next output.
            } else {
                Write-Verbose "$out"
            }

            if ("$out" -match $WarningPattern) {
                Write-Warning ("$out" -replace $WarningPattern)
                continue
            }

            if (-not $steppablePipelines) {
                # If we do not have steppable pipelines, output directly
                $ParserOut
            }
            else {
                # If we have steppable pipelines, then we have to do a similar operation as we did for begin.
                $spi = 0
                $spiToRemove = @()
                $processIsRunning = $false
                # We have to walk thru each steppable pipeline,
                :processParser foreach ($steppable in $steppablePipelines) {
                    if ($processIsRunning) {  # if $ProcessIsRunning, the pipeline was skipped with continue.
                        $spiToRemove+=$steppablePipelines[$spi] # and we should add it to the list of pipelines to remove
                    }
                    $processIsRunning = $true # Set $processIsRunning,
                    try {
                        $steppable.Process($ParserOut) | & {
                            process {
                                $ProcessedOutput.Enqueue($_)
                                $_
                            }
                        } # attempt to run process, using the $ParserOut object.
                    } catch {
                        $PSCmdlet.WriteError($_)    # (catch any exceptions and write them as errors).
                    }
                    $processIsRunning = $false # Set $processIsRunning to $false for the next step.
                }


                if ($processIsRunning) {  # If $ProcessIsRunning was true, the Command used break
                    # which should signal cancellation of all subsequent Commands.
                    $spiToRemove += @(for (; $spi -lt $steppablePipelines.Count; $spi++) {
                        $steppablePipelines[$spi]
                    })


                    $ParserOut # We will also output the ParserOut object in this case.
                }

                # Remove any steppable pipelines we need to remove.
                foreach ($tr in $spiToRemove) { $steppablePipelines.Remove($tr) }
            }
        }
    }

    end {
        $global:lastParserOutput = $AllParserOutput.ToArray()

        # End remaining steppable pipelines need to end.
        # Ending does not support the cancellation of other Commands.
        :endParser foreach ($steppable in $steppablePipelines) {
            try {
                $steppable.End() | & { process {
                    $ProcessedOutput.Enqueue($_)
                    $_
                }}
            } catch {
                Write-Error -ErrorRecord $_
            }
        }

        if (-not $global:ParserHistory -or
            $global:ParserHistory -isnot [Collections.IDictionary]) {
            $global:ParserHistory = [Ordered]@{}
        }
        $messageData = [Ordered]@{
            OutputObject  = $ProcessedOutput.ToArray()
            ParserOutputLine = $AllParserOutput.ToArray()
            CommandLine      =  @(@("Parser") + $ParserArgument) -join ' '            
            TimeStamp     = $TimeStamp
        }

        $eventSourceIds = @("Out-Parser","Out-Parser $CommandLine")

        $null =
            foreach ($sourceIdentifier in $eventSourceIds) {
                New-Event -SourceIdentifier $sourceIdentifier -MessageData $messageData
            }

        $global:ParserHistory["$($MyInvocation.HistoryId)::$ParserRoot::$ParserArgument"] = $messageData
    }
}