One-Touch Menus in PowerShell Console

Greetings, fellow scripters!

I’ve been asked about creating simple menus many times over the past few years by both students and clients.  “Is it possible to offer the script user a multiple choice menu?” is the typical question. “Yes,” is my typical answer.

Those of you in my generation remember the DOS era, during which such menus were prevalent. Those of you who entered the IT profession somewhat later may still have experienced some of Microsoft’s own text-based menus, such as SCONFIG.EXE. These menus can be generic or elaborate, depending on your time and the importance of aesthetics involved.

In this entry, I’ll walk you through the process, piece by piece, codeblock by codeblock, so that you can decide for yourself where your own preferred stopping place lies. We’ll look at how to use single keypresses to make simple choices. We’ll add some formatting to improve appearance and screen real estate usage. We’ll even decorate the CLI, first a little, then more, and finally a lot, to give you the text menu appearance, functionality, and professional flare you find most suitable to your needs.

Ready? Let’s get started! Feel free to write your own code or copy and paste mine to test out our menu. We’ll create it step by step, explaining as we go.

The Basics:

We all likely know about the Read-Host cmdlet. It’s purpose is to pause the code, prompt for input, accept the input, and then proceed once the user presses ENTER. Customarily, we place the user’s input into variable to be used in our imminent code. Occasionally, if we only need to use the input once, we’ll simply place the Read-Host line in parentheses in the spot where the variable would normally be used.  Below are some examples to illustrate these concepts:

SAMPLE CODE:

# To place the answer into a variable:

$MyVariableName = Read-Host “Please type a value, then press ENTER”

“This is the code that uses the value: $MyVariableName.”

# To place the answer directly into a command line as a parameter:

“This is the code that uses the value: $(Read-Host ‘Please enter a value’)”

# END

The problems with using Read-Host for choice-making arise when someone types something that is not a legitimate choice. Imagine expecting number from 1 to 5, and receiving “Fred,” or even 6 or 55. The result could crash your script, or worse yet, do the wrong thing. To prevent this, we would have to do some coding to ensure that the value entered met our expectations, and if not, take evasive action, or insist that the user input a valid choice. Certainly, this can be done. Here’s one possibility for handling such an issue with RegEx:

SAMPLE CODE:

#>

# Offering Choices

#—–

# First, tell the prompt to loop if improper value is entered:

while (!($myinput -match “/b[1-5]/b”)){

# Second, prompt for and accept the user’s input:

$myinput = read-host (“Enter a number from 1 to 5”)

# Set a trap to deal with improper input types, if entered:

Trap {if (!($myinput -match “[0-9]”)) # Check for non-numeric values:

{write-host “You appear to have entered text. Please try again.`n”

continue}

elseif (!($myinput -match “/b([1-5])/b”)) # Check for invalid numbers:

{write-host “That number is not within range. Please try again`n”

continue}}

# Take action on choice:

Switch ($myinput) {

# Productive code goes between the curly braces below:

1 {“You entered a one.”;exit}

2 {“You entered a two.”;exit}

3 {“You entered a three.”;exit}

4 {“You entered a four.”;exit}

5 {“You entered a five.”;exit}

#Throw error if invalid choice was entered.

Default {Throw} } }

# END

NOTE: You may have noticed already that there are two ways to paste copied code into the CLI.  If you right-click the CLI, the code will paste itself in and run automatically.  That’s not always what we prefer.  If you left-click the CLI prompt line, and then press CTRL-V, the entire selection of copied code will be passed (as you see below) into the CLI buffer, where you can scroll through it prior to pressing ENTER to execute it. Notice the CLI continuation prompts.

