PowerShell - Passing hashtables to a function - powershell

I'm in the process of writing a CLI for a thing I'm working on, and at various points in the process I want to ask the user for input. Each time I ask the user for input, the questions/answers are likely to be different so I started out with something like:
$choices = [Management.Automation.Host.ChoiceDescription[]] #(
New-Object Management.Automation.Host.ChoiceDescription("&Yes","Option Description for Yes")
New-Object Management.Automation.Host.ChoiceDescription("&No","Option Description for No.")
)
$choice = $Host.UI.PromptForChoice("Question Title","Question Text",$choices,1)
This works pretty well, but it's a bit clunky when it comes to re-use especially if the number of choices expands.
I want to wrap it in a function that I can call easier - like:
$options = #{
Yes = "Option Description for Yes"
No = "Option Description for No"
}
askQuestion -title "Question Title" -question "Question Text" -options $options
So far so good. The bit I'm struggling with is accessing the properties of $options:
function askQuestion {
param (
[hashtable]$options,
[string]$title,
[string]$question
)
Write-Host $title -ForegroundColor Cyan
Write-Host $question -ForegroundColor Cyan
foreach($option in $options)
{
# Do stuff HERE
}
}
If I just access $option directly in the foreach, it prints out a table like:
Name Value
---- -----
No Option Description for No
Yes Option Description for Yes
If I try accessing $option.Name or .Value it doesn't seem to do anything at all.
Can someone point out where I'm going wrong with this please?

I think you can use the GetNumerator() method to iterate through the hash table entries. Then create a custom message using the format operator -f. $i here is just something to track a number for each line in the output. This should be fairly dynamic provided your values/descriptions are consistently worded so there are no grammatical/comprehension issues.
$i = 1
foreach ($option in $options.GetEnumerator()) {
"{2}. Enter {0} for {1}" -f $option.key,$option.value,$i++
}

For anyone interested, this is what this finished up looking like:
function askQuestion {
param (
[hashtable]$options,
[string]$title,
[string]$question
)
$choices = [Management.Automation.Host.ChoiceDescription[]] #(
foreach ($option in $options.GetEnumerator()) {
$selection = $option.key
$description = $option.value
New-Object Management.Automation.Host.ChoiceDescription("&$selection",$description)
}
)
$choice = $Host.UI.PromptForChoice($title,$question,$choices,1)
return
}
And it can be called with something like this, where $options is very flexible.
$options = #{
Yes = "Yes Description"
No = "No Description"
Maybe = "Maybe Description"
}
askQuestion -title "Question Title" -question "Question Text" -options $options

Related

Read-host not passing variable?

