Viewing entries tagged
module

PSElvUI, PowerShell ElvUI Updater Module

PSElvUI, PowerShell ElvUI Updater Module

What's it do?

This post is mostly for the World of Warcraft nerds out there. /raises hand
This module allows you to:

  • Check / Update ElvUI if there is a newer version

  • Install ElvUI if it is not already installed

  • Simply check if there is a new version, but do nothing else

Why? Isn't there already a tool for this?

There is a tool provided by the fine folks that created the AddOn, and it works great. I wanted a way to possible automate it (maybe a scheduled task on boot), and a fast way to check/update without having to login/launch anything other than PowerShell (which I happen to already have up most of the time anyway).

How can I install it?

Currently this is only working on Windows, with Mac support coming soon.

You can install the module from the PowerShell Gallery by using (as admin):

Install-Module PSElvUI

For more detailed instructions, check out the README.md here.

install.PNG

Using the module

Using the module is simple!

Check for update/don’t install:

Invoke-ElvUICheck -OnlyCheck -Verbose
checkonly.PNG

Check/Update if there is an update:

Invoke-ElvUICheck -Verbose
update.PNG

Install if it doesn’t exist:

Invoke-ElvUICheck -InstallIfDoesntExist -Verbose
install2.PNG

And that’s it! Leave a comment if you have any suggestions or ideas.

I gotta tissue if you have an issue, but also feel free to let me know what doesn’t work for you here.

Deep Dive

This module was also a way for me to get some Pester tests written. You can check out the code, as well as the tests written via the GitHub repo for this module.

Output for Invoke-Pester in the folder for this module:

pester.PNG

Get-WowInstallPath.tests.ps1

This file contains some very basic tests. It calls the functions and verifies that the paths returned exist via Pester’s exist function.

InModuleScope PSElvUi {

    describe 'Get-WowInstallPath' {

        $wowInfo = Get-WowInstallPath

        it 'Returns WoW Install Path' {

            $wowInfo.WoWInstallPath | Should Exist

        }

        it 'Finds addons folder in path' {

            $wowInfo.AddonsFolder | Should Exist

        }
    }
}

Get-WowInstallPath
This function uses the Windows registry to get the WoW install path. Mac support will do something different when it is added. Here is the code for now:

function Get-WoWInstallPath {
    [cmdletbinding()]
    param(

    )

    Write-Verbose "Attempting to find WoW install path..."

    try {

        $wowInstallPath = (Get-Item 'hklm:\SOFTWARE\WOW6432Node\Blizzard Entertainment\World of Warcraft').GetValue('InstallPath')
        $addonsFolder   = "$($wowInstallPath)Interface\AddOns" 

        $wowInstallInfo = [PSCustomObject]@{

            AddonsFolder   = $addonsFolder
            WowInstallPath = $wowInstallPath

        }

        return $wowInstallInfo
        
    }

    catch {

        $errorMessage = $_.Exception.Message 
        throw "Error determining WoW Install Path/ElvUi Version -> [$errorMessage]!"
        break

    }
}

It returns both the install path, and the addons folder location.

Finding the version number of ElvUI

Remote (Get-RemoteElvUiVersion)

The remote code check utilizes Invoke-WebRequest to get the version number. This method can fail when the website gets updated. There is no public API for this, so for now this is the method that works.

function Get-RemoteElvUiVersion {
    [cmdletbinding()]
    param(

    )
    
    try {
        
        $baseUrl      = 'https://www.tukui.org'
        $downloadPage = "$baseUrl/download.php?ui=elvui"
        $dlString     = '.+Download ElvUI.+'

        Write-Verbose "Attempting to retrieve ElvUI information from [$downloadPage]..."
        $downloadLink = "$baseUrl$(Invoke-WebRequest -Uri $downloadPage | 
                                    Select-Object -ExpandProperty Links | 
                                    Where-Object {
                                        $_.Outerhtml -match $dlString
                                    }                                   | 
                                    Select-Object -ExpandProperty href)" 
    
        $fileName             = $($downloadLink.Split('/')[4])
        [double]$elvUiVersion = $fileName.Split('-')[1].Replace('.zip','')
    
        $remoteElvInfo = [PSCustomObject]@{
    
            FileName     = $fileName
            Version      = $elvUiVersion
            DownloadLink = $downloadLink
    
        }
    
        return $remoteElvInfo 

    }
    catch {
        $errorMessage = $_.Exception.Message
        throw "Error getting remote ElvUI Information -> [$errorMessage]"
    }
}

