Start-TypedDemo.ps1

#requires -modules @{ModuleName='PSReadline';ModuleVersion='2.0.0'}

Function Start-TypedDemo {
    [cmdletBinding(DefaultParameterSetName = "Random")]
    [Alias("std")]

    Param(
        [Parameter(Position = 0, Mandatory = $True, HelpMessage = "Enter the name of a text file with your demo commands")]
        [ValidateScript( {Test-Path $_})]
        [string]$File,
        [ValidateScript( {$_ -gt 0})]
        [Parameter(ParameterSetName = "Static")]
        [int]$Pause = 80,
        [Parameter(ParameterSetName = "Random")]
        [ValidateScript( {$_ -gt 0})]
        [int]$RandomMinimum = 50,
        [Parameter(ParameterSetName = "Random")]
        [ValidateScript( {$_ -gt 0})]
        [int]$RandomMaximum = 140,
        [Parameter(ParameterSetName = "Random")]
        [string]$Transcript,
        [switch]$NoExecute,
        [switch]$NewSession
    )

    #this is an internal function so I'm not worried about the name
    Function PauseIt {
        [cmdletbinding()]
        Param()
        Write-Verbose "PauseIt"

        #wait for a key press
        $Running = $true
        #keep looping until a key is pressed
        While ($Running) {
            if ($host.ui.RawUi.KeyAvailable) {
                $key = $host.ui.RawUI.ReadKey("NoEcho,IncludeKeyDown")
                if ($key) {
                    $Running = $False
                    #check the value and if it is q or ESC, then bail out
                    if ($key -match "q|27") {
                        Microsoft.PowerShell.Utility\Write-Host "`r"
                        Return "quit"
                    } #if match q|27
                } #if $key
            } #if key available
            Start-Sleep -millisecond 100
        } #end While
    } #PauseIt function

    #abort if running in the ISE
    if ($host.name -match "PowerShell ISE") {
        Write-Warning "This will not work in the ISE. Use the PowerShell console host."
        Return
    }

    Clear-Host

    if ($NewSession) {
        #simulate a new PowerShell session
        #define a set of coordinates
        $z = new-object System.Management.Automation.Host.Coordinates 0, 0

        #get a header based on what version you are using.
        Switch -Regex ($PSVersionTable.PSVersion.toString()) {
            "^5.1" {
$header = @"
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.
 
Try the new cross-platform PowerShell https://aka.ms/pscore6
 
"@

            } #5.1
           "^7.0" {
$header = @"
PowerShell 7.0.0
Copyright (c) Microsoft Corporation. All rights reserved.
 
https://aka.ms/powershell
Type 'help' to get help.
 
"@

            } #7.0
            Default {
                Write-Warning "This function only supports Windows PowerShell 5.1 or PowerShell 7."
                #abort the command
                return
            }
        } #switch

        Microsoft.PowerShell.Utility\Write-Host $header
    } #if new session

    if ($Transcript) {
        Try {
            $RunningTranscript = $True
            $startTranscript = @"
 
*******************************
PowerShell transcript start
Start time: $(Get-Date)
*******************************
 
"@

            $startTranscript | Out-File -filepath $Transcript -Encoding ascii -erroraction Stop
        }
        Catch {
            Write-Warning "Could not start a transcript. One may already be running."
        }
    }
    else {
        $RunningTranscript = $False
    }
    #strip out all comments and blank lines
    Write-Verbose "Getting commands from $file"

    $commands = Get-Content -Path $file | Where-Object {$_ -notmatch "#" -AND $_ -match "\w|::|{|}|\(|\)"}

    $count = 0

    #write a prompt using your current prompt function
    Write-Verbose "prompt"
    Microsoft.PowerShell.Utility\Write-Host $(prompt) -NoNewline

    $NoMultiLine = $True
    $StartMulti = $False

    #define a scriptblock to get typing interval
    Write-Verbose "Defining interval scriptblock"
    $interval = {
        if ($pscmdlet.ParameterSetName -eq "Random") {
            #get a random pause interval
            Get-Random -Minimum $RandomMinimum -Maximum $RandomMaximum
        }
        else {
            #use the static pause value
            $Pause
        }
    } #end Interval scriptblock

    Write-Verbose "Defining PipeCheck Scriptblock"
    #define a scriptblock to pause at a | character in case an explanation is needed
    $PipeCheck = {
        if ($command[$i] -eq "|") {
            If ((PauseIt) -eq "quit") {Return}
        }
    } #end PipeCheck scriptblock

    Write-Verbose "Processing commands"
    foreach ($command in $commands) {
        #trim off any spaces
        $command = $command.Trim()

        $count++
        #pause until a key is pressed which will then process the next command
        if ($NoMultiLine) {
            If ((PauseIt) -eq "quit") {Return}
        }

        #SINGLE LINE COMMAND
        if ($command -ne "::" -AND $NoMultiLine) {
            Write-Verbose "single line command"

            for ($i = 0; $i -lt $command.length; $i++) {

                #write the character
                Write-Verbose "Writing character $($command[$i])"
                Microsoft.PowerShell.Utility\Write-Host $command[$i] -NoNewline

                #insert a pause to simulate typing
                Start-Sleep -Milliseconds $(&$Interval)

                &$PipeCheck

            }

            #remove the backtick line continuation character if found
            if ($command.contains('`')) {
                $command = $command.Replace('`', "")
            }

            #Pause until ready to run the command
            If ((PauseIt) -eq "quit") {Return}
            Microsoft.PowerShell.Utility\Write-Host "`r"
            #execute the command unless -NoExecute was specified
            if ($RunningTranscript) {
              "$(prompt)$Command" | Out-File -filepath $Transcript -Encoding ascii -erroraction Stop -Append
            }
            if (-NOT $NoExecute) {
                [datetime]$start = [datetime]::now
                $h=@{
                    PSTypeName = "Microsoft.PowerShell.Commands.HistoryInfo"
                    CommandLine = $Command
                    StartExecutionTime = $start
                }

                Invoke-Expression $command -OutVariable r | Out-Default
                if ($RunningTranscript) {
                    $r | Out-File -filepath $Transcript -Encoding ascii -Append -ErrorAction stop
                }


                #Add to PSReadline History
                [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($command)

                #Add to command history
                [datetime]$end = [datetime]::now
                $h.Add("EndExecutionTime", $end)
                $h.Add("ExecutionStatus","Completed")
                if ($psversiontable.psversion.major -eq 7) {
                    $h.add("Duration",(New-Timespan -start $start -end $End))
                }
                [pscustomobject]$h | Add-History
            }
            else {
                Microsoft.PowerShell.Utility\Write-Host $command -ForegroundColor Cyan
            }
        } #IF SINGLE COMMAND
        #START MULTILINE
        #skip the ::
        elseif ($command -eq "::" -AND $NoMultiLine) {
            $NoMultiLine = $False
            $StartMulti = $True
            #define a variable to hold the multiline expression
            [string]$multi = ""
        } #elseif
        #FIRST LINE OF MULTILINE
        elseif ($StartMulti) {
            for ($i = 0; $i -lt $command.length; $i++) {
                if ($IncludeTypo -AND ($(&$Interval) -ge ($RandomMaximum - 5)))
                { &$Typo }
                else { Microsoft.PowerShell.Utility\Write-Host $command[$i] -NoNewline} #else
                Start-Sleep -Milliseconds $(&$Interval)
                #only check for a pipe if we're not at the last character
                #because we're going to pause anyway
                if ($i -lt $command.length - 1) {
                    &$PipeCheck
                }
            } #for

            $StartMulti = $False

            #remove the backtick line continuation character if found
            if ($command.contains('`')) {
                $command = $command.Replace('`', "")
            }
            #add the command to the multiline variable
            $multi += " $command"
            # if (!$command.Endswith('{')) { $multi += ";" }
            if ($command -notmatch ",$|{$|}$|\|$|\($") { $multi += " ; " }
            If ((PauseIt) -eq "quit") {Return}

        } #elseif
        #END OF MULTILINE
        elseif ($command -eq "::" -AND !$NoMultiLine) {
            Microsoft.PowerShell.Utility\Write-Host "`r"
            Microsoft.PowerShell.Utility\Write-Host ">> " -NoNewline
            $NoMultiLine = $True
            If ((PauseIt) -eq "quit") {Return}
            #execute the command unless -NoExecute was specified
            Microsoft.PowerShell.Utility\Write-Host "`r"
            $cmd =  $(($multi -replace ';(\s=?)$','').trim())
            if ($RunningTranscript) {
                    "$(prompt)$cmd" | Out-File -path $Transcript -Encoding ascii -erroraction Stop -Append
            }
            if (-NOT $NoExecute) {
                [datetime]$start = [datetime]::now
                $h = @{
                    CommandLine        = $cmd
                    StartExecutionTime = $start
                }
                Invoke-Expression $cmd -OutVariable r | Out-Default

                if ($RunningTranscript) {
                    $r | Out-File -filepath $Transcript -Encoding ascii -append -erroraction stop
                }
                #Add clean command to PSReadline History
                [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($cmd)

                #Add to command history
                [datetime]$end = [datetime]::now
                $h.Add("EndExecutionTime", $end)
                $h.Add("ExecutionStatus", "Completed")
                if ($psversiontable.psversion.major -eq 7) {
                    $h.add("Duration",(New-Timespan -Start $start -end $end))
                }
                [pscustomobject]$h | Add-History
            }
            else {
                Microsoft.PowerShell.Utility\Write-Host $cmd -ForegroundColor Cyan
            }
        }  #elseif end of multiline
        #NESTED PROMPTS
        else {
            Microsoft.PowerShell.Utility\Write-Host "`r"
            Microsoft.PowerShell.Utility\Write-Host ">> " -NoNewLine
            If ((PauseIt) -eq "quit") {Return}
            for ($i = 0; $i -lt $command.length; $i++) {
                if ($IncludeTypo -AND ($(&$Interval) -ge ($RandomMaximum - 5)))
                { &$Typo  }
                else { Microsoft.PowerShell.Utility\Write-Host $command[$i] -NoNewline }
                Start-Sleep -Milliseconds $(&$Interval)
                &$PipeCheck
            } #for

            #remove the backtick line continuation character if found
            if ($command.contains('`')) {
                $command = $command.Replace('`', "")
            }
            #add the command to the multiline variable and include the line break
            #character
            $multi += " $command"
            # if (!$command.Endswith('{')) { $multi += ";" }

            if ($command -notmatch ",$|{$|\|$|\($") {
                $multi += " ; "
                #$command
            }

        } #else nested prompts

        #reset the prompt unless we've just done the last command
        if (($count -lt $commands.count) -AND ($NoMultiLine)) {
            Microsoft.PowerShell.Utility\Write-Host $(prompt) -NoNewline
        }

    } #foreach

    #stop a transcript if it is running
    if ($RunningTranscript) {
        #stop this transcript if it is running
       # Stop-Transcript | Out-Null
        $stopTranscript = @"
 
*******************************
PowerShell transcript end
End time: $(Get-Date)
*******************************
 
"@

            $stopTranscript | Out-File -path $Transcript -Encoding ascii -erroraction Stop -Append
    }

} #function