Category Archives: Scripting/Shortcuts

Python goodness

I’m just about to finish implementing a NAC solution using ClearPass to do 802.1x and ran into some challenges with our current infrastructure design…so I have to change it.

Basically the problem is that to do dynamic vlan assignment you can either have a standardized vlan structure for your access switches (we don’t), or you have to build out a crazy complex profile logic to accommodate the existing vlan structure.

I figure for the ease of support later it’s probably better to standardize the access vlans across the organization. That means that I’d have to touch every access switch in the org. There’s no way I’m doing that manually.

Typically I would do this in VB because it’s what I’m used to but that language is pretty old and it’d be better if I knew Python, so I decided to take this opportunity to learn Python and do it that way instead.

Man am I glad I did.

The script I wrote takes the whole config file and parses it for the needed information. Because we actually had a standard subnet scheme this was a lot easier because I could work off of that to identify the parts of the config that needed to be changed. I pulled all that information out and automatically ran it through a template config I made (you know…a STANDARD!) to spit out the config changes that needed to be made on every switch.

Turns out I wasn’t done there because somebody back before I got here decided to use every access switch as a DHCP server. Ugh. We’re not leaving that in there. I mean, if I’m doing this already I might as well fix that too.

So I added some functions to dump out the networks that we needed to create DHCP scopes and client reservations for in our windows DHCP servers. One of our systems guys used that to create a powershell script to automate the scope creation.

I think I’m in love with Python. It’s pretty easy to read, the tab/space formatting is forced on you (not that I needed it, I can’t stand reading code that’s not formatted properly), and it’s FAST. OMG, so much faster than VB. File manipulation is nice and smooth and it only took me a little bit of time to wrap my head around the dynamic variable types and the output implications that go along with it.

Some personal events that have happened over the last year have already got me thinking about a really cool side project and Python is clearly the tool for the job.

Now I have to decide how much to focus on being a better coder vs. completing that pesky CCIE lab.

Why scripting will save you Pt2

continued from Pt1

2) Scripts in Excel, Access, whatever other program you like to use

Looking back I should have done this next task in Access where I get to use sql commands, but everybody has Excel, so this seemed like a better choice should I get hit by a bus or something.

I’m not a professional programmer.  I know I do things that are not necessarily correct or pretty.  I have a tendency to use functions instead of subs because I like to use the return value of the function during debug.   I’m sure I have other bad programming habits that would drive some people crazy, but at the end of the day I can get the job done and make my life easier when the day of a change comes.

Here’s some code that takes a log file, dumps it into a new sheet with a timestamp and then pulls the vlan info I need, Vlan ID, Root Bridge, and any blocking ports into an existing sheet.  It will do this for Cisco IOS switches, CatOS switches, and JunOS switches.

What we’re expecting in the log files is pretty specific here: CatOS “show spantree x” output, IOS “show spanning-tree” output, and Junos “show spanning-tree bridge” and “show spanning-tree interface x” output.   You can tweak this code pretty easily to look for other things…I know I’m holding onto it for future tasks.

 
 ‘this imports a file which must be named with the exact hostname of the device
‘and parses the file displaying the vlans: root bridge and bridge ID
‘and the interfaces: forwarding or blocking per vlan

Sub Main()

Dim RouterId As String
Dim PromptTxt As String
Dim throwaway As Integer
Dim filePath As String
Dim devicetype As String
Dim whileBreaker As Integer
Dim sheetname As String
Dim SheetList(10, 10) As String

‘WS is current worksheet and opens a new sheet at the beginning of the run
‘I might need to move this to the functions that import the files….
Dim WS As Worksheet
Set WS = Sheets.Add
‘get the log file to parse
filePath = Application.GetOpenFilename

‘chop out the routerid from the filepath
‘this assumes that the filename is the router-id
RouterId = GetFilenameFromPath(filePath)
RouterId = Left(RouterId, Len(RouterId) – 4)

‘so the output of a couple of switches changes with the version.  some didn’t have a > others did…quick fix below
If RouterId = “switch3” Or RouterId = “switch4” Then
    RouterId = RouterId & “>”
End If

