Powershell parameter handling - powershell

I have a powershell script that looks something like this.
Function Write-DeploymentConf {
param
(
[Parameter(Mandatory)]
[ValidateLength(4, 15)]
[String]
$CustomerName,
[Parameter(Mandatory)]
[ValidateLength(4, 15)]
[String]
$FacilityName = $CustomerName,
[Parameter(Mandatory)]
[ValidateLength(5, 1024)]
[String]
$LocationAddress,
[Parameter(Mandatory)]
[ValidateSet("onpremise", "cloud")]
[String]
$DeploymentMode = "onpremise"
)
Process {
$( Write-Host "CustomerName: $CustomerName")
$( Write-Host "FacilityName: $FacilityName")
$( Write-Host "LocationAddress: $LocationAddress")
$( Write-Host "DeploymentMode: $DeploymentMode")
}
}
Write-DeploymentConf
It's close to what I want, but there are a few things wrong with it.
It silently accepts inputs that will fail validation, then holds them until the user has entered values for every parameter, then dies with an error message. I don't want to die, I just want to re-prompt when validation fails. And, ideally, I want to re-prompt right away, not after everything has been input. I only want to re-prompt for ones that failed validation.
I want to tell the user what the validation rules are before they mess it up. If the only allowable values are "onpremise" or "cloud", then the user should be told that. Ideally the user should just type "1, enter" for "onpremise", "2, enter" for "cloud".
Some parameters have a reasonable default that I want to prompt the user to accept. For example, if $CustomerName is "abcd", then "abcd" is a reasonable default value for $FacilityName. When prompting $FacilityName, I want to allow the user to change it, or press "enter" to accept "abcd" as the default. Not only do I want to accept that as a default, but I want to show that the 'abcd' will be the default value on the prompt line. Likewise, "onpremise" is a reasonable default for $DeploymentMode, but I want them to affirmatively accept that default.
Some sets of parameters are only conditionally mandatory. E.g., if this is a cloud deploy, then a bunch of credentials become mandatory to supply. Can I keep the nice "accept from command line but fall back to prompting" behavior with runtime conditionals?

Related

Pipeline input not being validated when a function emits no output down the pipeline for ValueFromPipelineByPropertyName parameters

I was able to reproduce this in a more generic way, and the issue is different than originally presented. I have rewritten this question to reflect the issue experienced along with a generic reproducible example.
I have a cmdlet that sometimes produces no output when it doesn't find any data to return. However, I use this function to pass information to another cmdlet which accepts pipeline input via way of the ValueFromPipelineByPropertyName attribute. When there is an actual object being passed down the pipeline, everything works as expected, including parameter validation checks. However, if the passed object is $null, then parameter validation gets skipped. Note that this is not reproduceable when simply passing $null down the pipeline; I've only been able to reproduce this when emitting no output down the pipeline.
I've been able to reproduce this generically. The parameters are defined with the same attributes as my real code:
Function Get-InfoTest {
Param(
[switch]$ReturnNothing
)
if( !$ReturnNothing ) {
[PSCustomObject]#{
Name = 'Bender'
Age = [int]::MaxValue
}
}
}
Function Invoke-InfoTest {
Param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]$Age
)
Write-Host "Hello, $Name. I see you are $Age years old."
}
# With valid object
Get-InfoTest | Invoke-InfoTest
# Correct behavior when $null is directly passed into the cmdlet, throws error
$null | Invoke-InfoTest
# With returned null object, should throw an error but executes with an incorrect result
Get-InfoTest -ReturnNothing | Invoke-InfoTest
What is going on here? While it's not difficult to write null-or-whitespace checks in the function body, this is the point of the Mandatory parameter option as well as the Validate* parameter attributes. In my real code, I now need to write null-or-whitespace checks for several parameters which already have validation attributes set. As stated in the code comments, passing $null into the target cmdlet results in the correct error being thrown, but no output produced from a function results in the function executing as if everything was provided correctly.
If you don't define begin/process/end blocks, functions bodies default to an end block. However, putting the function body in an explicit process block results in the correct behavior:
The following modification to Invoke-InfoTest results in the sample code working correctly for all cases:
Function Invoke-InfoTest {
Param(
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(Mandatory, ValueFromPipelineByPropertyName)]
[int]$Age
)
# Note that I've wrapped this in a process block
process {
Write-Host "Hello, $Name. I see you are $Age years old."
}
}
This works because as stated above, functions default to an end block if unspecified. However, the end and begin blocks are executed regardless of the pipeline object being input. process only gets executed when there is data passed in. Defining the code using the pipeline variables inside of a process block effectively stops the code using the missing data from being executed, which seems to be by design.
Thanks to #SantiagoSquarzon in the comments for helping me realize the actual problem.

Joining strings and # character missing

