code/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(Mandatory,ParameterSetName = "Static")] [int]$Pause, [Parameter(ParameterSetName = "Random")] [ValidateScript( { $_ -gt 0 })] [int]$RandomMinimum = 50, [Parameter(ParameterSetName = "Random")] [ValidateScript( { $_ -gt 0 })] [int]$RandomMaximum = 110, [Parameter(ParameterSetName = "Random")] [parameter(HelpMessage = "Enter the path for a transcript file")] [ValidateNotNullOrEmpty()] [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 Function EnterCommand { [cmdletbinding()] Param() $typing = $true #keep looping until a key is pressed $list = [System.Collections.Generic.list[object]]::new() do { if ($host.ui.RawUi.KeyAvailable) { $key = $host.ui.RawUI.ReadKey("NoEcho,IncludeKeyDown") if ($key.VirtualKeyCode -eq 13) { $typing = $False } #if return else { Microsoft.PowerShell.Utility\Write-Host $key.Character -NoNewline $list.add($key.character) } } #if key available # Start-Sleep -millisecond 10 } while ($typing) #end do $cmd = $list -join "" $sb = [scriptblock]::create($cmd) $start = Get-Date Invoke-Command -ScriptBlock $sb -OutVariable result | Out-Host $end = Get-Date if ($RunningTranscript) { "$(prompt)$Cmd" | Out-File -FilePath $Transcript -Encoding ascii -ErrorAction Stop -Append $result | Out-File -FilePath $Transcript -Encoding ascii -ErrorAction Stop -Append } #add to PSReadlineHistory [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($cmd) $h = @{ PSTypeName = "Microsoft.PowerShell.Commands.HistoryInfo" CommandLine = $cmd StartExecutionTime = $start } [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 } #enterCommand function Function WriteWord { [cmdletbinding()] Param([string]$command) Write-Debug $command Function writechar { [cmdletbinding()] Param([string]$word, [string]$color) for ($i = 0; $i -lt $word.length; $i++) { #write the character Write-Verbose "Writing character $($word[$i])" Microsoft.PowerShell.Utility\Write-Host $word[$i] -NoNewline -ForegroundColor $color #insert a pause to simulate typing if ($Pause) { $rest = $pause } else { $rest = Get-Random -Minimum $RandomMinimum -Maximum $RandomMaximum } Start-Sleep -Milliseconds $rest if ($word -eq "|") { If ((PauseIt) -eq "quit") { Return } } } #for #Write-Host " " -NoNewline } #writechar $sb = [scriptblock]::Create($command) New-Variable astTokens -Force New-Variable astErr -Force $ast = [System.Management.Automation.Language.Parser]::ParseInput($sb, [ref]$astTokens, [ref]$astErr) $run = $False foreach ($item in $asttokens) { #insert spaces if ($Run -AND ($item.Extent.StartOffset -gt $last)) { Microsoft.PowerShell.Utility\Write-Host " " -NoNewline } if ($item.kind -eq 'NewLine') { If ((PauseIt) -eq "quit") { Return } Microsoft.PowerShell.Utility\Write-Host "" Microsoft.PowerShell.Utility\Write-Host ">>" -NoNewline $Inmulti = $True } else { if ($item.TokenFlags -match "Operator") { $color = "darkgray" } Else { switch -regex ($item.Kind) { "Generic" { $color = "Yellow" } "Variable" { $color = "green" } "String" { $color = "darkcyan" } "Parameter" { $color = "darkgray" } default { $color = "white" } } } writechar -word $item.text -color $color $run = $True $last = $item.Extent.EndOffset } #not a new line } If ($Inmulti) { Microsoft.PowerShell.Utility\Write-Host "" } } #WriteWord function #abort if NOT running in the console host if ($host.name -ne "ConsoleHost") { Write-Warning "This command will only work in Windows PowerShell or PowerShell 7.x 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\." { $header = @" PowerShell $($psversiontable.psversion.tostring()) 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 "^#|(Return)" -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() Write-Debug "processing: $command" $count++ #pause until a key is pressed which will then process the next command if ($NoMultiLine) { If ((PauseIt) -eq "quit") { Return } } if ($command -eq "<live>") { Write-Debug "going live" if ($NoExecute) { "# Insert live PowerShell here" } else { entercommand } } #SINGLE LINE COMMAND elseif ($command -notmatch "^::" -AND $NoMultiLine) { Write-Debug "single line command: $command" WriteWord $command #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" # "`n" #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 rex | Out-Default # Invoke-Command -ScriptBlock ([scriptblock]::Create($command)) -NoNewScope -OutVariable rex | Out-Default if ($RunningTranscript) { $rex | 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 -match "^::" -AND $NoMultiLine) { $NoMultiLine = $False $StartMulti = $True Write-Debug "initializing `$multi" #define a variable to hold the multiline expression [string]$multi = @' '@ } #elseif #FIRST LINE OF MULTILINE elseif ($StartMulti) { <# $command.split() | ForEach-Object { WriteWord $_ } 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 ($i = 0; $i -lt $command.length; $i++) { else { # Microsoft.PowerShell.Utility\Write-Host $command[$i] -NoNewline write-host "" } #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 Write-Debug "Adding $command to `$multi" $multi += "$command`r" # if (!$command.Endswith('{')) { $multi += ";" } # if ($command -notmatch ",$|{$|}$|\|$|\($") { $multi += " ; " } If ((PauseIt) -eq "quit") { Return } } #elseif elseif (!$NoMultiline) { #add next line if ($command -match "^::") { Write-Debug "ending multiline" WriteWord $multi $NoMultiLine = $True $cmd = $multi # $(($multi -replace ';(\s=?)$', '').trim()) Write-Debug "cmd = $cmd" #Microsoft.PowerShell.Utility\Write-Host "`r" 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 rex | Out-Default #Invoke-Command -ScriptBlock ([scriptblock]::Create($cmd)) -NoNewScope -OutVariable rex | Out-Default if ($RunningTranscript) { $rex | 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 } } else { Write-Debug "Adding $command to `$multi" $multi += "$command`r" } } #END OF MULTILINE elseif ($command -match "^::" -AND !$NoMultiLine) { #TODO This might be deleted # Microsoft.PowerShell.Utility\Write-Host "`r" # Microsoft.PowerShell.Utility\Write-Host ">> " -NoNewline Write-Debug "show multiline" WriteWord $multi $NoMultiLine = $True Write-Warning "execute multi" #If ((PauseIt) -eq "quit") { Return } #execute the command unless -NoExecute was specified Microsoft.PowerShell.Utility\Write-Host "`r" $cmd = $multi # $(($multi -replace ';(\s=?)$', '').trim()) Write-Warning "cmd = $cmd" 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 rex | Out-Default #Invoke-Command -ScriptBlock ([scriptblock]::Create($cmd)) -NoNewScope -OutVariable rex | Out-Default if ($RunningTranscript) { $rex | 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 { #TODO I think this can be deleted Write-Debug "in nested prompts" Microsoft.PowerShell.Utility\Write-Host "`r" Microsoft.PowerShell.Utility\Write-Host ">> " -NoNewline If ((PauseIt) -eq "quit") { Return } $command.split() | ForEach-Object { WriteWord $_ } Start-Sleep -Milliseconds $(&$Interval) &$PipeCheck <# for ($i = 0; $i -lt $command.length; $i++) { 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)) { Write-Debug "prompt" Microsoft.PowerShell.Utility\Write-Host $(prompt) -NoNewline } } #foreach command #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 } Write-Debug "end" } #function |