Reading/Parsing Console Output

Know first, I am pretty new at Powershell but I am here to hopefully learn and get better.

I am working on a script that will log into Cisco devices (routers for the moment) via SSH. The script will then run a command and take the output and save it to a text file. All of this works running just a single command (it’s a work in progress, working towards running more commands). This is not for device configuration, simply to run some show commands (and eventually a ping command). For the moment I am just running a ‘show BGP summary’ command. However, I want to run a show BGP neighbor command and be able to parse the output to read the BGP state (Idle, Active, Established, etc.).

And that is where I am hitting a wall and not sure where to start. When the command 'show bgp neighbor 1.2.3.4 is run, I want the entire output saved to the text file, which I can do, but where do I start in trying to parse that output?

I can share what I have at the moment (if needed) just be warned, its probably going to make some PowerShell expert cringe, but I did not think it would be needed in order to get started on parsing the output.

Parsing output highly depends on how the output is formatted. Usually the output is in columns, and there you often just split the lines on spaces and translate that to psobjects.

Here is an example of how you can parse a simple output. I am saving output of Get-Process (ps) as a string, and then parsing it back to powershell objects.

$output = ps | select -first 10 | select id, si, processname | format-table | out-string
"Original output:"
$output

"Parsed output:"
$lines  = $output -split "`n"
foreach ($line in $lines | Select -Skip 3) {
    $id, $si, $processname = $line.Trim() -split '\s+'
    [pscustomobject] @{
        My_Id = $id
        My_SI = $si
        My_ProcessName = $processname
    }
}


Original output:

   Id SI ProcessName          
   -- -- -----------          
 3360  0 aaHMSvc              
12936  1 AiChargerPlus        
 9488  1 ApplicationFrameHost 
 3320  0 armsvc               
 3416  0 AsusFanControlService
 3340  0 atkexComSvc          
 1212  1 Code                 
 1776  1 Code                 
 2708  1 Code                 
 3144  1 Code                 



Parsed output:

My_Id My_SI My_ProcessName       
----- ----- --------------       
3360  0     aaHMSvc              
12936 1     AiChargerPlus        
9488  1     ApplicationFrameHost 
3320  0     armsvc               
3416  0     AsusFanControlService
3340  0     atkexComSvc          
1212  1     Code                 
1776  1     Code                 
2708  1     Code                 
3144  1     Code                 
                                 
                                 

Can you post sample output of both commands?

Also look at ConvertFrom-String

I could post a sample output, but it will have to wait until I get to work. However, it’s simple Cisco router console output.

Thank you. That looks like it is purpose-built for what I am trying to do. I will look into that and see what I can come up with.

When parsing you want to use a sample file that captures are many output scenarios as possible. I used this sample input file based on https://www.cisco.com/c/en/us/td/docs/ios/iproute_bgp/command/reference/irg_book/irg_bgp5.html

Router# show ip bgp summary 

 BGP router identifier 172.16.1.1, local AS number 100 
 BGP table version is 199, main routing table version 199 
 37 network entries using 2850 bytes of memory 
 59 path entries using 5713 bytes of memory 
 18 BGP path attribute entries using 936 bytes of memory 
 2 multipath network entries and 4 multipath paths 
 10 BGP AS-PATH entries using 240 bytes of memory 
 7 BGP community entries using 168 bytes of memory 
 0 BGP route-map cache entries using 0 bytes of memory 
 0 BGP filter-list cache entries using 0 bytes of memory
 90 BGP advertise-bit cache entries using 1784 bytes of memory 
 36 received paths for inbound soft reconfiguration 
 BGP using 34249 total bytes of memory 
 Dampening enabled. 4 history paths, 0 dampened paths 
 BGP activity 37/2849 prefixes, 60/1 paths, scan interval 15 secs 
  
 Neighbor        V    AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down State/PfxRcd
 10.100.1.1      4   200      26      22      199    0    0 00:14:23 23
 10.200.1.1      4   300      21      51      199    0    0 00:13:40 0

Router# show ip bgp summary

 BGP router identifier 192.168.3.1, local AS number 45000
 BGP table version is 1, main routing table version 1

 Neighbor        V    AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
 *192.168.3.2    4 50000       2       2        0    0    0 00:00:37        0
 * Dynamically created based on a listen range command
 Dynamically created neighbors: 1/(200 max), Subnet ranges: 1

 BGP peergroup group192 listen range group members: 
   192.168.0.0/16