I'm assuming its obvious, but I'm completely missing it. I've got a PowerShell script I'm working on to automate user creation. One of my functions generates the email address. Right now, its not doing anything fancy, just joining the username and domain. I'll be adding more later to increment the value if in use. But I can't get the basic version to work.
function generateMailbox {
param(
[parameter(Mandatory=$true)]
$userName,
[parameter(Mandatory=$true)]
$domain
)
$uniqueMailbox = $null
#set initial value for $uniqueMailbox as the initial mailbox name
$uniqueMailbox = "$userName#$domain"
return $uniqueMailbox
}
So, pretty straightforward. BUT... the "#" is missing from the emails it generates. I put in a break point on the return and verified $uniquemailbox is missing that "#". So if $userName = "uname" and $domain = "domain.com", then $uniqueMailbox will be set to "unamedomain.com"
While in the debug mode, I can manually enter $uniqueMailbox = "$userName#$domain" and then the value returns correctly. For the life of me I can't see my mistake.
I did double check the $uniqueMailbox variable isn't used anywhere outside this function.

Trying to use parameters dynamically using powershell

I am trying to setup dynamic parameters that vary depending on if you are adding or modifying/removing a drone. Ex: If you are adding a drone you would need its IP/Name/Location.. To remove the drone you would only need its name. I have tried looking online and try various examples I've seen but I am completely stuck here. Any help to steer me in the right direction would be appreciated. I am somewhat new to powershell. Here's what I have.
[CmdletBinding(SupportsShouldProcess=$True)]
Param( [Parameter(Mandatory=$true,
HelpMessage = "Add remove or Modify a drone?")]
[ValidateSet("Add", "Remove", "Modify")]
[String]$Action)
DynamicParam{
if ($action = "Add"){
Param( [Parameter(Mandatory)]
[ValidateSet("NorthAmerica", "SouthAmerica", "NorthernEurope","UK", "CEE", "FRMALU", "SouthernEurope", "AsiaPacific")]
[String]$curRegion,
[Parameter(Mandatory)]
[IPAddress]$ip,
[Parameter(Mandatory)]
[String]$droneName)
}
if ($action = "Remove"){
Param(
[Parameter(Mandatory)]
[string]$droneRemoveName)
}
}
Consider driving your parameter constraints with named Parameter Sets instead. I'm suggesting this because dynamic parameters don't work quite like you think they do, but named parameter sets are an easier way to solve your problem. In case you're interested, here's a blog post explaining how to use dynamic parameters and it winds up being pretty manual parameter handling.
You can add a parameter to more than one parameter set depending on the contexts in which each parameter is required. Instead of using -Action ACTION as a driver for a dynamic parameter, use a [switch] instead, such as -Add and -Remove, and have each switch part of its own parameter set. For example, when defining your parameters, it may look something like this:
Param(
[Parameter(ParameterSetName='Remove')]
[switch]$Remove,
[Parameter(ParameterSetName='Add')]
[switch]$Add,
[Parameter(ParameterSetName='Remove', Mandatory)]
[Parameter(ParameterSetName='Add', Mandatory)]
[string]$IPAddress
)
In this example, -IPAddress is valid when you use the -Add or -Remove switch, but won't be relavant outside of this context. Of course, if a parameter should only be valid for a certain parameter set, don't define it under more than one parameter set name.
If you want to make sure at least one "action" switch is defined before executing, you can check that one of those parameters was used when invoking the cmdlet by checking $PSBoundParameters:
('Add' -in $PSBoundParameters.Keys) -Or ('Remove' -in $PSBoundParameters.Keys)

Is there a way to make a Powershell function ignore a default parameter's value if its parameter set is not in use?

I understand from this answer that when you have default parameters and multiple parameter sets in a Powershell function, the default parameter values will be instantiated even if the parameter set in use is not the one in which they are inserted.
Is there a way to avoid this?
For example, in the function below, assuming that there is a really expensive calculation used to compute the default value of $FirstParameter, I would like to avoid using it when it is not necessary:
function PrintStuff {
[CmdletBinding(DefaultParameterSetName='FirstSet')]
Param(
[Parameter(ParameterSetName='FirstSet')]
[String]$FirstParameter=(ReallyExpensiveFunction),
[Parameter(ParameterSetName='SecondSet')]
[String]$SecondParameter
)
if (-not ($FirstParameter -eq $null)) {Write-Host $FirstParameter}
Write-Host "$($PSCmdlet.ParameterSetName)"
}
function ReallyExpensiveFunction {
# Very expensive calculation
"I Am First"
}
However, at the moment running it would still give me the results below:
PS C:\> PrintStuff
# I Am First
# FirstSet
PS C:\> PrintStuff -SecondParameter "don't print this"
# I Am First
# SecondSet
As per above, when SecondSet is used $FirstParameter is still being defined. Is there a way to get only SecondSet printed when the second parameter set is used?
Bear in mind, I am looking to find out if there is a solution which would allow me to keep ReallyExpensiveFunction as the default value for $FirstParameter, and avoid solutions which would involve transferring the logic to the body of the function, such as:
...
Param(
[Parameter(ParameterSetName='FirstSet')]
[String]$FirstParameter,
...
)
if ($PSCmdlet.ParameterSetName -eq 'FirstSet' -and ($FirstParameter -eq '')) {
$FirstParameter = ReallyExpensiveFunction
}
...
Sorry if the pitch is too specific, but I am curious to find out if this is possible.
Unfortunately, the answer is no. ParameterSet allows to present a simpler interface to user for complex argument sets by filtering out the non-relevant ones. However, PowerShell goes through each parameter, whether it is in the selected parameterset or not and assign the default value to the parameter, if you specify one. So, simply put in the context of your question, ParameterSet may be thought as just a filter for presentation.