So I writing some code to run some patching on AWS, I have the following script snippet taken out of the whole thing for now.. I seem to be running into an issue with $PSBoundParameters..
(It's Friday & I've had a long week so I may just need to sleep on it) - but I can't seem to pass anything out of read-host...
param (
[Parameter(Mandatory = $true)][ValidateNotNullorEmpty()][string]$Prof,
[Parameter(Mandatory = $false)][ValidateNotNullorEmpty()][string]$Reminder,
[Parameter(Mandatory = $false)][ValidateNotNullorEmpty()][string]$AddToTGs,
[Parameter(Mandatory = $false)][ValidateNotNullorEmpty()][string]$PatchType,
[Parameter(Mandatory = $false)][ValidateNotNullorEmpty()][string]$Instance,
[Parameter(Mandatory = $false)][ValidateNotNullorEmpty()][string]$Environment
)
function PromptInstance {
$Instance = Read-Host -Prompt "Please Specify the Instance"
Write-Host "Using: $Instance" -ForegroundColor Cyan
}
function PromptEnvtoPatch {
$Environment = Read-Host -Prompt "Please Specify the Environment (e.g. dev)"
Write-Host "Using: $Environment" -ForegroundColor Cyan
}
function PromptReminder {
$title = "Calendar"
$message = "Do you want to Add an Outlook Reminder in 24 Hrs?"
$pA = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Adds an Outlook Reminder"
$pB = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Won''t add a reminder"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($pA, $pB)
$CalResult = $host.ui.PromptForChoice($title, $message, $options, 0)
switch ($CalResult)
{
0 {
$global:CalRes = 1
Write-Host "Reminder will be added.." -ForegroundColor Cyan
}
1 {
$global:CalRes = 0
Write-Host "No Reminder will be added.." -ForegroundColor Cyan
}
}
}
function PromptAddToTGs {
$title = "Re-Add to Target Groups"
$message = "Do you want to have this script Automatically add the instances back into the Target Groups after Patching?"
$pA = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Will ADD the instance back into Target Groups"
$pB = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Will NOT add the instance back into Target Groups"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($pA, $pB)
$result = $host.ui.PromptForChoice($title, $message, $options, 1)
switch ($result)
{
0 {
$global:AddTGRes = 1
Write-Host "Instances WILL be added back into Target Groups" -ForegroundColor Cyan
}
1 {
$global:AddTGRes = 0
Write-Host "Instances will NOT be added back into Target Groups" -ForegroundColor Cyan
}
}
}
function PromptPatchType {
$title = "Patching Type"
$message = "Do you want to Patch a Single Instance, or ALL Instances for a specific Team Env?"
$pA = New-Object System.Management.Automation.Host.ChoiceDescription "&Instance", "Patches an Instance"
$pB = New-Object System.Management.Automation.Host.ChoiceDescription "&ALL Instances for an Env", "Patches ALL Instances in a Team Env"
$pQ = New-Object System.Management.Automation.Host.ChoiceDescription "&Quit", "Cancel/Exit"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($pA, $pB, $pQ)
$PatchResult = $host.ui.PromptForChoice($title, $message, $options, 0)
switch ($PatchResult)
{
0 {
$Instance = Read-Host "Please Specify the Instance Id"
}
1 {
$Environment = Read-Host "Please Specify the Team (i.e. dev)"
}
2 {
Write-Host "You Quitter!... :-)"
Exit
}
}
}
function KickOffPatchingEnv {
param ($Prof, $Reg, $Reminder, $AddToTGs, $PatchType, $Environment)
Write-Host "Using the Following Options: (Profile:$Prof) (Region:$Reg) (Reminder:$Reminder) (AddToTGs:$AddToTGs) (PatchType:$PatchType) (Environment:$Environment)"
}
function KickOffPatchingInst {
param ($Prof, $Reg, $Reminder, $AddToTGs, $PatchType, $Instance)
Write-Host "Using the Following Options: (Profile:$Prof) (Region:$Reg) (Reminder:$Reminder) (AddToTGs:$AddToTGs) (PatchType:$PatchType) (Instance:$Instance)"
}
switch -wildcard ($Prof) {
"*dev*" { $Reg = "eu-west-1"; $Bucket = "s3-dev-bucket" }
"*admin*" { $Reg = "eu-west-1"; $Bucket = "s3-admin-bucket" }
"*prod*" { $Reg = "eu-west-1"; $Bucket = "s3-prod-bucket" }
"*staging*" { $Reg = "eu-west-1"; $Bucket = "s3-staging-bucket" }
}
if (!$PSBoundParameters['Reminder']) { PromptReminder }
if (!$PSBoundParameters['AddToTGs']) { PromptAddToTGs }
if ($PSBoundParameters['PatchType']) {
if (($PatchType -eq "i") -or ($PatchType -eq "instance") -or ($PatchType -eq "I") -or ($PatchType -eq "Instance")) {
if (!$PSBoundParameters['Instance']) { PromptInstance }
KickOffPatchingInst $Prof $Reg $Reminder $AddToTGs $PatchType $Instance
}
if (($PatchType -eq "a") -or ($PatchType -eq "all") -or ($PatchType -eq "A") -or ($PatchType -eq "All")) {
if (!$PSBoundParameters['Environment']) { PromptEnvtoPatch }
KickOffPatchingEnv $Prof $Reg $Reminder $AddToTGs $PatchType $Environment
}
} else { PromptPatchType }
If I use the parameters on the command line, it works fine..
PS C:\Users\myself\Desktop> .\test.ps1 -Prof dev -Reminder y -AddToTGs y -PatchType a -Environment dev
Using the Following Options: (Profile:dev) (Region:eu-west-1) (Reminder:y) (AddToTGs:y) (PatchType:a) (Environment:dev)
But if I omit an option, say for instance the Environment, I'm prompted for it, but the value is not displayed..
PS C:\Users\myself\Desktop> .\test.ps1 -Prof dev -Reminder y -AddToTGs y -PatchType a
Please Specify the Environment (e.g. dev): dev
Using: dev
Using the Following Options: (Profile:dev) (Region:eu-west-1) (Reminder:y) (AddToTGs:y) (PatchType:a) (Environment:)
Environment is empty....
I've tried loads of different things such as setting global:Environment etc, but I always seem to be missing out whichever variable isn't specified in the command?
Maybe this isn't the best way to write this, but i've never used $PSBoundParameters before so this is my first time trying it out..
Can anyone see my glaring error here at all?
TIA :)
#Mathias R. Jessen answered this for me.
I put this in the function
function PromptProfile {
$Prof = Read-Host -Prompt "Please Specify the Profile"
$global:UserProf = $Prof
}
then changed code later on with the global variable, so I can use them