Router# show ip bgp summary

 BGP router identifier 172.17.1.99, local AS number 65538
 BGP table version is 1, main routing table version 1

 Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  Statd
 192.168.1.2     4       65536       7       7        1    0    0 00:03:04      0
 192.168.3.2     4       65550       4       4        1    0    0 00:00:15      0

Router# show ip bgp summary

 BGP router identifier 172.17.1.99, local AS number 1.2
 BGP table version is 1, main routing table version 1

 Neighbor        V           AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  Statd
 192.168.1.2     4         1.0       9       9        1    0    0 00:04:13      0
 192.168.3.2     4        1.14       6       6        1    0    0 00:01:24      0

and this script would parse a file like that:

#Requires -Version 5
#Requires -RunAsAdministrator
# Script to parse 'show ip bgp summary' output - Sam Boutros - 21 November 2017

#region input
$SourceFile = '.\SampleCisco1.txt'
$NewRecordMarker = 'show ip bgp summary'
#endregion

#region Process
# Read input file
$Lines = Get-Content $SourceFile

# Breakdown input file into records based on $NewRecordMarker
$i=0; $RecordStartLines = $Lines | % { if ($_ -match $NewRecordMarker) { $i }; $i++ }
"Identified $($RecordStartLines.Count) records at lines: $($RecordStartLines -join ', ' )"

# Parse each record to create the output PS object
$myOutput = 0..($RecordStartLines.Count-1) | % {
    $RecordStartLine = $RecordStartLines[$_]
    $RecordEndLine = $(
        if ($_ -eq $RecordStartLines.Count-1) {
            $Lines.Count-1
        } else {
            $RecordStartLines[$_+1]-1
        }        
    )

    Remove-Variable PastNeighborline -EA 0
    foreach ($Line in $Lines[$RecordStartLine..$RecordEndLine]) {
        # Get RouterID from 'BGP router identifier' line
        if ($Line -match 'BGP router identifier') { $RouterID = $Line.Split(' ')[4].Replace(',','').Trim() }
        
        # We're only interested in the lines after 'Neighbor' line that has a '.'
        if ($Line -match 'Neighbor') { $PastNeighborline = $true }
        if ($Line -match 'Dynamically created') { $PastNeighborline = $false }
        if ($PastNeighborline -and $Line -match '\.') {  
            $Neighbor = ($Line.Split(' ') | ? { $_ })[0]
            if ($Neighbor -match '\*') { $Neighbor = $Neighbor.Replace('*',''); $Dynamic = $true } else { $Dynamic = $false }
            [PSCustomObject][Ordered]@{
                RouterID = $RouterID
                Neighbor = $Neighbor
                Dynamic  = $Dynamic
                State    = ($Line.Split(' ') | ? { $_ })[9] 
            } # PSCustomObject
        } # if $PastNeighborline 
    } # foreach $Line
} # foreach 0..($RecordStartLines.Count-1) 
#endregion

#region output
# Dedup the output
$myOutput | group RouterID,Neighbor,Dynamic,State | % {
    $_.Group | select RouterID,Neighbor,Dynamic,State -First 1
}
#endregion

giving output like:

RouterID    Neighbor    Dynamic State
--------    --------    ------- -----
172.16.1.1  10.100.1.1    False 23   
172.16.1.1  10.200.1.1    False 0    
192.168.3.1 192.168.3.2    True 0    
172.17.1.99 192.168.1.2   False 0    
172.17.1.99 192.168.3.2   False 0   

Obviously you can output more properties depending on what matters to you…

The following video might help you as well:
6 Weltner Sophisitcated Techniques of Plain Text Parsing

Thank you all for the great replies thus far. Sorry, it has taken so long to respond. 10-hour work days in a NOC on minimal holiday staffing doesn’t leave a lot of time to freeload on the web :slight_smile:

I did realize something today in my approach to this (I told you I was new). My thinking was that I could parse the console output in real-time. Apparently, I was a bit off base in my thinking. So in that regard, I learned something today. It appears I will have to read the output into a file and/or a variable and then pick it apart there, most likely using the suggestion of ConvertFrom-String.

Sam, I can’t thank you enough for the example you gave, but you went so far over my head I will probably spend the next week just trying to pick through and understand your code. However, it looks like you doing pretty much exactly what I am wanting to do.