So far, we’ve been dealing with Read-Host. You can see the issues. We’re at the mercy of what is typed in, and must accept errors or prevent them. There is, however, another technique for handling the input. .NET classes are available within PowerShell, and many are very easily exploited. True, some of the display-related .NET classes, while useful, have certain limitations of which the scripter must remain aware. For instance, some [CONSOLE] members are valid only on the Command Line Interface (CLI) screen, and not in the ISE. That means that while we can write our code in the ISE, certain portions must be tested in the CLI instead.  These mostly involve cursor positions and sizes, window width and height. There are also certain [ESC] sequences that work only in the CLI, such as making your line of text begin at the left margin, overwriting whatever might already be there. Most importantly for our menu project:  the ReadKey() method cannot be used in the ISE, and that’s what we must use to have a single keypress involved in our choice-making process. These CLI-only capabilities allow scripters to display multiple choice menus. Fortunately, while we develop our scripts in the ISE, we seldom use the ISE in production. That is to say, our scripts end up being executed elsewhere. With this in mind, let’ have a look next at moving to a more menu-like display in the CLI. Read the code and comments below, and then give the code a try in your own CLI, if you like.

SAMPLE CODE:

# Menu for CLI (Save this excerpt in a new script file and run it in the CLI.)

# We usually clear the screen prior to displaying menus.

cls

# These next three linefeed [ESC] sequences move us down a bit from the top.

“`n`n`n”

<# Now, we need to position our choices in columns on the screen. We’ll use -f formatting. Note that column numbers begin with 1, but spacing from the left begins with 0. In this example, 40 columns are reserved for the first item (index 0) in our list. The second item (index 1) requires no extra spaces, and thus begins at column 41. Once we start framing our menu later, we’ll notice the off-by-one issue this causes. The “`t” is the TAB [ESC] character to give us a little left margin space.

#>

Write-Host (“{0,-40}{1,-0}” -f “`tA) Check free space on (C:)”,”C) Check BIOS version”) -ForegroundColor Yellow

Write-Host (“{0,-40}{1,-0}” -f “`tB) Check System Model Number”,”D) Check amount of installed RAM”) -ForegroundColor Yellow

# These next three linefeed (new line) [ESC] sequences move us down a bit from the menu.

“`n`n`n”

# Next, we ask the user to chose a menu item:

Write-Host “`tEnter Choice, or X to Exit: ” -ForegroundColor Yellow -NoNewline

# As in our previous example, we’ll use a WHILE loop, but this time, we’ll expect a single character.

while ($true) {

# Notice that the input opportunity comes within the WHILE loop. One advantage

# of using the ReadKey() method is that it moves forward after a single keypress. The

# ReadKey() method stores the single keypress character in its key property.

$choice = (([console]::ReadKey()).key)

# Some more line spacing for readability and appearance:

“`n`n”

# As in our previous example, we’ll use a SWITCH construct to evaluate the input.

# In this case, I’ve used some universally available real-world code for illustration.

# Notice that the character being compared matches our two-column menu created above.

Switch ($choice) {

“A” {“`r`t$([math]::round((get-volume c).SizeRemaining /1gb, 2)) GB`n”;exit}

“B” {“`r`t$((Get-WmiObject win32_computersystem)|

foreach{$_.Manufacturer+”–“+$_.model})`n”;exit}

“C” {“`r`t$((Get-WmiObject win32_bios).biosversion) `n”;exit}

“D” {“`r`t$((Get-WmiObject Win32_physicalmemory).capacity / 1gb) GB`n”;exit}

“X” {“`r`tBye!  `n`n” ;exit}

Default {Write-Host “`tInvalid choice. Try again:  ” `

-ForegroundColor Yellow -NoNewline}

}

}

# END

So now we have our functioning menu. It lacks only a bit of decoration. Creating ANSI-like graphics in the CLI can be tedious, but what the heck. We have time! To use the native graphic characters provided by ASCII and Unicode encoding, we must hold down the ALT key while entering the character’s code on the number keypad, and then let go. The resulting character will appear at our cursor position. Just for a demo, try placing a ▒ character on your screen with ALT+177. Did it work? This is how we will get the characters to decorate our screen. We’ll place them in variables, so we can use them without having to create them from scratch each time. (These characters can be displayed in the ISE, but their screen positioning can’t.) To view a full list of all the ASCII codes, visit:

https://theasciicode.com.ar/extended-ascii-code/block-graphic-character-ascii-code-219.html

Decorating Our Menu Display:

First, we visit the above URL and select the characters we’d like to use. I’ve selected these:

ALT 185 = ╣

ALT 186 = ║

ALT 187 = ╗

ALT 188 = ╝

ALT 200 = ╚

ALT 201 = ╔

ALT 202 = ╩

ALT 203 = ╦

ALT 204 = ╠

ALT 205 = ═

ALT 206 = ╬

ALT  32 = <SPACEBAR>

—–

Just for practice, try drawing a little box, line by line, by using the above ALT codes:

╔═══╗

║   ║

╚═══╝

That ALT code sequence is:

201,205,205,205,187

186, 32, 32, 32,186

200,205,205,205,188

Storing the characters in variables is much easier:

$topleft = ALT 201

$horizontal = ALT 205

$topright = ALT 187

$vertical = ALT 186

$bottomleft = ALT 200

$bottomright = ALT 188

$space = ALT 32

That’s what we’ll do in the following routines.

SAMPLE CODE:

# First, we’ll load the variables as strings:

$topleft = “╔”

$horizontal = “═”

$topright = “╗”

$vertical = “║”

$bottomleft = “╚”

$bottomright = “╝”

$space = ” ”

# Now, let’s draw the same box as we did before:

“$topleft$horizontal$horizontal$horizontal$topright”

“$vertical$space$space$space$vertical”

“$bottomleft$horizontal$horizontal$horizontal$bottomright”

# Test that code out. It should produce the same little box.

# Next, let’s implement some text manipulation techniques by

# multiplying our horizontal line variables by a number:

“$topleft$($horizontal*3)$topright”

“$vertical$space$space$space$vertical”

“$bottomleft$($horizontal*3)$bottomright”

# Test that code out. You should see the box again.

# Now, let’s try out some screen positioning:

# Note: Remember, you can only test these in the CLI.

# Because the CLI’s prompt gets in the way, it’s best to

# save the script and then execute the script manually.

# Here, we retrieve and store our coordinates in variables.

$WindowWidth = [console]::WindowWidth  #X

$WindowHeight = [console]::WindowHeight #Y

$WindowLeft = 1 #X

$WindowRight = ($WindowWidth-2) #X

$WindowTop = 1 #Y

$WindowBottom = ($WindowHeight-1)  #Y

# Tidy up the screen before drawing the box:

cls

# Draw our box:

Write-Host “`r$topleft$($horizontal*$WindowRight)$topright” -NoNewline;

for ($y=$WindowTop;$y -lt $WindowBottom-10;$y++) {

[console]::SetCursorPosition($WindowLeft-1,$y);Write-Host “$vertical”};

for ($y=$WindowTop;$y -lt $WindowBottom-10;$y++) {

[console]::SetCursorPosition($WindowRight+1,$y);Write-Host “$vertical”};

Write-Host “`r$bottomleft$($horizontal*$WindowRight)$bottomright” -NoNewline;

# END

By this point, as you see above, we’ve drawn a frame for our menu.  Note that you could change the bottom line location of the frame to suit your preferred size by changing the number of lines subtracted from the $WindowBottom variable in the two FOR loops.  You could even pass the desired size as a command line parameter, if you like.  I’ll do that in our finished script.

Next, we need to work on positioning menu text inside the frame. For the purposes of this demo, I selected the four WMI commands we saw and used above. Of course, you may substitute your own favorites, but you’ll have to be sure that the results don’t overwrite our colorful frame, at least for the time being. I’ll show you how to overcome that issue later in this post. As it turns out, since we previously positioned our menu text by using the -f format operator, the code we used then won’t work now.  It will overwrite our borders.  Instead, we’ll position our cursor at an appropriate starting place for each menu option, and then write the text starting there.

We’ll do this using [console] .NET class’s methods.

SAMPLE CODE:

# Display menu text in specific locations:

# (Use CTRL-C and CTRL-V to paste this code into the CLI to test it.)

cls

[console]::SetCursorPosition(10,10);

Write-Host (“[A] Check free space on C:”) -NoNewline

[console]::SetCursorPosition(40,10);

Write-Host (“[B] Check System Model Number”)