How do I ask for confirmation? [duplicate]

This question already has answers here:
do until user input yes/no
(3 answers)
How to Use -confirm in PowerShell
(11 answers)
Closed 3 years ago.
I want to create a confirmation box. I want to have a popup box that says that it appears that this is the factory key. Do you wish to continue? and if so proceed with the rest of the script if not, exit script.
The following is my script:
if (Test-Path $USB2\sources\install3.swm) {
$caption = "*** IT APPEARS THIS IS A FACTORY KEY ***"
$message = "Are you Sure You Want To Proceed:"
[int]$defaultChoice = 1
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "I understand, flash over it."
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$choiceRTN = $host.UI.PromptForChoice($caption, $message, $options, $defaultChoice)
if ( $choiceRTN -ne 1 ) {
"Your Choice was Yes"
} else {
Stop-Process -Id $PID
}
}
See the bit, $options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no). Where I am stuck is the "Your Choice was yes" I don't need that... I just need it to continue with the script if yes. How do I do that?
Consider using the System.Windows.MessageBox...
Add-Type -AssemblyName PresentationFramework
[System.Windows.MessageBox]::Show('It appears that this is the factory key. Do you wish to continue?', 'Confirmation', 'YesNo');
For your use case,
# You need this to use System.Windows.MessageBox
Add-Type -AssemblyName 'PresentationFramework'
# By default, you're blasting the USB drive
$continue = 'Yes'
# If you see the file, prompt the user
if ( $(Test-Path -Path "${USB2}\sources\install3.swm") ) {
$caption = "*** IT APPEARS THIS IS A FACTORY KEY ***"
$message = "Are you Sure You Want To Proceed:"
$continue = [System.Windows.MessageBox]::Show($message, $caption, 'YesNo');
}
if ($continue -eq 'Yes') {
# The user either (a) said "yes" to the "do you prompt" or
# (b) the file didn't exist.
Write-Output "Flash over..."
# ...do the flashing here...
} else {
# The user said "no" to the flash prompt
Write-Output "Terminating process..."
Start-Sleep 1
}
The message box will return Yes or No, and you can use that in your control flow to determine next steps. If that includes closing the window, call exit.
The below prompts for input, stores the clicked value in a variable, and returns it as a string. Checking to see the value of $var and proceeding from there should be simple enough.
$var = [System.Windows.MessageBox]::Show('It appears this is a factory key. Do you wish to continue?','DialogTitle','YesNoCancel','Warning')
echo("The value of the dialog box is $($var)")

PowerShell PromptForChoice