Local (Get-LocalElvUiVersion)

The local version check loops through the contents of the ElvUI.toc file and matches on the ‘## Version”’ line. Then some string manipulation is used to grab the version. Error handling is in place to ensure issues are caught if they arise.

function Get-LocalElvUiVersion {
    [cmdletbinding()]
    param(
        [string]
        $addonsFolder
    )

    [double]$localVersion = 0.0

    if ((Test-Path $addonsFolder)) {

        try {

            $toc = Get-Content -Path "$addonsFolder\ElvUI\ElvUI.toc" -ErrorAction Stop

            $toc | ForEach-Object {

                if ($_ -match "## Version:") {
        
                    $localVersion = $_.Split(':')[1].trim()                    

                }
            }

            if ($localVersion -ne 0.0) {

                return $localVersion

            } else {

                throw 'No luck finding version in file'

            }
            

        }
        catch [System.Management.Automation.ItemNotFoundException] {

            throw "ElvUI addon not found!"

        }
        catch {            

            $errorMessage = $_.Exception.Message
            throw "Error determining ElvUI version -> [$errorMessage]!"

        }
        
    } else {

        throw "Unable to access WoW addon folder [$addonsFolder]!"

    }                
}

If you’ve gotten this far, I appreciate your interest! If you’d like more deep dives into the inner workings, leave a comment and let me know.

PowerShell: Ninja Downloader (Modular File Downloading Utility)

PowerShell: Ninja Downloader (Modular File Downloading Utility)

Download Files With PowerShell Dynamically!

Knowing PowerShell can come in handy when you need to download files. Invoke-WebRequest is the command to get to know when working with web parsing, and obtaining downloads.

I've noticed, however, that different files show up as different content types, and parsing out the file name requires all sorts of voodoo. What if there was a way to use one tool that could utilize the power of PowerShell, and make downloading files a modular experience?

This tool, and blog post, are is inspired by folks asking me for help downloading files via PowerShell. I always appreciate feedback and questions, and this is exactly why!

Prerequisites

  • PowerShell 3.0+ 
  • Access to the internet

Ninja Downloader Overview

Ninja Downloader works by executing the main script (download.ps1), which takes the following parameters:

  • DownloadName
    • This is the name of the script you'd like to execute (use 'all' to execute all scripts)
    • Scripts are located in the .\scripts directory
    • Argument must omit the .ps1 extension of the script in the .\scripts directory
  • OutputType
    • This parameter let's you specify the output type, the default is none.
      • XML -> Export results as clixml
      • CSV -> Export results as a CSV file (default)
      • HTML -> Export results as an HMTL file
      • All -> Export results as all of the above
    • Output is exported to the .\output directory
  • DownloadFolder
    • This parameter allows you to specify a location to place the downloaded files
      • Folder will be created if it does not exist
    • If left empty the folder .\downloads will be used
  • UnZip
    • This parameter will look for zip archives after files are downloaded and attempt to extract them
      • Files extracted to .\downloads\fileName_HHmm-MMddyy\
  • ListOnly
    • This parameter (a switch) will give you a list of all possible names to use for DownloadName, as well as the paths to the scripts.

Downloading a File

There are several scripts included by default with tool. 

  • Ccleaner.ps1
  • Chrome.ps1
  • FireFox.ps1
  • Java.ps1
  • Skype.ps1
  • template.ps1 (never executed, this is a template for creating your own download script)