[console]::SetCursorPosition(10,12);

Write-Host (“


Check BIOS version”) -NoNewline

[console]::SetCursorPosition(40,12);

Write-Host (“[D] Check Installed RAM`n”)

# END

We verify below that the code places choices in acceptable positions:

At this point, you’ve experienced the concepts and techniques you need. What follows is documented code that should be reasonably intuitive for experienced scripters. I’ve added a parameter to select a menu size and played with colors a bit to improve the cosmetics. You’ll see that I’ve documented those trivial tweaks. The goal below is to use what we’ve discussed to produce a real CLI menu.

# Complete demo script starts here:

# Project name: Framed Menu Selection Box Demo

# Accept optional parameter. Default to “Small” frame:

param ($FrameSize = “Small”) # Param statement must come first.

# Quit if running is ISE:

if ($psISE){“`nThis script cannot run in the ISE.”;exit}

# Save original values, then set console colors:

$origback = [console]::BackgroundColor

$origfore = [console]::ForegroundColor

[console]::BackgroundColor = “White”

[console]::ForegroundColor = “DarkBlue”

# Save original CLI Window title, then set a custom title:

$origtitle = [console]::Title

$wt = “CLI Menu Project” # Modify this value to change title.

[console]::Title = $wt

# Adjust these values to control Window size .

$height = 30; $width = 80

# Adjust menu text positions based on frame size:

# (You can adjust these values to suit your needs.)

switch ($FrameSize){

“Small”{$bm=($height/2);$tm=4}

“Large”{$bm=($height/3);$tm=6}  }

<# Abbreviation Legend: (This helps keep variable names short.)

tl = top left corner

tr = top right corner

bl = bottom left corner

br = bottom right corner

h = horozontal line

v = vertical line

ft = frame top

fb = frame bottom

bm = bottom margin

ww = window width

wh = window height

bw = buffer width

bh = buffer height

x = cursor column position

y = cursor line position

ln = line number

wt = window title

fc = foreground color

bc = background color

#>

# Tidy up

cls

# Select a frame and menu colors:

$fc = “DarkBlue”

$bc = “Gray”

# Note: Below, we must set the height values prior to the width values.

# Failing to do so will result in mismeasurements caused by scrollbars.

[console]::WindowHeight=$height;[console]::BufferHeight=$height

[console]::WindowWidth=$width;  [console]::BufferWidth=$width

#Keep menu on screen by looping until X is pressed:

While ($true) {

# First, we’ll load the variables as strings:

$tl = “╔”;$h = “═”;$tr = “╗”

$bl = “╚”;$v = “║”;$br = “╝”

$spc = ” ”

# Now, let’s try out some screen positioning:

# Note: Remember, you can only test this in the CLI.

# Window and frame coordinates:

$ww = [console]::WindowWidth  #X

$wh = [console]::WindowHeight #Y

$wl = 1 #X

$wr = ($ww-2) #X

$wt = 1 #Y

$wb = ($wh-1)  #Y

# Position coordinates:

$x1 = 8 # Column 1 from left

$x2 = 40 # Column 2 from left

$y1 = $tm # Row 1 from top

$y2 = $tm+2 # Row 2 from top

$yQ = $y2+3 # Row for our question

# Now, we draw our box with a function we’ll call later:

function show-box {

Write-Host “`r$tl$($h*$wr)$tr” `

-ForegroundColor $fc -BackgroundColor $bc -NoNewline;

$global:ft = ($host.ui.RawUI.CursorPosition).Y;

for ($y=$wt;$y -lt $wb-$bm;$y++) {

[console]::SetCursorPosition($wl-1,$y);

Write-Host “$v” -ForegroundColor $fc -BackgroundColor $bc};

for ($y=$wt;$y -lt $wb-$bm;$y++) {

[console]::SetCursorPosition($wr+1,$y);

Write-Host “$v” -ForegroundColor $fc -BackgroundColor $bc};

Write-Host “`r$bl$($h*$wr)$br” -ForegroundColor $fc  -BackgroundColor $bc `

-NoNewline ;

$global:fb = ($host.ui.RawUI.CursorPosition).Y;

} # End of box function

# Next, we’ll use another function to call later to place our menu inside the box:

function show-menu {

[console]::SetCursorPosition($x1,$y1);

Write-Host (“[A] Check free space on C:”) -NoNewline

[console]::SetCursorPosition($x2,$y1);

Write-Host (“[B] Check System Model Number”)

[console]::SetCursorPosition($x1,$y2);

Write-Host (“


Check BIOS version”) -NoNewline

[console]::SetCursorPosition($x2,$y2);

Write-Host (“[D] Check Installed RAM”)

# Start menu loop: (Once we have the menu on the screen, we have some

# further requirements to accommodate the involved I/O:

while ($true) {

# Present the prompt inside the box:

[console]::SetCursorPosition($x1,$yQ);

Write-Host “Enter Choice, or X to Exit: ” -NoNewline;

[console]::CursorVisible = $False

$choice =(([console]::ReadKey()).key)

# Ensure that with each subsequent choice, the results area is clean:

for ($ln=$global:fb+1;$ln -le $global:fb+3;$ln++){

[console]::SetCursorPosition(0,$ln);[console]::WriteLine(” “*$ww)}

# Evaluate input and take appropriate action:

Switch ($choice) {

“A” {“`r`t$([math]::round((get-volume c).SizeRemaining /1gb, 2)) GB $(” “*70)”}

“B” {“`r`t$((Get-WmiObject win32_computersystem)|

foreach{$_.Manufacturer+”–“+$_.model}) $(” “*70)”}

“C” {“`r`t$((Get-WmiObject win32_bios).biosversion) $(” “*70)”}

“D” {“`r`t$((Get-WmiObject Win32_physicalmemory).capacity/1gb) GB $(” “*70)”}

“X” {[console]::ForegroundColor = $origfore; [console]::BackgroundColor = $origback;

[console]::CursorVisible = $True;cls;[console]::Title=””;

[console]::Title = $origtitle; exit}

Default {[console]::SetCursorPosition(0,$global:fb+2);

for ($ln=$global:fb+2;$ln -le $global:fb+3;$ln++){

[console]::SetCursorPosition(0,$ln);[console]::WriteLine(” “*$ww)}

Write-Host “`r`tInvalid choice. Try again…$(” “*70)” -NoNewline}

} # End of Switch

} # End of menu function

}  # End of Menu loop.

# At last, we can run our two functions:

show-box

show-menu

}  # End of Script loop.

# END of Script

Here’s a screenshot of the finished menu:

 

Just another word or two before summing up…

As I mentioned earlier, you may need to produce more than a line or two of output. This could cause our menu to scroll up off the screen or parts of the output to overwrite our menu or frame. To avoid that, we execute the involved menu choice code such that it produces a second titled window displaying the results. If the code is complicated, we might want to build it into a function that our keypress code would launch when the selection is made. Below is an example. In this case, pressing E would just evoke ServiceList.

Here’s an alternative menu item that launches a separate window:

SAMPLE CODE:

function ServiceList {

start-process powershell “-noexit &{[console]::title = ‘Services’;

get-service;’`nEnter ”EXIT” to close this window.’}”

}

# END

 

Summary:

In this post, we’ve explored a few of the core techniques for creating task-specific menus to be displayed in customized windows.

We’ve seen how to…

1) use graphics and colors to create frames and borders;

2) retrieve, store, and use CLI screen (X,Y) coordinates;

3) move to a specific row and column prior to displaying text;

4) read single keypress values with ReadKey();

5) execute code and functions based on a menu choice;

6) keep our screen clean and maintain our custom text positioning;

7) require specific keypresses in order to proceed.

8) use several .NET classes and their members

9) create and call functions for common routines

I hope you’ve had as much fun reading and learning from this project as I had producing it. You might like to try tweaking the code we’ve explored to make it more useful in your daily routines.

Meanwhile, happy scripting!

–Mike

Type to search blog.learningtree.com

Do you mean "" ?

Sorry, no results were found for your query.

Please check your spelling and try your search again.