So, Brandon owes me for this one … he had a problem with the Start-Demo script posted by the PowerShell team because when you try to assign output to a variable (eg: $fileInfo = ls | gm) it would spew it to the host instead. At any rate, I fixed it up for him (for reasons that escape me now), and since I didn’t bill him for the work,
I might as well share it with all of you for free too, to spare you the same frustration
.
If you’re not familiar with Start-Demo, it’s a script which basically takes another script and steps through it with pauses. It lets you step forward and backward through the script and even suspend it so you can execute commands in a nested prompt. Now, it has a few more features, and a few more comments to make it a little easier for others to hack in the future.
I actually changed several of the shortcut keys, but they just made more sense to me this way. I also changed the way the input is handled to ReadKey rather than ReadHost so you don’t have to hit ENTER except to run the command … hopefully you’ll find this makes demo smoother to run. To make it less jarring on my setup with my custom prompt, I made the various colors used in the script settable. I also added a few options so that you can even run it fully automatic with a settable pause between commands.
Anyway, I posted the code to the PowerShell Central Scripts repository (where I’ll post any improvements I make in the future), but I’ll post the current version here as well.
## Start-Demo.ps1 (originally from Jeffrey Snover -- changes by Joel Bennett)
## version 3.2
## 3.0
## - Fixed: Commands which set a variable, like: $files = ls
## - Fixed: Default action doesn't continue
## - Changed: Use ReadKey instead of ReadLine
## - Changed: Modified the option prompts (sorry if you had them memorized)
## - Changed: Various time and duration strings have better formatting
## - Enhance: Colors are settable: prompt, command, comment
## - Added: NoPauseAfterExecute switch removes the extra pause
## If you set this, the next command will be displayed immediately
## - Added: Auto Execute mode (FullAuto switch) runs the rest of the script
## at an automatic speed set by the AutoSpeed parameter (or manually)
## - Added: Automatically append an empty line to the end of the demo script
## so you have a chance to "go back" after the last line of you demo
## 3.1
## - Fixed: No Output when invoking Get-Member (and other cmdlets like it???)
## 3.2
## - Fixed: Put back the trap { continue; }
##
## I also cleaned up a few routines ...
##
param(
$file=".\demo.txt",
[int]$command=0,
[System.ConsoleColor]$promptColor="Yellow",
[System.ConsoleColor]$commandColor="White",
[System.ConsoleColor]$commentColor="Green",
[switch]$FullAuto,
[int]$AutoSpeed = 3,
[switch]$NoPauseAfterExecute
)
$RawUI = $Host.UI.RawUI
$hostWidth = $RawUI.BufferSize.Width
# A function for reading in a character
function Read-Char() {
$_OldColor = $RawUI.ForeGroundColor
$RawUI.ForeGroundColor = "Red"
$inChar=$RawUI.ReadKey("IncludeKeyUp")
# loop until they press a character, so Shift or Ctrl, etc don't terminate us
while($_input.Character -eq 0){
$inChar=$RawUI.ReadKey("IncludeKeyUp")
}
$RawUI.ForeGroundColor = $_OldColor
return $inChar.Character
}
$file = Resolve-Path $file
while(-not(Test-Path $file)) {
$file = Read-Host "Please enter the path of your demo script (Crtl+C to cancel)"
$file = Resolve-Path $file
}
Clear-Host
$_lines = Get-Content $file
# Append an extra (do nothing) line on the end so we can still go back after the last line.
$_lines += "Write-Host 'The End'"
$_starttime = [DateTime]::now
$FullAuto = $false
Write-Host -nonew -back black -fore $promptColor $(" " * $hostWidth)
Write-Host -nonew -back black -fore $promptColor @"
<Demo Started :: $(split-path $file -leaf)>$(' ' * ($hostWidth -(18 + $(split-path $file -leaf).Length)))
"@
Write-Host -nonew -back black -fore $promptColor "Press"
Write-Host -nonew -back black -fore Red " ? "
Write-Host -nonew -back black -fore $promptColor "for help.$(' ' * ($hostWidth -17))"
Write-Host -nonew -back black -fore $promptColor $(" " * $hostWidth)
# We use a FOR and an INDEX ($_i) instead of a FOREACH because
# it is possible to start at a different location and/or jump
# around in the order.
for ($_i = $Command; $_i -lt $_lines.count; $_i++)
{
# Put the current command in the Window Title along with the demo duration
$Dur = [DateTime]::Now - $_StartTime
$RawUI.WindowTitle = "$(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s {3}" -f
$dur.Hours, $dur.Minutes, $dur.Seconds, $($_Lines[$_i])
# Echo out the commmand to the console with a prompt as though it were real
Write-Host -nonew -fore $promptColor "[$_i]PS> "
if ($_lines[$_i].Trim(" ").StartsWith("#")) {
Write-Host -fore $commentColor "$($_Lines[$_i]) "
continue
} else {
Write-Host -nonew -fore $commandColor "$($_Lines[$_i]) "
}
if( $FullAuto ) { Start-Sleep $autoSpeed; $ch = [char]13 } else { $ch = Read-Char }
switch($ch)
{
"?" {
Write-Host -Fore $promptColor @"
Running demo: $file
(n) Next (p) Previous
(q) Quit (s) Suspend
(t) Timecheck (v) View $(split-path $file -leaf)
(g) Go to line by number
(f) Find lines by string
(a) Auto Execute mode
"@
$_i--
}
"n" { # Next (do nothing)
Write-Host -Fore $promptColor "<Skipping Line>"
}
"p" { # Previous
Write-Host -Fore $promptColor "<Back one Line>"
while ($_lines[--$_i].Trim(" ").StartsWith("#")){}
$_i-- # extra line back because we're gonna step forward when we loop
}
"x" { # EXECUTE (Go Faster)
$AutoSpeed = [int](Read-Host "Pause (seconds)")
$FullAuto = $true;
Write-Host -Fore $promptColor "<eXecute Remaining Lines>"
$_i-- # Repeat this line, and then just blow through the rest
}
"q" { # Quit
Write-Host -Fore $promptColor "<Quiting demo>"
$_i = $_lines.count;
break;
}
"v" { # View Source
$lines[0..($_i-1)] | Write-Host -Fore Yellow
$lines[$_i] | Write-Host -Fore Green
$lines[($_i+1)..$lines.Count] | Write-Host -Fore Yellow
$_i-- # back a line, we're gonna step forward when we loop
}
"t" { # Time Check
$dur = [DateTime]::Now - $_StartTime
Write-Host -Fore $promptColor $(
"{3} -- $(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s" -f
$dur.Hours, $dur.Minutes, $dur.Seconds, ([DateTime]::Now.ToShortTimeString()))
$_i-- # back a line, we're gonna step forward when we loop
}
"s" { # Suspend (Enter Nested Prompt)
Write-Host -Fore $promptColor "<Suspending demo - type 'Exit' to resume>"
$Host.EnterNestedPrompt()
$_i-- # back a line, we're gonna step forward when we loop
}
"g" { # GoTo Line Number
$i = [int](Read-Host "line number")
if($i -le $_lines.Count) {
if($i -gt 0) {
$_i = $i - 1 # extra line back because we're gonna step forward when we loop
} else {
$_i = -1 # back a line, we're gonna step forward when we loop
}
}
}
"f" { # Find by pattern
$match = $_lines | Select-String (Read-Host "search string")
if($match -eq $null) {
Write-Host -Fore Red "Can't find a matching line"
} else {
$match | % { Write-Host -Fore $promptColor $("[{0,2}] {1}" -f ($_.LineNumber - 1), $_.Line) }
if($match.Count -lt 1) {
$_i = $match.lineNumber - 2 # back a line, we're gonna step forward when we loop
} else {
$_i-- # back a line, we're gonna step forward when we loop
}
}
}
"$([char]13)" { # on enter
Write-Host
trap [System.Exception] {Write-Error $_; continue;}
Invoke-Expression ($_lines[$_i]) | out-default
if(-not $NoPauseAfterExecute -and -not $FullAuto) {
$null = $RawUI.ReadKey("NoEcho,IncludeKeyUp") # Pause after output for no apparent reason... ;)
}
}
default
{
Write-Host -Fore Green "`nKey not recognized. Press ? for help, or ENTER to execute the command."
$_i--
}
}
}
$dur = [DateTime]::Now - $_StartTime
Write-Host -Fore $promptColor $(
"<Demo Complete -- $(if($dur.Hours -gt 0){'{0}h '})$(if($dur.Minutes -gt 0){'{1}m '}){2}s>" -f
$dur.Hours, $dur.Minutes, $dur.Seconds, [DateTime]::Now.ToLongTimeString())
Write-Host -Fore $promptColor $([DateTime]::now)
Write-Host
Send a comment