So how do we use them, then?

  1. Open PowerShell, and navigate to the root directory of the project/script.
  2. Run the following code:
$downloadResults = .\download.ps1 -DownloadName ccleaner

This will give us access to the results in $downloadResults.

downloadresults.PNG

You can see that the results we get include the name of the script executed, if it was executed successfully, any errors, and another object inside of this object named FileInfo.

FileInfo contains the file namepath to the file (full), any errors, and if we could verify it exists.

This attempt was successful, and our results echoed that! Let's take a look in the downloads folder just to be sure...

Awesome!

Downloading All Files

What if we wanted to download all the files via every script in the .\scripts folder?

  1. Open PowerShell, and navigate to the root directory.
  2. Run the following code:
$downloadResults = .\download.ps1 -DownloadName all -Verbose

This time we can see some of the output as it happens via the -Verbose switch.

Now let's take a look at $downloadResults:

For good measure, we'll also look at the downloads folder:

Alright! It worked.

Output Types

This script allows you output the results in various ways. All of the results will be time-stamped with the date and time.

CSV

To output results as a CSV, run:

$downloadResults = .\download.ps1 -DownloadName all -OutputType csv

After it runs, the results will be in the .\output folder.

csv.PNG

HTML

To output results as HTML, run:

$downloadResults = .\download.ps1 -DownloadName all -OutputType html

After it runs, the results will be in the .\output folder.

XML

To output results as XML, run:

$downloadResults = .\download.ps1 -DownloadName all -OutputType xml

After it runs, the results will be in the .\output folder.

All

To output results in all three formats, run:

$downloadResults = .\download.ps1 -DownloadName all -OutputType all

After it runs, the results will be in the .\output folder.

Creating Your Own Download Script

You can create your own script to use with the Ninja Downloader tool. The template provided is a working example of how Firefox is downloaded. The template is located in the .\scripts folder.

Template code:

#Template example (works for Firefox, adjust as needed for your download)

#Set this to the URL you'll be navigating to first
$navUrl    = 'https://www.mozilla.org/en-US/firefox/all/' 

#Text to match on, if applicable
$matchText = '.+windows.+64.+English.+\(US\)'

# IMPORTANT: This is the format of the object needed to be returned to the description
# Whichever way you get the information, you need to return an object with the following properties:
# DownloadName (string, file name)
# Content (byte array, file contents)
# Success (boolean)
# Error (string, any error received)
$downloadInfo = [PSCustomObject]@{

    DownloadName = ''
    Content      = ''
    Success      = $false
    Error        = ''  

} 

#Go to first page
Try {

    $downloadRequest = Invoke-WebRequest -Uri $navURL -MaximumRedirection 0 -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox -ErrorAction SilentlyContinue

}
Catch {

    $errorMessage = $_.Exception.Message

    $downloadInfo.Error = $errorMessage

    return $downloadInfo

}

#Look for urls that match
$downloadURL = $downloadRequest.Links | Where-Object {$_.Title -Match $matchText} | Select-Object -ExpandProperty href

#Go to matching URL, look for download file (keeping redirects at 0)
try {

    $downloadRequest = Invoke-WebRequest -Uri $downloadURL -MaximumRedirection 0 -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox -ErrorAction SilentlyContinue

}
catch {
    
    $errorMessage = $_.Exception.Message

    $downloadInfo.Error = $errorMessage

    return $downloadInfo

}

#Get file info
$downloadFile = $downloadRequest.Headers.Location

#Parse file name, whichever way needed
if ($downloadRequest.Headers.Location) {
            
    $downloadInfo.DownloadName = $downloadFile.SubString($downloadFile.LastIndexOf('/')+1).Replace('%20',' ')

}  