‘asks for the type of file to parse
whileBreaker = 0
While whileBreaker = 0
     PromptTxt = “Enter device type.  Only CatOS, iOS, and Junos are accepted”
    devicetype = InputBox(PromptTxt)
    If devicetype = “CatOS” Or devicetype = “iOS” Or devicetype = “Junos” Then
        whileBreaker = 1
    End If
Wend
‘Changes current sheet name to reflect the file about to be read in
sheetname = Time()
sheetname = Replace(sheetname, “:”, “-“)
sheetname = RouterId & ” ” & Replace(sheetname, “/”, “-“)
ActiveSheet.Name = sheetname
‘import the file into the new sheet
ImportFile (filePath)

‘Parse the output sheet based on device type
    throwaway = Parselog(sheetname, RouterId, devicetype)

End Sub
 ________________________________________________________________________
Function GetFilenameFromPath(ByVal strPath As String) As String
‘ Returns the rightmost characters of a string upto but not including the rightmost ‘\’
‘ e.g. ‘c:\winnt\win.ini’ returns ‘win.ini’

    If Right$(strPath, 1) <> “\” And Len(strPath) > 0 Then
        GetFilenameFromPath = GetFilenameFromPath(Left$(strPath, Len(strPath) – 1)) + Right$(strPath, 1)
    End If

End Function
________________________________________________________________________________________________

Sub ImportFile(Logfilepath As String)
‘takes the file name, reads it into a string, then uses WriteToExcel to line import it into a new sheet

Dim Streng As String
Dim StrFileArray() As String
Dim hFile As Long

hFile = FreeFile

Open Logfilepath For Input As #hFile
    Streng = Input$(LOF(hFile), hFile)
Close #hFile
 StrFileArray = Split(Streng, vbCrLf)
 WriteToExcel (StrFileArray)
 End Sub

____________________________________________________________________________________________________
Function WriteToExcel(StrArray)
‘takes the array output from ImportFile and writes it into the current sheet starting at A1
Dim counter As Integer
Dim cellname As String

For counter = LBound(StrArray) To UBound(StrArray)
    cellname = “A” & (counter + 1)
    Range(cellname).Value = StrArray(counter)
Next counter
End Function
________________________________________________________________________________________________________
Function Parselog(ByVal workingSheetname As String, switchname As String, devicetype As String) As Integer

Dim showVlancells() As String
Dim DRCells() As String
Dim ARTemp(4) As String
Dim BlockingCells() As String
Dim searchString As String
Dim bFound As Boolean ‘Flag
Dim sRange As Range
Dim rowCounter As String
Dim Lastcell As String
Dim throwaway As Integer
Dim sheetname As Worksheet
Dim counter As Integer
Dim rFnd As Range
Dim iArr As Integer ‘ Counter for Array
Dim rFirstAddress ‘ Address of the First Find
Dim x As Integer

‘count how much to search and set sheetname to activesheet
Lastcell = Range(“A65536”).End(xlUp).Row
rowCounter = “A2:A” & Lastcell
Set sheetname = ActiveSheet

‘start parsing based on CatOS
‘__________________________________________________CATOS_________________________________________________
If devicetype = “CatOS” Then
    searchString = ” show spantree”
    searchString = switchname & searchString
    ‘look for all instances of “routerid show spantree ” and record those cells
    bFound = FindAll(searchString, sheetname, rowCounter, showVlancells())
    ‘look between values in showVlancells()for designated root cells
    ‘set the counter properly to not error at the end of the sheet
    Erase DRCells()
    For counter = 1 To UBound(showVlancells)
        searchString = “Designated Root    ”
        If counter = UBound(showVlancells) Then
            rowCounter = showVlancells(counter) & “:A” & Lastcell
        Else
            rowCounter = showVlancells(counter) & “:” & showVlancells(counter + 1)
        End If
        ‘start the search for DR
            Set rFnd = Nothing
            Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)

            ReDim Preserve DRCells(counter)
            If Not rFnd Is Nothing Then
                DRCells(counter) = rFnd.Address
            Else
                DRCells(counter) = ” ”
            End If

    Next counter
    ‘now I have 1:1 arrays with the vlan number and DR…at least I should
    throwaway = MsgBox(“These should match and be one more than the count from the SecureCRT script” & vbCrLf & UBound(showVlancells) & vbCrLf & UBound(DRCells), vbOKOnly)
    ‘now I need to get all the blocking ports
    ReDim BlockingCells(UBound(showVlancells), 4)
    For counter = 1 To UBound(showVlancells)
        searchString = “blocking”
        If counter = UBound(showVlancells) Then
            rowCounter = showVlancells(counter) & “:A” & Lastcell
        Else
            rowCounter = showVlancells(counter) & “:” & showVlancells(counter + 1)
        End If

        bFound = FindBlocking(searchString, sheetname, rowCounter, ARTemp())
        ‘only copy elements that matter
        If ARTemp(1) <> “” Then
        ‘throwaway = MsgBox(ARTemp(1), vbOKOnly)
            For x = 1 To UBound(ARTemp)
                BlockingCells(counter, x) = ARTemp(x)
            Next x
        End If

    Next counter

