PowerShell

Building a PowerShell Console Menu Revisited, Part 2

tiny-people-working-on-computer-hero-img

In a previous article, I demonstrated how to create a text-based PowerShell menu. This is something you could wrap up in a script for a technician, end-user, or even yourself. But I wanted to do more, so let’s see how to use PowerShell tools to build new tools.

We’ll start by initializing a counter variable.

$i=0

In my sample menu from last time, all of my entries were numbered. I’m too lazy to count, so I’ll let PowerShell do it. I’m also going to prompt for a menu title:
$Title = Read-Host "Enter the title for your menu"

Defining a menu title variable (Image Credit: Jeff Hicks)
Defining a menu title variable (Image Credit: Jeff Hicks)

For the sake of my demonstration, simply assign a value to $Title. At this point, I’m going to create a custom object. You’ll see why in a bit.

$MyMenu = [pscustomobject]@{
Title = $Title
Items = @()
}

Note that you can only use the [pscustomobject] type in PowerShell 3.0 and later. Now that I have two items to the menu, I think the process is easier if you enter them in the order you want them presented.

Sponsored Content

What is “Inside Microsoft Teams”?

“Inside Microsoft Teams” is a webcast series, now in Season 4 for IT pros hosted by Microsoft Product Manager, Stephen Rose. Stephen & his guests comprised of customers, partners, and real-world experts share best practices of planning, deploying, adopting, managing, and securing Teams. You can watch any episode at your convenience, find resources, blogs, reviews of accessories certified for Teams, bonus clips, and information regarding upcoming live broadcasts.

To accomplish this, I’m going to use a do loop to prompt for a menu description and a corresponding PowerShell expression. Each time through the loop, the counter is incremented by one, and I create a nested custom object with the counter number, the menu item or description, and the action.

Do {
#increment the counter
$i++
#prompt for a menu item
$item = Read-Host "Enter the menu item, e.g. Restart Spooler. Leave blank to finish."

if ($item) {
#prompt for a corresponding action
$action = Read-Host "Enter the PowerShell expression to execute"
$sb = [scriptblock]::Create($action)

$MyMenu.Items += [pscustomobject]@{
ItemNumber = $i
MenuItem = $item
Action = $sb
}
} #if $item

} While ($item)

The loop will continue as long as something is entered for Read-Host.

Building a menu object (Image Credit: Jeff Hicks)
Building a menu object (Image Credit: Jeff Hicks)

The variable $MyMenu is now my menu object.

My menu object (Image Credit: Jeff Hicks)
My menu object (Image Credit: Jeff Hicks)

As an alternative to being prompted, you could create a hashtable of menu items.

$hash = [ordered]@{
"Get running services" = {Get-Service | where status -eq 'running'}
"Get uptime" = {((Get-Date) - (Get-CimInstance win32_operatingsystem).lastbootuptime).ToString()}
"Get Drive C" = {Get-CimInstance win32_logicaldisk -filter "deviceid='c:'"}
"Get System Eventlog" = {Get-Eventlog -LogName System -Newest 10}
}

#create item objects
$items = $hash.GetEnumerator() | foreach -Begin {
$i=0 } -Process {
$i++
[pscustomobject]@{
ItemNumber = $i
MenuItem = $_.Name
Action = $_.Value
}
} -end {}

$MyMenu = [pscustomObject]@{
Title = $Title
Items = $items
}

My actions are all local, but you could invoke your own scripts or functions that could prompt for computer names or other specific information. For now, I just want to focus on the structure. In either event, I end up with a custom object that represents my menu. You’ll note that I didn’t include any options to quit, which you’ll see why in a moment. The reason I went through all of this effort is to save the results to an XML file.


$Path = "C:\scripts\MySampleConsoleMenu.xml"
$MyMenu | Export-Clixml -Path $path

Now my menu definition is stored in a structured document. This makes it easier to create different menus, but use the same script to display them. To do that, I need to import the XML file.
$importedMenu = Import-Clixml -Path $path

Next, I build a here string from the imported items.
$hereMenu = @"

$($ImportedMenu.title)

"@

foreach ($item in $importedMenu.Items) {
$hereMenu+= "{0} - {1}`n" -f $item.ItemNumber,$item.MenuItem
}
$hereMenu += "Enter a menu number or Q to quit"

My imported menu (Image Credit: Jeff Hicks)
My imported menu (Image Credit: Jeff Hicks)

All that remains is to put some logic behind the menu and respond to Read-Host. I invoke the corresponding scriptblock using Invoke-Command. Although the XML format appears to store a scriptblock, it gets imported as a string so I have to take an extra step to turn it back into a scriptblock. I could have used Invoke-Expression, but that’s a bad security practice.

#Keep looping and running the menu until the user selects Q (or q).
$Running = $True
Do {
cls
$r = Read-Host $hereMenu
if ($r -match "^q" -OR $r.length -eq 0) {
#quit the menu
$running = $False
Write-Host "Exiting the menu. Have a nice day" -ForegroundColor green
#bail out
Return
}
elseif ( -Not ([int]$r -ge 1 -AND [int]$r -le $($importedMenu.Items.count)) ) {
Write-Warning "Enter a menu choice between 1 and $($importedMenu.Items.count) or Q to quit"
}
else {
#create a scriptblock from the corresponding action
$cmd = [scriptblock]::Create($importedMenu.Items[$r-1].action)
Invoke-Command -ScriptBlock $cmd | Out-Host

}
#pause
$nothing = Read-Host "Press any key to continue"
} While ($Running)

The menu in action (Image Credit: Jeff Hicks)
The menu in action (Image Credit: Jeff Hicks)