#Switch out the StatusDescription, as applicable
Switch ($downloadRequest.StatusDescription) {

    'Found' {
                
        $downloadRequest = Invoke-WebRequest -Uri $downloadRequest.Headers.Location -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox 

    }

    default {

        $downloadInfo.Error = "Status description [$($downloadRequest.StatusDescription)] not handled!"
        
        return $downloadInfo

    }

}

#Switch out the proper content type for the file download
Switch ($downloadRequest.BaseResponse.ContentType) {

    'application/x-msdos-program' {
                
        $downloadInfo.Content = $downloadRequest.Content
        $downloadInfo.Success = $true

        return $downloadInfo

    }
   
    Default {

        $downloadInfo.Error = "Content type [$($downloadRequest.BaseResponse.ContentType)] not handled!"
        
        return $downloadInfo

    }

}

What matters the most is that you return an object that has the following properties:

  • DownloadName
    • String value of the downloaded file name
  • Content
    • Byte array containing the actual file
  • Success
    • Boolean (true if the file was able to be downloaded)
  • Error
    • String representing any errors encountered

Example script creation:

For this example, let's download ElvUI (as we learned how to in detail, here).

First, I'll save the template as elvui.ps1

Then, based on our knowledge of web parsing (more here if you would like to learn more about Invoke-WebRequest), we can edit the template to reflect the correct web parsing needs and content type.

Full code for elvui.ps1

#Template example (works for Firefox, adjust as needed for your download)

#Set this to the URL you'll be navigating to first
$navUrl    = 'http://www.tukui.org/dl.php' 

# IMPORTANT: This is the format of the object needed to be returned to the description
# Whichever way you get the information, you need to return an object with the following properties:
# DownloadName (string, file name)
# Content (byte array, file contents)
# Success (boolean)
# Error (string, any error received)
$downloadInfo = [PSCustomObject]@{

    DownloadName = ''
    Content      = ''
    Success      = $false
    Error        = ''  

} 

#Go to first page
Try {

    $downloadRequest = Invoke-WebRequest -Uri $navUrl -UserAgent [Microsoft.PowerShell.Commands.PSUserAgent]::FireFox -ErrorAction SilentlyContinue

}
Catch {

    $errorMessage = $_.Exception.Message

    $downloadInfo.Error = $errorMessage

    return $downloadInfo

}

#Look for urls that match
$downloadURL = ($downloadRequest.Links | Where-Object {$_ -like '*elv*' -and $_ -like '*download*'}).href
$downloadInfo.DownloadName = $downloadURL.Substring($downloadURL.LastIndexOf('/')+1)

#Go to matching URL, look for download file (keeping redirects at 0)
try {
     
    $downloadRequest = Invoke-WebRequest -Uri $downloadURL 

}
catch {
    
    $errorMessage = $_.Exception.Message

    $downloadInfo.Error = $errorMessage

    return $downloadInfo

}

#Switch out the proper content type for the file download
Switch ($downloadRequest.BaseResponse.ContentType) {

    'application/zip' {
                
        $downloadInfo.Content = $downloadRequest.Content
        $downloadInfo.Success = $true

        return $downloadInfo

    }
   
    Default {

        $downloadInfo.Error = "Content type [$($downloadRequest.BaseResponse.ContentType)] not handled!"
        
        return $downloadInfo

    }

}

Let's test it out!

.\download.ps1 -downloadName elvui

And there you have it, modular file downloading via PowerShell.

Github Repository

This project is available on Github!

Click here to go to the psNinjaDownloader repository.

You can download the contents as a ZIP file by going to 'Clone or download', and then selecting 'Download ZIP'.

If you download the code, and would like to run it, you'll need to unblock download.ps1 first.

To do this, right click download.ps1, and go to Properties, and check 'unblock', then click Apply/OK.

unblock.PNG

You'll also need to do this for all the scripts in the .\scripts folder

You can repeat the above step for every script in the .\scripts folderor we can use PowerShell to do it!

  1. In PowerShell, navigate to the .\scripts folder of psNinjaDownloader
  2. Run the following command:
Get-ChildItem | Unblock-File