throwaway = DumpToOutputCat(showVlancells(), DRCells(), BlockingCells(), workingSheetname, switchname)
End If

‘_______________________________________________________IOS _____________________________________________________

If devicetype = “iOS” Then

    searchString = “VLAN0”
    ‘look for all instances of “routerid show spantree ” and record those cells
    bFound = FindAll(searchString, sheetname, rowCounter, showVlancells())
    ‘look between values in showVlancells()for designated root cells
    ‘set the counter properly to not error at the end of the sheet
    Erase DRCells()
    For counter = 1 To UBound(showVlancells)
        searchString = “Root ID ”
        If counter = UBound(showVlancells) Then
            rowCounter = showVlancells(counter) & “:A” & Lastcell
        Else
            rowCounter = showVlancells(counter) & “:” & showVlancells(counter + 1)
        End If
        ‘start the search for DR
            Set rFnd = Nothing
            Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)

            ReDim Preserve DRCells(counter)
            ‘ios output isn’t formatted friendly for this kind of search so I need to increment the drcells up one
            If Not rFnd Is Nothing Then
                Set rFnd = rFnd.Offset(1, 0)
                DRCells(counter) = rFnd.Address
            Else
                DRCells(counter) = ” ”
            End If

    Next counter
    ‘now I have 1:1 arrays with the vlan number and DR…at least I should
    throwaway = MsgBox(“These should match and be one more than the count from the SecureCRT script” & vbCrLf & UBound(showVlancells) & vbCrLf & UBound(DRCells), vbOKOnly)

    ‘now I need to get all the blocking ports
    ReDim BlockingCells(UBound(showVlancells), 4)
    For counter = 1 To UBound(showVlancells)
        searchString = “BLK”
        If counter = UBound(showVlancells) Then
            rowCounter = showVlancells(counter) & “:A” & Lastcell
        Else
            rowCounter = showVlancells(counter) & “:” & showVlancells(counter + 1)
        End If

        bFound = FindBlocking(searchString, sheetname, rowCounter, ARTemp())
        ‘only copy elements that matter
        If ARTemp(1) <> “” Then
        ‘throwaway = MsgBox(ARTemp(1), vbOKOnly)
            For x = 1 To UBound(ARTemp)
               BlockingCells(counter, x) = ARTemp(x)
            Next x
        End If

    Next counter
throwaway = DumpToOutputiOS(showVlancells(), DRCells(), BlockingCells(), workingSheetname, switchname)

End If
‘______________________________________________________Junos Section__________________________________________
If devicetype = “Junos” Then
    searchString = “STP bridge parameters for VLAN ”
    ‘look for all instances of “STP bridge parameters for VLAN ” and record those cells
    bFound = FindAll(searchString, sheetname, rowCounter, showVlancells())
    ‘look between values in showVlancells()for designated root cells
    ‘set the counter properly to not error at the end of the sheet
    Erase DRCells()
    For counter = 1 To UBound(showVlancells)
        searchString = ”  Root ID”
        If counter = UBound(showVlancells) Then
            rowCounter = showVlancells(counter) & “:A” & Lastcell
        Else
            rowCounter = showVlancells(counter) & “:” & showVlancells(counter + 1)
        End If
       ‘start the search for DR
            Set rFnd = Nothing
            Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)

            ReDim Preserve DRCells(counter)
            If Not rFnd Is Nothing Then
                DRCells(counter) = rFnd.Address
            Else
                DRCells(counter) = ” ”
            End If

    Next counter
