Wednesday, December 4, 2013

Pause in PowerShell

So what ever happened to "pause"?

Back in the old batch file days, you could stick in a "pause", and your script would, well, pause.  It would sit there and wait until someone hit the any key.  Most often you would stick it at the end of a batch file, so that the user could see that the script was done and read any output before the little black box disappeared.

It would sometimes be helpful to do that in our little blue boxes as well.  But PowerShell doesn't have a Pause-Script cmdlet.

.Net to the rescue!  I keep telling you that everything in PowerShell is an object.  And when I say "everything", I mean everything.  Even PowerShell itself.  $Host is the built-in variable representing the PowerShell host itself, the thing that is running the script, the little blue box as it were.  And we can access some of the things it can do, such as watching for keyboard input.

The $Host has a UI, $Host.UI.  And the .UI has a UI, $Host.UI.RawUI.  I have no idea why it has two layers.  The .RawUI has some methods.  Pipe it to Get-Member to see them.

$Host.UI.RawUI | Get-Member

.ReadKey() is the one we want.  .ReadKey() tells the RawUI to get the next key press.  Which means it has to sit and wait for someone to press the any key.
Let's try it without parameters.

$Host.UI.RawUI.ReadKey()

Well, that does wait for the any key, but when we press the any key, it tells us more than we ever need to know about the key we pressed.  So let's pipe the output and send it to nowhere.

$Host.UI.RawUI.ReadKey() | Out-Null

Better.  That prevented most of the information from showing up on the screen, but it did delay the key we pressed.  It would be nice if we could make that go away as well.

Maybe if we fed it into a dummy variable.

$X = $Host.UI.RawUI.ReadKey()

Nope.  That just fed the key information we had previously out-nulled into $X.  That can be useful if we want to capture the specific key pressed for some reason.  But the key that was pressed still displayed on the screen.

Maybe we need parameters.  When we ran Get-Member on the RawUI, it showed us that for parameters it wants…  Oh.  It didn't really tell us.  The information was truncated.  We can fool around with the Get-Member command to get it to give up the information, or we can turn to MSDN, which we were going to do in a few paragraphs anyway.

After some Googling and clicking around, we end up at the MSDN article on the "PSHostRawUserInterfaceReadKey method."  And we find that .ReadKey() wants TeadKeyOptions.  What the heck are those?  Well click on it.  Hmm.  That didn't help much.  Apparently the "Windows PowerShell Managed Reference" section isn't formatted an internally linked as logically and intuitively and discoverable as the .Net Framework library.  (Fortunately, most of the time we are searching for information that resides in the .Net library, but not this time.)  so after a little more Googling and clicking, we find the article on "ReadKeyOptions enumeration."
We see there are four options we can include:  Whether to react when the key is pressed or when it is released (you have to specify at least one of the two); whether or not to echo the key press to the screen; and whether or not to intercept Ctrl-c as a key press.

We really don't care whether we respond to the key up or the key down, so just pick one.  We do not want the key to print to the screen, so we want the no echo option.  Don't include the AllowCtrlC option, because we might want to use the pause to stop the script.

We could use the ReadKeyOptions enumerations, using bit-wise or to combine them.

$Host.UI.RawUI.ReadKey( [System.Management.Automation.Host.ReadKeyOptions]::NoEcho `
    -bor [System.Management.Automation.Host.ReadKeyOptions]::IncludeKeyDown )

But that's crazy.

Enumerations just represent integers.  In this case, each enumeration is a power of two, as the ReadKeyOptions parameter is storing each of the options in a single bit within.  We can ask PowerShell what our enumerations represent.

[System.Management.Automation.Host.ReadKeyOptions]::NoEcho `
    -bor [System.Management.Automation.Host.ReadKeyOptions]::IncludeKeyDown

Returns 6.

So for brevity, we could just use:

$Host.UI.RawUI.ReadKey( 6 )

To avoid having to remember 6, flipping all four bits by using 15 actually produces the same desired result.

$Host.UI.RawUI.ReadKey( 15 )

I am tempted to use this one.  It is unlikely that I will ever use .ReadKey() for anything else, so I won't need to remember or understand what the 15 does.  But I don't like it when I don't understand things, so the next time I look at this and can't remember, I'll waste time looking it up.

So I will compromise and use this:

$Host.UI.RawUI.ReadKey( "NoEcho, IncludeKeyDown" )

PowerShell's powerful dynamic type conversion capabilities convert the string to the appropriate enumerations and thence into the integer they represent.  It's easier to read than the full enumerations and more informative than the integers.

.ReadKey(), of course, reads the key, and then gives it to us.  But we didn't really want the key, we just wanted the pause while waiting for it, so we banish it to null.

And that gives us our (almost) simple one line replacement for "pause":

$Host.UI.RawUI.ReadKey( “NoEcho, IncludeKeyDown” ) | Out-Null

I experimented further and found two other one-liners that do the same thing.  I'll spare you the blow-by-blow on discovering these.

[console]::ReadKey($True)

And

cmd /c pause

Except it doesn't work in ISE

If you have been following along and trying this in a PowerShell console, you think I'm brilliant.
But if you have been trying it in PowerShell ISE, you think I'm an idiot, because all three methods have thrown an error every time, no matter what you try.

The problem is, the "console" in the bottom of the ISE is not a real PowerShell console.  It's a simulated PowerShell console.  PowerShell ISE does a very good job of simulating the PowerShell console, but one of the few things that it can't do is give you direct control of the raw console UI, because there isn't one.

So what is my brilliant solution for a snippet that would work equally well in the ISE?  Unfortunately, I don't have one.  (I told you that you would think I was an idiot.)  All of my research thus far has only suggested alternatives to the desired behavior, such as a "Click OK to continue" pop-up box to replace our "Hit any key to continue."

Such solutions have their place, but seem unsatisfactory when we were searching for something specific, something so simple that a batch script could do it with a single word.  If you have any ideas, please comment below.

Thanks to Charles Christianson for asking the question that I ultimately failed to answer.  Thank you, Charles, for making me look like an idiot.

No comments:

Post a Comment