Voila, you now have a working copy of the script!

What's Next?

  • Create your own download scripts, and test downloading things auto-magically.
  • Use this to download the latest version of tools as a schedule task
  • See the full help for the script by running: 
Get-Help .\download.ps1 -Full

If you have any feedback, questions, or issues, leave a comment here or contact me!

[top]

PowerShell: Building a basic text menu system (part 1)

PowerShell: Building a basic text menu system (part 1)

Menus?! But...why?

As I dove deeper into PowerShell years ago I started to think about ways to help those utilizing my scripts. I wrote a script that became a module that the whole team would use.

In this module there were a ton of functions. Some functions were for creating new hires, others for terminations, and even more for general AD user account and Exchange management. Once there were more than 6 different functions I created a basic menu system for the Helpdesk to use when using PowerShell with my module.

Where to start

The first thing you'll need to do is present the user with options. Write-Host is my favorite way to do this. You'll want to define the variable $foregroundColor to see the full effect of the following code. I like to keep that set as a global variable so it can be changed later if desired. This is of course, very important, as who doesn't want to sometimes tweak the color output of your scripts? 

I also keep a version number in the variable $ncVer.

Building the menu:

Write-Host `n"Ninja Center v" $ncVer -ForeGroundColor $foregroundcolor
Write-Host `n"Type 'q' or hit enter to drop to shell"`n
Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Active Directory"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "A" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "A1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Look up a user"
Write-Host -NoNewLine `t`n "A2 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Enter PowerShell session on DC"
Write-Host -NoNewLine `t`n "A3 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Run DCDiag on a DC (or all DCs)"`n`n

Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Exchange"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "E" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "E1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Forward a mailbox"
Write-Host -NoNewLine `t`n "E2 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Clear a mailbox forward"
Write-Host -NoNewLine `t`n "E3 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "See if an IP address is being relayed"`n`n

Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "Storage"
Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "["
Write-Host -NoNewLine "S" -foregroundcolor $foregroundColor
Write-Host -NoNewLine "]"

Write-Host -NoNewLine `t`n "S1 - " -foregroundcolor $foregroundcolor
Write-host -NoNewLine "Connect to a NetApp controller"`n`n

When starting out I would use Write-Host to display information as I wrote scripts. This was nice, and was useful for troubleshooting. Then as my knowledge expanded I began to realize how else it worked. My new favorite, especially when building menus, is -NoNewLine. 

What -NoNewLine allows you to do is alternate different colors on the same line. Again, extremely uhhh... important. The output does look nice, though!

There are also some escaped string options I like to use. They are `t (horizontal tab), and -n(new line). These are used to keep the menu looking nice and not too cluttered.

OK so I built something that shows a bunch of stuff... what now?

Now we'll need to get some input from the user and take action! To do this we'll use Read-Host combined with a Switch to deal with the input.

$sel = Read-Host "Which option?"

Switch ($sel) {
    "A1" {Get-ADinfo;Load-NinjaCenter}
    "A2" {Enter-DCSession}
    "A3" {
        $DCs      = Read-Host "DC (specify name or put 'all' for all)?"
        $test     = Read-Host "Enter 'error' or 'full' for test feedback"         
       
        
        $global:dcDiagResults = Get-DCDiagInfo -DC $DCs -Type $test -Verbose
        
        Write-Host `n"Results stored in the variable: dcDiagResults"`n
        Write-Host -NoNewLine "Type "
        Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
        Write-Host -NoNewLine "to load the menu again."`n
    }
    
    "E1" {Forward-Email}
    "E2" {Clear-Forward}
    "E3" {Check-EXRelayIP}
    
    "S1" {
        Connect-NetAppController
    
        Write-Host -NoNewLine "Type "
        Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
        Write-Host -NoNewLine "to load the menu again."`n
}

    {($_ -like "*q*") -or ($_ -eq "")} {
        
        Write-Host `n"No input or 'q' seen... dropping to shell" -foregroundColor $foregroundColor
        Write-Host "Type Load-NinjaCenter to load them menu again"`n
        
        
    }          
        
}