throwaway = MsgBox(“These should match and be one more than the count from the SecureCRT script” & vbCrLf & UBound(showVlancells) & vbCrLf & UBound(DRCells), vbOKOnly)

‘    ‘now I have 1:1 arrays with the vlan number and DR…at least I should

‘instead of returning the cells that contain the string I just want a pop-up to tell me that something is blocking
‘because I’ll have to manually figure out why it’s blocking anyway

searchString = “BLK”
Set rFnd = Nothing
Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)
If Not rFnd Is Nothing Then
    throwaway = MsgBox(“Something is blocking in cell: ” & rFnd.Address, vbOKOnly)
End If

throwaway = DumpToOutputJunos(showVlancells(), DRCells(), BlockingCells(), workingSheetname, switchname)

End If

End Function
______________________________________________________________________________________________________________
Function FindAll(ByVal searchString As String, ByRef sheetname As Worksheet, ByRef rowCounter As String, ByRef showVlancells() As String) As Boolean

‘ ————————————————————————————————————–
‘ FindAll – To find all instances of the1 given string and return the row numbers.
‘ If there are not any matches the function will return false
‘ ————————————————————————————————————–

‘On Error GoTo Err_Trap

Dim rFnd As Range ‘ Range Object
Dim iArr As Integer ‘ Counter for Array
Dim rFirstAddress ‘ Address of the First Find
Dim throwaway As Integer
Dim tossstring As String

‘ —————–
‘ Clear the Array
‘ —————–
Erase showVlancells

Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)
If Not rFnd Is Nothing Then
    rFirstAddress = rFnd.Address
    Do Until rFnd Is Nothing
        iArr = iArr + 1
        ReDim Preserve showVlancells(iArr)
        showVlancells(iArr) = rFnd.Address ‘ rFnd.Row ‘ Store the Row where the text is found
        Set rFnd = sheetname.Range(rowCounter).FindNext(rFnd)
        If rFnd.Address = rFirstAddress Then Exit Do ‘ Do not allow wrapped search
    Loop

FindAll = True
Else
‘ ———————-
‘ No Value is Found
‘ ———————-
FindAll = False
End If

‘ ———————–
‘ Error Handling
‘ ———————–
Err_Trap:
If Err <> 0 Then
MsgBox Err.Number & ” ” & Err.Description, vbInformation, “Find All”

Err.Clear
FindAll = False
Exit Function
End If
End Function
____________________________________________________________________________________________________________
Function FindBlocking(ByVal searchString As String, ByRef sheetname As Worksheet, ByRef rowCounter As String, ByRef ARTemp() As String) As Boolean

‘ ————————————————————————————————————–
‘ FindAll – To find all instances of the1 given string and return the row numbers.
‘ If there are not any matches the function will return false
‘ ————————————————————————————————————–

‘On Error GoTo Err_Trap

Dim rFnd As Range ‘ Range Object
Dim iArr As Integer ‘ Counter for Array
Dim rFirstAddress ‘ Address of the First Find
Dim throwaway As Integer
Dim tossstring As String

‘ —————–
‘ Clear the Array
‘ —————–
Erase ARTemp

Set rFnd = sheetname.Range(rowCounter).Find(What:=searchString, LookIn:=xlValues, LookAt:=xlPart)
If Not rFnd Is Nothing Then
    rFirstAddress = rFnd.Address
    Do Until rFnd Is Nothing
        iArr = iArr + 1
‘        ReDim Preserve ARTemp(iArr)  ‘this may need to come back later
        ARTemp(iArr) = rFnd.Address ‘ rFnd.Row ‘ Store the Row where the text is found
        Set rFnd = sheetname.Range(rowCounter).FindNext(rFnd)
        If rFnd.Address = rFirstAddress Then Exit Do ‘ Do not allow wrapped search
    Loop

FindBlocking = True
Else
‘ ———————-
‘ No Value is Found
‘ ———————-
FindBlocking = False
End If

‘ ———————–
‘ Error Handling
‘ ———————–
Err_Trap:
If Err <> 0 Then
MsgBox Err.Number & ” ” & Err.Description, vbInformation, “Find All”

Err.Clear
FindBlocking = False
Exit Function
End If
End Function

Function DumpToOutputCat(vlan() As String, DR() As String, Blocking() As String, sheetname As String, switchname As String)

‘Dim workingRow As Integer
‘Dim wokingCol As String
Dim workcounter As Integer
Dim Lastcell As Integer
Dim cRange As String
Dim throwaway As Integer
Dim vlanNumber() As String
Dim vlanid As String
Dim drID As String
Dim drNumber() As String
Dim blockint() As String
Dim x
Dim blockcellId() As String
Dim blockId As String

‘need to make Output the working sheet
Sheets(“Output”).Select

‘find the end of the sheet
Lastcell = Range(“A65536”).End(xlUp).Row

‘dump the output of vlans

For workcounter = 1 To UBound(vlan)
    ‘write the vlan ID’s
    cRange = “B” & Lastcell + workcounter
    vlanNumber() = Split(Sheets(sheetname).Range(vlan(workcounter)).Value, ” “)
    vlanid = vlanNumber(UBound(vlanNumber))
    Range(cRange).Value = vlanid
    ‘write the switch name
    cRange = “A” & Lastcell + workcounter
    Range(cRange).Value = switchname
    ‘write the DR
    cRange = “C” & Lastcell + workcounter
    Erase drNumber
    drID = ” ”

        If DR(workcounter) <> ” ” Then
            drNumber() = Split(Sheets(sheetname).Range(DR(workcounter)).Value, ” “)
            drID = drNumber(UBound(drNumber))
        End If

    Range(cRange).Value = drID
    ‘now you need to write the blocking ports….
    cRange = “D” & Lastcell + workcounter
    Erase blockcellId
    For x = 1 To UBound(Blocking, 2)
        ReDim Preserve blockcellId(x)
        If Blocking(workcounter, x) <> “” Then
            blockcellId(x) = Sheets(sheetname).Range(Blocking(workcounter, x)).Value
            blockint = Split(blockcellId(x), ” “)
            blockcellId(x) = blockint(1)
        End If
    Next x
    blockId = Join(blockcellId, ” “)
    Range(cRange).Value = blockId

Next workcounter

End Function
_______________________________________________________________________________________________________________
Function DumpToOutputJunos(vlan() As String, DR() As String, Blocking() As String, sheetname As String, switchname As String)

‘Dim workingRow As Integer
‘Dim wokingCol As String
Dim workcounter As Integer
Dim Lastcell As Integer
Dim cRange As String
Dim throwaway As Integer
Dim vlanNumber() As String
Dim vlanid As String
Dim drID As String
Dim drNumber() As String
Dim blockint() As String
Dim x
Dim blockcellId() As String
Dim blockId As String

‘need to make Output the working sheet
Sheets(“Output”).Select

‘find the end of the sheet
Lastcell = Range(“A65536”).End(xlUp).Row

‘dump the output of vlans

For workcounter = 1 To UBound(vlan)
    ‘write the vlan ID’s
    cRange = “B” & Lastcell + workcounter
    vlanNumber() = Split(Sheets(sheetname).Range(vlan(workcounter)).Value, ” “)
    vlanid = vlanNumber(UBound(vlanNumber))
    Range(cRange).Value = vlanid
    ‘write the switch name
    cRange = “A” & Lastcell + workcounter
    Range(cRange).Value = switchname
    ‘write the DR
    cRange = “C” & Lastcell + workcounter
    Erase drNumber
    drID = ” ”

        If DR(workcounter) <> ” ” Then
            drNumber() = Split(Sheets(sheetname).Range(DR(workcounter)).Value, ” “)
            drID = drNumber(UBound(drNumber))
        End If

    Range(cRange).Value = drID
    ‘now you need to write the blocking ports….
    cRange = “D” & Lastcell + workcounter
    Erase blockcellId

Next workcounter

End Function
___________________________________________________________________________________________________________
Function DumpToOutputiOS(vlan() As String, DR() As String, Blocking() As String, sheetname As String, switchname As String)

‘Dim workingRow As Integer
‘Dim wokingCol As String
Dim workcounter As Integer
Dim Lastcell As Integer
Dim cRange As String
Dim throwaway As Integer
Dim vlanNumber() As String
Dim vlanid As String
Dim drID As String
Dim drNumber() As String
Dim blockint() As String
Dim x
Dim blockcellId() As String
Dim blockId As String

‘need to make Output the working sheet
Sheets(“Output”).Select

‘find the end of the sheet
Lastcell = Range(“A65536”).End(xlUp).Row

‘dump the output of vlans

For workcounter = 1 To UBound(vlan)
    ‘write the vlan ID’s
    cRange = “B” & Lastcell + workcounter
    vlanNumber() = Split(Sheets(sheetname).Range(vlan(workcounter)).Value, ” “)
    vlanid = vlanNumber(UBound(vlanNumber))
    Range(cRange).Value = vlanid
    ‘write the switch name
    cRange = “A” & Lastcell + workcounter
    Range(cRange).Value = switchname
    ‘write the DR
    cRange = “C” & Lastcell + workcounter
    Erase drNumber
    drID = ” ”

        If DR(workcounter) <> ” ” Then
            drNumber() = Split(Sheets(sheetname).Range(DR(workcounter)).Value, ” “)
            drID = drNumber(UBound(drNumber))
        End If

    Range(cRange).Value = drID
    ‘now you need to write the blocking ports….
    cRange = “D” & Lastcell + workcounter
    Erase blockcellId
    For x = 1 To UBound(Blocking, 2)
        ReDim Preserve blockcellId(x)
        If Blocking(workcounter, x) <> “” Then
            blockcellId(x) = Sheets(sheetname).Range(Blocking(workcounter, x)).Value
            blockint = Split(blockcellId(x), ” “)
            blockcellId(x) = blockint(0)

        End If
    Next x
    blockId = Join(blockcellId, ” “)
    Range(cRange).Value = blockId

Next workcounter

End Function

Why scripting will save you PT1

In a previous post I talked about documentation and planning for a change, but what can we do to really shorten the time it takes to implement and verify a change?

Scripting.

If we script things out ahead of time we don’t have to use our valuable time during a change window to type things out.  Plus we get to check, double-check, test, and debug all ahead of time to make sure things go how we want them to.

Here are a couple of scripts I’ve used lately to help me get info that I need quickly and format it so that it’s easier to look at.

1) Scripts in SecureCRT.

If you don’t own SecureCRT go buy it.  You can try to get everything done in putty, but a good terminal program will take you to a new level.

You can use several different scripting languages to help you out here.  You can do simple things like have it type commands for you, or complex things like read outputs and make decisions based on what comes out on the terminal.  It’s great for data entry type tasks that are horribly repetitive  but sill need to get done.

This is an example of a script I put together to go through and enter a show command for a list of vlans on a CatOS switch.  It’s sloppy from a code perspective, but it was fast and gets the job done.  (I’m working in VBScript in this case)
#$Language=”VBScript”
#$Interface=”1.0″
Sub Main
Dim counter
‘generic counter variable
Dim Arraysize
Dim RouterID
arr_VlanSet = Array(“1”, “2”, …keep listing your vlans here)
‘sloppy way to populate the vlanset…you can pull this from another file or whatever, but that’s more effort
‘than I wanted to put into this simple script
‘Creates a linear array for holding list of vlans
Arraysize = UBound(arr_VlanSet)
counter = MsgBox(Arraysize, vbOKOnly)
RouterID = “hostname of device goes here”
crt.Screen.Synchronous = True
For counter = 0 To Arraysize

crt.Screen.Send “show spantree ” & arr_vlanset(counter) & vbCr
if crt.screen.WaitForString(“–More”, 1) then
crt.Screen.Send ” ”
end if
if crt.screen.WaitForString(“–More”, 1) then
crt.Screen.Send ” ”
end if
if crt.screen.WaitForString(“–More”, 1) then
crt.Screen.Send ” ”
end if

crt.Screen.Send ” ” & vbCr

crt.screen.WaitForString(RouterID)

Next
End Sub
Remember, these are supposed to save you time on the day, so you don’t need to be elegant in the code.  This goes through my list of vlans, put in the command, waits to see if a space needs to be entered (it does this 3 times) and then goes on to the next command.  If you have longer output than 3 screens it’ll wait for you to put in a keystroke manually instead of just going and missing a command.

 

More in PT2