Invoke-Equation.ps1

function Invoke-Equation
{    
    <#
    .Synopsis
        Invokes a PowerShell script for an equation
    .Description
        Writes a PowerShell script for an equation.
        To make sure that this is safe, you can only do the following things inside of a script.
                         
        Use static methods from the [Math] class.
        Use constant numbers
        Use the following operators
            '=','+', '-','-lt', '*', '-gt','-lt', '-ge','-le', '-ne', '-eq', '%', '/'
             
         
    .Example
        Invoke-Equation -Step {
# Square A
$aSquared = $a * $a
# Square B
$bSquared = $b * $b
# Get the square root of the values
[Math]::Sqrt($aSquared + $bSquared)
        }
    #>

    param(
    # The list of steps. Each step's comments will become
    [ScriptBlock[]]
    $Step,
    
    # The name of the equation. If not directly set, the equation name will become the name of the parent function
    [string]$EquationName,
    
    # If set, waits this amount of time between each step
    [Timespan]$WaitBetweenStep
    ) 
    
    process {

        #Region Filter out Bad Scripts
        $ok = $true
        foreach ($scriptBlock in $step) {
            $tokens = @([Management.Automation.PSParser]::Tokenize($step, [ref]$null))



            for ($i =0 ;$i -lt $tokens.Count; $i++) {
                $Token = $tokens[$i]    
                if ($token.Type -eq 'Command') { 
                    Write-Error "Cannot use commands in the filter"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'Keyword') { 
                    if ('if', 'else', 'elseif' -contains $token.Content) {
                    } 
                    Write-Error "Cannot use keywords in the filter"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'String') {
                    Write-Error "No Strings allowed"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'Type' -and ($token.Content -ne '[Math]' -and $token.Content -ne 'Math')) 
                {
                    Write-Error "Can only use the [Math] class in equations"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'Type') {
                    # Make sure the next item is a static member refrence
                    if ($i -eq ($tokens.Count - 1)) { 
                        Write-Error "May only use methods from [Math]"
                        $ok = $false
                        break
                    } else {
                        $i++
                    }
                }
                if ($token.Type -eq 'GroupStart' -and ('(','{' -notcontains $token.Content)) {
                    Write-Error "Only Parenthesis are allowed in equations"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'GroupEnd' -and (')','}' -notcontains $token.Content)) {
                    Write-Error "Only Parenthesis are allowed in equations"
                    $ok = $false
                    break
                }
                if ($token.Type -eq 'Variable' -or $Token.Type -eq 'Number' -or $token.Type -eq 'GroupEnd') {                                    
                    if (! $tokens[$i +1]) { 
                        break 
                    }           
                    $nextToken = $tokens[$i + 1]
                    $validOperators = '=','+', '-','-lt', '*', '-gt','-lt', '-ge','-le', '-ne', '-eq', '%', '/'
                    if (($nextToken.Type -ne 'Operator' -and 
                        $validOperators -notcontains $nextToken.Type) -and (
                        $nextToken.Type -ne 'Newline'                        
                        ) -and (
                        $nextToken.Type -ne 'GroupEnd'                        
                        )) {
                        Write-Error "`$$($token.Content) must be followed by one of the following operators $validOperators"
                        $ok = $false                        
                    } else {
                        $i = $i + 1
                    }                   
                }   
                if ($token.Type -eq 'Operator') {                    
                    $validOperators = '=','+', '-','-lt', '*', '-gt','-lt', '-ge','-le', '-ne', '-eq', '%', '/'
                    if ($validOperators -notcontains $token.Content) {
                        Write-Error "Unexpected Operator $($token.Content) ($($token.StartLine.ToString() +','+ $token.StartColumn)))"                   
                        $ok = $false
                        break
                    }
                }
                

            }
            foreach ($token in $tokens) { 
                
                if ($token.Type -eq 'Number') {                                    
                    if (! $foreach.MoveNext()) { 
                        break 
                    }           
                    $validOperators = '=','+', '-','-lt', '*', '-gt','-lt', '-ge','-le', '-ne', '-eq', '%', '/'
                    if (($foreach.current.Type -ne 'Operator' -and 
                        $validOperators -notcontains $foreach.current.Content) -and (
                        $foreach.current.Type -ne 'Newline'                        
                        ) -and (
                        $foreach.current.Type -ne 'GroupEnd'                        
                        )) {
                        Write-Error "`$$($token.Content) must be followed by one of the following operators $validOperators"
                        $ok = $false                        
                    }                    
                }
                
                if ($token.Type -eq 'GroupEnd') {                                    
                    if (! $foreach.MoveNext()) { 
                        break 
                    }           
                    $validOperators = '=','+', '-','-lt', '*', '-gt','-lt', '-ge','-le', '-ne', '-eq', '%', '/'
                    if (($foreach.current.Type -ne 'Operator' -and 
                        $validOperators -notcontains $foreach.current.Content) -and (
                        $foreach.current.Type -ne 'Newline'                        
                        ) -and (
                        $foreach.current.Type -ne 'GroupEnd'                        
                        )) {
                        Write-Error "`$$($token.Content) must be followed by one of the following operators $validOperators"
                        $ok = $false                        
                    }                    
                }
                 
                
                
                                                            
            }
        }                   
        
        
        if (-not $Ok) { return }
        #Endregion Filter out Bad Scripts
        
        #region Combine Scripts and get steps/explanations
        $text = $combinedScript = $step -join ([Environment]::NewLine)
        
        $tokens  = [Management.Automation.PSParser]::Tokenize($combinedScript, [ref]$null)
        $work = & { 
            $lastResult = @{Explanation=""}
            foreach ($token in $tokens) {        
                if ($token.Type -eq "Newline") { continue }
                if ($token.Type -ne "Comment" -or $token.StartColumn -gt 1) {
                    $isInContent = $true
                    if (-not $lastToken) { $lastToken = $token } 
                } else {
                    if ($lastToken.Type -ne "Comment" -and $lastToken.StartColumn -eq 1) {
                        $chunk = $text.Substring($lastToken.Start, 
                            $token.Start - 1 - $lastToken.Start)
                        $lastResult.Script = [ScriptBlock]::Create($chunk)
                        # mutliparagraph, split up the results if multiparagraph
                        
                        $paragraphs = @()    
                        $lastResult.Explanation = $lastResult.Explanation.trim()                
                        New-Object PSObject -Property $lastResult                    
                        $null = $paragraphs
                        $lastToken = $null
                        $lastResult = @{Explanation="";}
                        $isInContent = $false                
                    }
                }
                if (-not $isInContent) {
                    $lines = $token.Content.Trim("<>#")
                    $lines = $lines.Split([Environment]::NewLine, [StringSplitOptions]"RemoveEmptyEntries")
                    foreach ($l in $lines) {
                        switch ($l) {                                                   
                            default {
                                $lastResult.Explanation += ($l + [Environment]::NewLine)                        
                            }
                        }
                    }
                }                    
            }
            
            if ($lastToken -and $lastResult) {
                
                $chunk = $text.Substring($lastToken.Start)
                $lastResult.Explanation = $lastResult.Explanation.trim()                
                $lastResult.Script = [ScriptBlock]::Create($chunk)
                New-Object PSObject -Property $lastResult
            } elseif ($lastResult) { 
                $lastResult.Explanation = $lastResult.Explanation.trim()                
                New-Object PSObject -Property  $lastResult
            }
        }
        #endregion Combine Scripts and get steps/explanations
        
        #region Determine Equation Name
        if (-not $psBoundParameters.ContainsKey('EquationName')) {
            $EquationName = Get-PSCallStack | 
                Select-Object -Skip 1 -First 1 -ExpandProperty Command
        }
        #endregion Determine Equation Name
        
        #region Walk thru the equation
        $totalStepCount = @($work).Count
        foreach ($equationStep in $work) {
            Write-Progress "$EquationName " "$($equationStep.Explanation)" 
            $stepNumber++            
            $stepResult = New-Object PSObject -Property @{
                Number = $stepNumber
                Step = "$($equationStep.Explanation)"
                Script = $equationStep.Script
                Result =  . $equationStep.Script
            }            
            if ($stepResult.Result -eq $null) {
                $tokens =[MAnagement.Automation.PSParser]::Tokenize(
                    $equationStep.Script,
                    [ref]$null)
                for ($i =0; $i -lt $tokens.Count; $i++) {
                    $t = $tokens[$i]                    
                    if ($t.Type -eq 'NewLine') { continue }
                    if ($t.Type -eq 'Variable') {
                        $next = $tokens[$i+1]
                        if ($next.Type -eq 'Operator' -and ('=', '+=', '-=', '/=', '*=', '%=' -contains $next.Content)) {
                            $stepResult.Result = Invoke-Expression "`$$($t.Content)"
                        }
                    }
                }
            }            
            $null =$stepResult.pstypenames.Clear()
            $null = $stepResult.pstypenames.add('MathStep')
            if ($showWork) { $stepResult } 
            if ($stepNumber -eq $totalStepCount) {
                if ($showWork) {
                    $result = 
                        New-Object PSOBject -Property @{
                            Number = $stepNumber + 1
                            Step = "Done"
                            Script = ""                            
                            Result = $stepResult.Result
                        }
                    $null = $result.pstypenames.Clear()
                    $null = $result.pstypenames.add('MathResult')
                    $result
                } else {
                    $stepResult.Result
                }                        
            }
            
            
            if ($waitBetweenStep -and $waitBetweenStep.TotalMilliseconds) {
                Start-Sleep -Milliseconds $WaitBetweenStep.TotalMilliseconds
            }
        }
        #endregion
    }
}