You’ll see that I added logic to Quit separately. Here’s a more complete function for the entire process:

Function Invoke-MyMenu {
[cmdletbinding()]
Param(
[Parameter(Position=0,Mandatory,HelpMessage = "Enter the path to an XML file with an exported menu")]
[ValidateScript({
if (Test-Path $_) {
$True
}
else {
Throw "Cannot validate path $_"
}
})]

[string]$Path
)

$importedMenu = Import-Clixml -Path $path

#verify there are title and item properties

if ($importedMenu.Title -AND $importedMenu.Items) {
$hereMenu = @"

$($ImportedMenu.title)

"@

foreach ($item in $importedMenu.Items) {
$hereMenu+= "{0} - {1}`n" -f $item.ItemNumber,$item.MenuItem
}

$hereMenu += "Enter a menu number or Q to quit"

#Keep looping and running the menu until the user selects Q (or q).
$Running = $True
Do {
cls
$r = Read-Host $hereMenu
if ($r -match "^q" -OR $r.length -eq 0) {
#quit the menu
$running = $False
Write-Host "Exiting the menu. Have a nice day" -ForegroundColor green
#bail out
Return
}
elseif ( -Not ([int]$r -ge 1 -AND [int]$r -le $($importedMenu.Items.count)) ) {
Write-Warning "Enter a menu choice between 1 and $($importedMenu.Items.count) or Q to quit"
}
else {
#create a scriptblock
$cmd = [scriptblock]::Create($importedMenu.Items[$r-1].action)
Invoke-Command -ScriptBlock $cmd | Out-Host
#pause
$nothing = Read-Host "Press any key to continue"
}
} While ($Running)

}
else {
Write-Warning "$Path does not appear to have menu information"
}

} #end Function

With this, my script is actually quite simple:
#dot source the help desk
. C:\scripts\Invoke-MyMenu.ps1

Invoke-Mymenu -Path C:\scripts\MySampleConsoleMenu.xml

By separating the menu from the script, you have a bit more control. You can use a script to create the menu file, like this one to create an Active Directory related menu.
$title = "AD Tasks"
$path = "C:\scripts\ADTasks.xml"

$hash = [ordered]@{
"Get Domain Admins" = {get-adgroupmember "Domain Admins" | Select Name}
"Get Domain Controllers" = {(Get-ADDomain).ReplicaDirectoryServers | Get-ADDomainController | Select Name,IPv4Address,IsGlobalCatalog}
"Get FSMO Roles" = $a = {
$domain = Get-ADDomain
$forest = Get-ADForest
[pscustomobject]@{
Domain = $domain.name
Forest = $forest.Name
PDC = $domain.PDCEmulator
RIDMaster = $domain.RIDMaster
InfrastructureMaster = $domain.InfrastructureMaster
SchemaMaster = $forest.SchemaMaster
DomainNamingMaster = $forest.DomainNamingMaster
}
}
"Check DC services" = {
$dcs = (Get-ADDomain).ReplicaDirectoryServers
$svc = "DNS","KDC","ADWS","NTDS"
Get-Service -Name $svc -ComputerName $dcs | Sort Status,Computername |
Select @{Name="DomainController";Expression={$_.Machinename}},Name,Displayname,Status
}
}

#create item objects
$items = $hash.GetEnumerator() | foreach -Begin {
$i=0 } -Process {
$i++
[pscustomobject]@{
ItemNumber = $i
MenuItem = $_.Name
Action = $_.Value
}
} -end {}

$MyMenu = [pscustomObject]@{
Title = $Title
Items = $items
}

$MyMenu | Export-Clixml -Path $path

Now, we’ll create a simple script to invoke it.
#requires -version 4.0
#requires -module ActiveDirectory

#dot source the help desk
. C:\scripts\Invoke-MyMenu.ps1

Invoke-MyMenu -Path C:\scripts\ADTasks.xml

My Active Directory console menu (Image Credit: Jeff Hicks)
My Active Directory console menu (Image Credit: Jeff Hicks)

If I need to modify this menu, I can manually modify the XML or revise the first script and rerun it. I don’t have to touch any script that uses it. I hope you’ll let me know what you think of this approach.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

External Sharing and Guest User Access in Microsoft 365 and Teams

This eBook will dive into policy considerations you need to make when creating and managing guest user access to your Teams network, as well as the different layers of guest access and the common challenges that accompany a more complicated Microsoft 365 infrastructure.

You will learn:

  • Who should be allowed to be invited as a guest?
  • What type of guests should be able to access files in SharePoint and OneDrive?
  • How should guests be offboarded?
  • How should you determine who has access to sensitive information in your environment?

Sponsored by:

 
Office 365 Coexistence for Mergers & Acquisitions: Don’t Panic! Make it SimpleLive Webinar on Tuesday, November 16, 2021 @ 1 pm ET

In this session, Microsoft MVPs Steve Goodman and Mike Weaver, and tenant migration expert Rich Dean, will cover the four most common steps toward Office 365 coexistence and explain the simplest route to project success.

  • Directory Sync/GAL Sync – How to prepare for access and awareness
  • Calendar Sharing – How to retrieve a user’s shared calendar, or a room’s free time
  • Email Routing – How to guarantee email is routed to the active mailbox before and after migration
  • Domain Sharing – How to accommodate both original and new SMTP domains at every stage

Aimed at IT Admins, Infrastructure Engineers and Project Managers, this session outlines both technical and project management considerations – giving you a great head start when faced with a tenant migration.the different layers of guest access and the common challenges that accompany a more complicated Microsoft 365 infrastructure.

Sponsored by: