Search code examples
powershellparsingpowershell-cmdlet

Count commandlet occurence in powershell script at different points


I found some great sample code that autocounts the number of times I call the progress bar cmdlet in a script. Super awesome, then you dont have to manually increment steps and change stuff all the time.

$steps = ([System.Management.Automation.PsParser]::Tokenize((gc "$PSScriptRoot\$($MyInvocation.MyCommand.Name)"), [ref]$null) | where { $_.Type -eq 'Command' -and $_.Content -eq 'Write-ProgressHelper' }).Count

However! I am building a giant script that has multiple sections within it, each time I reboot I start the script at a different point.

I would like to display the overall progress of the ENTIRE installation, so at the start of each step I need to know how many occurrences of Write-Progress occurred before the section that current runs.

Ex: if I call the script to start at point 2, the progress bar should be starting at 4/7.

Is there a way to count the occurrences at a certain point in the script? The tokenize just collects the whole script.

Param(
    [Parameter(Mandatory=$True)]
    [int]
    $startstep
    #starting step for script, called with 

    )



        function Write-ProgressHelper {
        param(
            [int]$StepNumber,
            [string]$Message
        )
    
        Write-Progress -ID 0 -Activity 'Installation Part 1' -Status $Message -PercentComplete (($StepNumber / $steps) * 100)
        #call in code with Write-ProgressHelper -Message 'Doing something' -StepNumber ($step++)
    }
    
        $steps = ([System.Management.Automation.PsParser]::Tokenize((gc "$PSScriptRoot\$($MyInvocation.MyCommand.Name)"), [ref]$null) | where { $_.Type -eq 'Command' -and $_.Content -eq 'Write-ProgressHelper' }).Count
        $step = 0








    if ($startstep -eq 1){
        #count the number of occurences here and set STEP to that value (4 in this case)
        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5

        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5


        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5


        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5
    }


    if ($startstep-eq 2){
        write-host "Step $startstep"
        Write-ProgressHelper -Message 'Part 2' -StepNumber ($step++)
        sleep -Seconds 5


        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5

        write-host "Step $startstep"
        Write-ProgressHelper -Message 'part 1se' -StepNumber ($step++)
        sleep -Seconds 5
    }




Solution

  • This can be done using the AST (abstract syntax tree).

    • Find the if statements that check for the right value of $startstep.
    • Count the number of Write-ProgressHelper invocations within the bodies of these if statements.
    # these variables must exist and will be filled by reference later:
    $tokens = $errors = $null
    
    # parse the current script
    $ast = [Management.Automation.Language.Parser]::ParseFile( $MyInvocation.MyCommand.Path, [ref] $tokens, [ref] $errors)
    
    # get total number of invocations of Write-ProgressHelper
    $steps = $ast.FindAll({ param($item) 
        $item -is [Management.Automation.Language.CommandAst] -and $item.GetCommandName() -eq 'Write-ProgressHelper' 
    }, $true).Count
    
    # get if-statements that check for less than current value of $startstep
    $ifAsts = $ast.FindAll({ param($item) 
        if( $item -isnot [Management.Automation.Language.IfStatementAst] ) { return $false }
        # Item1 contains the AST of the if statement condition
        $item.Clauses.Item1.Extent.Text -match '\$startstep -eq (\d+)' -and ([int] $matches[1]) -lt $startstep 
    }, $true) 
    
    # get number of invocations of Write-ProgressHelper within body of matching if-statements 
    $step = 1
    $ifAsts | ForEach-Object {
        # Item2 contains the AST of the if statement body
        $step += $_.Clauses.Item2.FindAll({ param($item) 
            $item -is [Management.Automation.Language.CommandAst] -and $item.GetCommandName() -eq 'Write-ProgressHelper' 
        }, $true).Count
    }
    

    A good starting point for further experimentation is to list all items of the AST like this:

    $ast.FindAll({ $true }, $true)