Alright, now let's take some action!

As you can see above, the switch handles the user input. There are some more advanced ways to parse the input, which I will go over in a later post. For now, you can see a few things.

One is that you can stage other functions based on the input. For example, with the switch option "A3", (which is the menu option for running DC Diag against DCs), you can see that we provide the user with some more prompts to prep some variables to pass to the Get-DCDiagInfo function.

I actually wrote about my Get-DCDiagInfo (function, but also works as a standalone script), and you can read about it here.

This allows you to wrap your existing functions in different ways and present them to folks that will use them however you'd like to. For a Helpdesk environment, or those not super deep into PowerShell, this is a nifty way for them to encapsulate and utilize what you've written. 

Let's put it all together.

Here is the full code for the menu function:

function Load-NinjaCenter {  
[cmdletbinding()]
param()

    ##########################################
    #            Ninja Center Menu           #
    ##########################################
    Write-Host `n"Ninja Center v" $ncVer -ForeGroundColor $foregroundcolor
    Write-Host `n"Type 'q' or hit enter to drop to shell"`n
    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Active Directory"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "A" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "A1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Look up a user"
    Write-Host -NoNewLine `t`n "A2 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Enter PowerShell session on DC"
    Write-Host -NoNewLine `t`n "A3 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Run DCDiag on a DC (or all DCs)"`n`n

    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Exchange"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "E" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "E1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Forward a mailbox"
    Write-Host -NoNewLine `t`n "E2 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Clear a mailbox forward"
    Write-Host -NoNewLine `t`n "E3 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "See if an IP address is being relayed"`n`n

    Write-Host -NoNewLine "<" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "Storage"
    Write-Host -NoNewLine ">" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "["
    Write-Host -NoNewLine "S" -foregroundcolor $foregroundColor
    Write-Host -NoNewLine "]"

    Write-Host -NoNewLine `t`n "S1 - " -foregroundcolor $foregroundcolor
    Write-host -NoNewLine "Connect to a NetApp controller"`n`n

    $sel = Read-Host "Which option?"

    Switch ($sel) {
        "A1" {Get-ADinfo;Load-NinjaCenter}
        "A2" {Enter-DCSession}
        "A3" {
            $DCs      = Read-Host "DC (specify name or put 'all' for all)?"
            $test     = Read-Host "Enter 'error' or 'full' for test feedback"         
        
            
            $global:dcDiagResults = Get-DCDiagInfo -DC $DCs -Type $test -Verbose
            
            Write-Host `n"Results stored in the variable: dcDiagResults"`n
            Write-Host -NoNewLine "Type "
            Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
            Write-Host -NoNewLine "to load the menu again."`n
        }
        
        "E1" {Forward-Email}
        "E2" {Clear-Forward}
        "E3" {Check-EXRelayIP}
        
        "S1" {
            Connect-NetAppController
        
            Write-Host -NoNewLine "Type "
            Write-Host -NoNewLine "Load-NinjaCenter " -foregroundcolor $foregroundcolor
            Write-Host -NoNewLine "to load the menu again."`n
    }

        {($_ -like "*q*") -or ($_ -eq "")} {
            
            Write-Host `n"No input or 'q' seen... dropping to shell" -foregroundColor $foregroundColor
            Write-Host "Type Load-NinjaCenter to load them menu again"`n
            
            
        }          
            
    }
    ##########################################
    #        End Ninja Center Menu           #
    ##########################################

}

What you'd do to run it is put it in a module you created and then call the function when the module is imported at the end of your script. Once exited, it could be called by its function name at any time to bring the menu back up.

Some things I've noticed

When I started sharing my scripts this way I noticed that people will find ways to break your code. This is a great way to learn the weak points of your scripts and become even better at writing them. I hope you've found this useful, and as always I appreciate any feedback!