Send email alert from Performance Monitor using PowerShell script

I have created an alert in Performance Monitor (Windows Server 2008 R2) that should be triggered whenever \Processor(_Total)\% Processor Time is Above 10 (a small value just to guarantee that the condition for sending the alert is always met). You can see the Alert Task properties in the image.
In addition, I have also created a new task in the Task Scheduler that will run whether the user is logged on or not, and it will run with highest privileges. The trigger for this task has the following properties:
Begin the task: On an event
Settings: Basic
Log: System
Source: Processor
The Actions (and this is the part I don't know if it's correct) has the following settings:
Action: Start a program
Program/script: the path to a PowerShell script to send an email.
The PowerShell code is the following ($name, $date, $counter, $threshold, $value are supposed to come from the Performance Monitor data collector set alert task properties, as in the image above):
function SendMail ($name, $date, $counter, $threshold, $value) {
$MailMessage = New-Object Net.Mail.MailMessage
$MailMessage.To.Add("myemail#blah.bleh")
$MailMessage.From = "do-not-reply#blah.bleh"
$MailMessage.Subject = "ALERT - Performance Monitor"
$MailMessage.IsBodyHtml = $True
$MailMessage.Body = #"
<html><head></head><body>
The following counter needs attention:<BR><BR>
Name: $($name)<BR>
Date: $($date)<BR>
Counter: $($counter)<BR>
Threshold: $($threshold)<BR>
Actual Value: $($value)<BR>
<FONT face=Courier>$($html)</FONT>
<BR>
--- Automatically generated with SENDMAIL function ---
</body>
</html>
"#
$SmtpClient = New-Object Net.Mail.SmtpClient("blah.bleh")
$SmtpClient.Send($MailMessage)
}
Once the task is started, I have the following in the History: Task Started, Action Started, and Created ask Process. The email is never sent though.
I tried sending an email using the Action: Send an email, and it worked fine. Does anyone know what could be wrong?
There are basically two things to address that should make this work for you.
Get the alert parameters correctly passed to your script.
Actually call the function defined in your script.
We'll start with the parameters. On the Alert Task tab of your alert (pictured above), edit the Task Arguments field and replace:
{name}{date}{counter}{threshold}{value}
with:
"{name}" "{date}" "{counter}" "{threshold}" "{value}"
Your parameters are basically being parsed as a space-separated string value, so we add double-quotes around each individual parameter token to handle values that include spaces, and we add a space between each individual parameter token so that we'll be able to tell one parameter from the next.
Then, for the action of your scheduled task (named "Processor Monitoring") you have to tell it to expect parameters from the alert and to pass those parameters to the PowerShell script.
Your Action is correct, i.e. "Start a program".
For the Program/script field, enter "powershell.exe" (or browse for the full path).
And for the Add Arguments field, enter this:
-File C:\path\to\your\PowerShell\scripts\perfmon_send_email.ps1 $(Arg0)
Where perfmon_send_email.ps1 is the script file containing your SendMail() function as described above.
This bit was kind of finicky, so there may be other ways to set this up, but explicitly using the -File parameter made a difference for my tests. The $(Arg0) part is what gets replaced with the parameter string from the Alert when the scheduled task executes PowerShell to run your script.
So that should make the Alert parameters available to your PowerShell script. Now all you have to do is actually call the function you've already defined. Add the following to the end of your script (after the function definition):
# Get parameter values by position and pass them to the SendMail() function.
SendMail $args[0] $args[1] $args[2] $args[3] $args[4]
$args is an array containing the parameter values passed to a script file called from the command line, which is exactly what we configured the scheduled task to do.
Since we know the alert will always send the same values in the same order, i.e. name, date, counter, threshold, and value, we can just pull them from the command line arguments based on position and pass them to the SendMail() function.
Note that there are more robust ways to process command line arguments, but this should be sufficient for your purposes.