I have the following problem.
I want to offer the user of my powershell script the choice of what Azure Subscription to use. I've been following this example (included because it also shows the part I can't figure out).
Example
$title = "Delete Files"
$message = "Do you want to delete the remaining files in the folder?"
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", `
"Deletes all the files in the folder."
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", `
"Retains all the files in the folder."
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$result = $host.ui.PromptForChoice($title, $message, $options, 0)
I can get as far as offering the list with the following
My version
After connecting.....
$title = "Azure subscriptions"
$message = "Please pick a subscription"
$all_subscriptions = Get-AzureRmSubscription
$options = [System.Management.Automation.Host.ChoiceDescription[]]($all_subscriptions.name)
$result = $host.ui.PromptForChoice($title, $message, $options, 0)
I'm aware this is missing the part of the code that specifies the choice which in the example is this
New-Object system.Management.Automation.Host.ChoiceDescription "&Yes", `
"Deletes all the files in the folder."`
I've tried a foreach loop using the $all_subscriptions.name but this (at least how I done this) doesn't seem to work. Can anyone point me in the right direction here. Is PromptForChoice even the right way to do this?
TLDR;
How do I build a dynamic list a user can select from using PromptForChoice within powershell
Assuming you have less than 10 choices to pick from, you can prepend &N and generate the choice descriptions on the fly with hot keys then numbered 1 - 9:
$choiceIndex = 1
$options = $all_subscriptions |ForEach-Object {
New-Object System.Management.Automation.Host.ChoiceDescription "&$($choiceIndex++) - $($_.Name)"
}
$chosenIndex = $host.ui.PromptForChoice($title, $message, $options, 0)
$SubscriptionToUse = $all_subscriptions[$chosenIndex]
Looks like you can just pass a string array as choices.
[string[]]$choices = "machine", "another machine", "the third one", "obviously this one!"
$result = $host.ui.PromptForChoice($title, $message, $choices, 0)
Write-Host $choices[$result] -ForegroundColor Green
$result is an integer representing the indices of the choice. We can then refer back to our array and grab the item at that position and hey-presto - you get your result!

Powershell input box with multiple inputfields

I've found this code to create simple input box for powershell:
[void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
$title = 'Demographics'
$msg = 'Enter your demographics:'
$text = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title)
Does anyone know how to add more input fields?
WPF, XAML, .NET, and ShowUI are all options available to you.
https://learn-powershell.net/2012/09/13/powershell-and-wpf-introduction-and-building-your-first-window/
http://showui.codeplex.com/
alternatively, here is an example using the Show-Command cmdlet
function askforinfo {
param (
[string]$Demographics,
[validateset(1, 2, 3)]
[string]$otherstuff
)
[pscustomobject]#{
Demographics = $Demographics
OtherStuff = $otherstuff
}
}
$result = Invoke-Expression (Show-Command askforinfo -PassThru)
$result

How to Use -confirm in PowerShell

I'm trying to take user input and before proceeding I would like get a message on screen and than a confirmation, whether user wants to proceed or not. I'm using the following code but its not working:
write-host "Are you Sure You Want To Proceed:" -Confirm
-Confirm is a switch in most PowerShell cmdlets that forces the cmdlet to ask for user confirmation. What you're actually looking for is the Read-Host cmdlet:
$confirmation = Read-Host "Are you Sure You Want To Proceed:"
if ($confirmation -eq 'y') {
# proceed
}
or the PromptForChoice() method of the host user interface:
$title = 'something'
$question = 'Are you sure you want to proceed?'
$choices = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes'))
$choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No'))
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 0) {
Write-Host 'confirmed'
} else {
Write-Host 'cancelled'
}
Edit:
As M-pixel pointed out in the comments the code could be simplified further, because the choices can be passed as a simple string array.
$title = 'something'
$question = 'Are you sure you want to proceed?'
$choices = '&Yes', '&No'
$decision = $Host.UI.PromptForChoice($title, $question, $choices, 1)
if ($decision -eq 0) {
Write-Host 'confirmed'
} else {
Write-Host 'cancelled'
}
This is a simple loop that keeps prompting unless the user selects 'y' or 'n'
$confirmation = Read-Host "Ready? [y/n]"
while($confirmation -ne "y")
{
if ($confirmation -eq 'n') {exit}
$confirmation = Read-Host "Ready? [y/n]"
}
Read-Host is one example of a cmdlet that -Confirm does not have an effect on.-Confirm is one of PowerShell's Common Parameters specifically a Risk-Mitigation Parameter which is used when a cmdlet is about to make a change to the system that is outside of the Windows PowerShell environment. Many but not all cmdlets support the -Confirm risk mitigation parameter.
As an alternative the following would be an example of using the Read-Host cmdlet and a regular expression test to get confirmation from a user:
$reply = Read-Host -Prompt "Continue?[y/n]"
if ( $reply -eq 'y' ) {
# Highway to the danger zone
}
The Remove-Variable cmdlet is one example that illustrates the usage of the -confirm switch.
Remove-Variable 'reply' -Confirm
Additional References: CommonParameters, Write-Host, Read-Host, Comparison Operators, Regular Expressions, Remove-Variable
Here is the documentation from Microsoft on how to request confirmations in a cmdlet. The examples are in C#, but you can do everything shown in PowerShell as well.
First add the CmdletBinding attribute to your function and set SupportsShouldProcess to true. Then you can reference the ShouldProcess and ShouldContinue methods of the $PSCmdlet variable.
Here is an example:
function Start-Work {
<#
.SYNOPSIS Does some work
.PARAMETER Force
Perform the operation without prompting for confirmation
#>
[CmdletBinding(SupportsShouldProcess=$true)]
param(
# This switch allows the user to override the prompt for confirmation
[switch]$Force
)
begin { }
process {
if ($PSCmdlet.ShouldProcess('Target')) {
if (-not ($Force -or $PSCmdlet.ShouldContinue('Do you want to continue?', 'Caption'))) {
return # user replied no
}
# Do work
}
}
end { }
}
write-host does not have a -confirm parameter.
You can do it something like this instead:
$caption = "Please Confirm"
$message = "Are you Sure You Want To Proceed:"
[int]$defaultChoice = 0
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Do the job."
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "Do not do the job."
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$choiceRTN = $host.ui.PromptForChoice($caption,$message, $options,$defaultChoice)
if ( $choiceRTN -ne 1 )
{
"Your Choice was Yes"
}
else
{
"Your Choice was NO"
}
For when you want a 1-liner
while( -not ( ($choice= (Read-Host "May I continue?")) -match "^(y|n)$")){ "Y or N ?"}
Write-Warning "This is only a test warning." -WarningAction Inquire
from:
https://serverfault.com/a/1015583/584478
Here's a solution I've used, similiar to Ansgar Wiechers' solution;
$title = "Lorem"
$message = "Ipsum"
$yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "This means Yes"
$no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", "This means No"
$options = [System.Management.Automation.Host.ChoiceDescription[]]($yes, $no)
$result = $host.ui.PromptForChoice($title, $message, $Options, 0)
Switch ($result)
{
0 { "You just said Yes" }
1 { "You just said No" }
}
A slightly prettier function based on Ansgar Wiechers's answer. Whether it's actually more useful is a matter of debate.
function Read-Choice(
[Parameter(Mandatory)][string]$Message,
[Parameter(Mandatory)][string[]]$Choices,
[Parameter(Mandatory)][string]$DefaultChoice,
[Parameter()][string]$Question='Are you sure you want to proceed?'
) {
$defaultIndex = $Choices.IndexOf($DefaultChoice)
if ($defaultIndex -lt 0) {
throw "$DefaultChoice not found in choices"
}
$choiceObj = New-Object Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]
foreach($c in $Choices) {
$choiceObj.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList $c))
}
$decision = $Host.UI.PromptForChoice($Message, $Question, $choiceObj, $defaultIndex)
return $Choices[$decision]
}
Example usage:
PS> $r = Read-Choice 'DANGER!!!!!!' '&apple','&blah','&car' '&blah'
DANGER!!!!!!
Are you sure you want to proceed?
[A] apple [B] blah [C] car [?] Help (default is "B"): c
PS> switch($r) { '&car' { Write-host 'caaaaars!!!!' } '&blah' { Write-Host "It's a blah day" } '&apple' { Write-Host "I'd like to eat some apples!" } }
caaaaars!!!!
This version asks if the user wants to perform an action before continuing with the rest of the script.
DO
{
$confirmation = Read-Host "Do want Action before continue? [Y/N]"
if ($confirmation -eq 'y') {
write-Host "Doing the Action"
}
} While (($confirmation -ne 'y') -and ($confirmation -ne 'n'))
I prefer a popup.
$shell = new-object -comobject "WScript.Shell"
$choice = $shell.popup("Insert question here",0,"Popup window title",4+32)
If $choice equals 6, the answer was Yes
If $choice equals 7, the answer was No