When writing a function:
function Manage-fullAccess
{
Param
([Parameter(ValueFromPipelineByPropertyName=$true,
Position=0,
mandatory=$true)]
$user,
[string] [Parameter(ValueFromPipelineByPropertyName = $true,
Position = 1,
mandatory=$true)]
$mailboxName,
[string] [Parameter(Position = 2)]
$fullAccess
)
# ...
}
I am being prompted to provide values, but variables are not being saved. Probably some super rookie mistake, but I would need a hand here. Both $user and $mailboxName variables are empty
You have two problems here. The first is that you are referencing $user from the main (outer) scope, the variable defined inside the function is out of scope.
The second is that PS does something interesting when a variable is reference, but there is no such variable in scope. It quietly invents such a variable, and leaves it empty.
You are going to have to learn how to transfer values from inside the function to your main scope. That's a little long for an answer here. There is some online help about Scopes. See the link below.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-7.1
Adding return $user, $mailboxName to the end of your function is one way to solve this issue.
function Manage-fullAccess
{
Param
([Parameter(ValueFromPipelineByPropertyName=$true,
Position=0,
mandatory=$true)]
$user,
[string] [Parameter(ValueFromPipelineByPropertyName = $true,
Position = 1,
mandatory=$true)]
$mailboxName,
[string] [Parameter(Position = 2)]
$fullAccess
)
return $user, $mailboxName
}
Related
I am implementing a function in Powershell which will perform REST calls. One of the parameters may differ in contents, depending on given scenarios. For instance, the body of the REST call may be a string or a hash table. How do you implement this within the CmdletBinding() declaration?
For instance
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[Hashtable]$headers
[Parameter(Mandatory=$true)]
[???????]$body # what type here??
)
.
.
.
}
To declare parameters where any type is allowed you can either not type-constrain the parameter at all or use type constraint [object] (System.Object), by doing so, no type conversion will be needed, since all objects in PowerShell inherit from this type.
It's worth mentioning that unconstrained parameters will allow $null as argument, to avoid this, [ValidateNotNull()] and / or [parameter(Mandatory)] can be used.
function Test-Type {
param(
[parameter(ValueFromPipeline, Mandatory)]
[object]$Value
)
process
{
[pscustomobject]#{
Type = $Value.GetType().FullName
IsObject = $Value -is [object]
}
}
}
PS /> 1, 'foo', (Get-Date) | Test-Type
Type IsObject
---- --------
System.Int32 True
System.String True
System.DateTime True
The correct way to tackel this is to create a ParameterSet:
Function doRESTcall(){
[CmdletBinding()]
param (
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 0)]
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 0)]
[Hashtable]$headers,
[Parameter(Mandatory=$true, ParameterSetName = 'StringBody', Position = 1)]
[String]$Stringbody,
[Parameter(Mandatory=$true, ParameterSetName = 'HashBody', Position = 1)]
[Hashtable]$Hashbody
)
Write-Host 'Parameter set:' $PSCmdlet.ParameterSetName
Write-Host 'StringBody:' $StringBody
Write-Host 'HashBody:' $HashBody
}
doRESTcall -?
NAME
doRESTcall
SYNTAX
doRESTcall [-headers] <hashtable> [-Hashbody] <hashtable> [<CommonParameters>]
doRESTcall [-headers] <hashtable> [-Stringbody] <string> [<CommonParameters>]
ALIASES
None
REMARKS
None
doRESTcall #{a = 1} 'Test'
Parameter set: StringBody
StringBody: Test
HashBody:
Note: to accept a larger variety of dictionaries (like [Ordered]), I would use a [System.Collections.Specialized.OrderedDictionary] (rather than [Hashtable]) type for the concerned parameters.
This is a small script I'm calling from an excel spreadsheet:
Param (
[Int] $ID,
[String] $server,
[String] $db,
[String] $uid,
[String] $pw,
[String] $Navn,
[String] $Path
)
Try
{
Add-Type -Path $Path
foreach($i in [BrReader2.BrReader]::Main($ID, $uid, $pw, $db, $server)){if($i.Key -eq $Navn){$Data = $i}}
[BrReader2.BrReader]::WriteToTemporaryTable($Data, $ID, $uid, $pw, $db, $server)
}
Catch
{
$ErrorMessage = $_.Exception.Message
Write-Host $ErrorMessage
Read-Host -Prompt "Press Enter to exit"
}
It works mostly pretty well, except for one little thing. If for some reason one of the parameters is entered in the wrong way, like if it's completely empty, it will fail there and not write out an error message. The console window will simply quickly appear and disappear again and the user might not realise what they've done wrong.
Giving a wrong path is fine, it will still enter the try/catch block and write a proper error message to the console. Giving NO path does not keep the console window open so the user can see what went wrong though.
I need to put the 'Param (...' part at the top of the script though, so I can't put it into the try/catch block. Is there any way I can write out an error message and keep the console window open if one of the parameters fail?
I'm not very experienced with powershell so there might be something obvious I'm missing.
If you want the parameters to be set, you can make them mandatory.
If they should contain a certain value, you can also validate them.
For example:
Param (
[Parameter(Mandatory = $true)]
[ValidateRange(10,99)]
[Int] $ID,
[Parameter(Mandatory = $true)]
[String] $server,
[Parameter(Mandatory = $true)]
[ValidateSet("sql01.contoso.local","sql02.contoso.local")]
[String] $db,
[Parameter(Mandatory = $true)]
[String] $uid,
[Parameter(Mandatory = $true)]
[String] $pw,
[Parameter(Mandatory = $true)]
[String] $Navn,
[Parameter(Mandatory = $true)]
[ValidateScript({Test-Path $_ -PathType 'Container'})]
[String] $Path
)
I have been writing a Powershell library which uses a lot of wrappers around one or two functions. Thanks to a previous question, I discovered that I could use the GetSteppablePipeline() method to do the wrapping. However, I would like to be able to generalize this code. Googling the command, I discovered that there are only one or two samples which have been copied to death, but no single function. So I tried it. It seemed to work at first, and the sample below is roughly what I created first. But when I started to use the code in anger, it just seemed to break all over the place.
If you run the below straight through, it works ok. But if you put in any breakpoints in the ISE, it will break with errors like "Cannot find drive. A drive with the name 'function' does not exist.". If you incorporate the code into any other script, it will also break with seemingly incomprehensible errors (for instance, Where-Object not being recognised as a Cmdlet).
Function CreateSteppablePipeline
{
Param(
[Parameter(Mandatory = $true, Position = 0)]
[string]
$InnerFunctionName,
[Parameter(Mandatory = $true, Position = 1)]
[System.Collections.Generic.Dictionary`2[System.String,System.Object]]
$OuterPSBoundParameters,
[Parameter(Mandatory = $true, Position = 2)]
[System.Management.Automation.EngineIntrinsics]
$OuterExecutionContext,
[Parameter(Mandatory = $true, Position = 3)]
[System.Management.Automation.InvocationInfo]
$OuterMyInvocation
)
$outBuffer = $null
if ($OuterPSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$OuterPSBoundParameters['OutBuffer'] = 1
}
[System.Management.Automation.FunctionInfo] $wrappedCmd = $OuterExecutionContext.InvokeCommand.GetCommand($InnerFunctionName, [System.Management.Automation.CommandTypes]::Function);
[ScriptBlock] $scriptCmd = { & $wrappedCmd #OuterPSBoundParameters };
[System.Management.Automation.SteppablePipeline] $steppablePipeline = $scriptCmd.GetSteppablePipeline($OuterMyInvocation.CommandOrigin);
$steppablePipeline.Begin($OuterMyInvocation.ExpectingInput);
$steppablePipeline;
}
Function Inner
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[int] $i = 0;
}
Process {
$i++;
ForEach ($myValue In $Value)
{
$myValue;
}
}
End {
$i;
$ExParam;
}
}
Function Outer
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true, Position=0)]
[string] $ExParam,
[Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true)]
[string[]] $Value
)
Begin {
[System.Management.Automation.SteppablePipeline] $steppablePipeline = CreateSteppablePipeline "Inner" $PSBoundParameters $ExecutionContext $MyInvocation;
}
Process {
$steppablePipeline.Process($_) |
ForEach-Object {
$_ + " : Pipelined";
}
}
End {
$steppablePipeline.End()
}
}
Outer -ExParam 22 "Name1","Name2","Name3","Name4","Name5","Name6","Name7"
"Name1","Name2","Name3","Name4","Name5","Name6","Name7" | Outer -ExParam 22
It has probably to do with scoping (a problem I have come across a number of times in scripts). I tried moving parts of the CreateSteppablePipeline() function between the calling and function scope, but with no luck.
PARAM (
[parameter(Mandatory=$true)]
[string]$Poolname,
[array]$Ports = 443,
[parameter(Mandatory=$true)]
[ValidateSet("ELB","ALB")]
$Loadbalncertype,
[parameter(Mandatory=$true)]
[ValidateSet("Ping","HTTPGet")]
$HealthCheckConfigType,
[parameter(Mandatory=$true)]
[array]$LBSubnets,
[parameter(Mandatory=$true)]
[string]$SecGroupID,
[int]$IdleTimeoutsec = 60,
[bool]$SSLPassthrough = $false,
string]$SSLCertificateName,
[string]$HealthCheckPath,
[string]$SSLPolicyName,
[bool]$ConfigureProxyProtocol = $true
)
In the above I would like to use Parameter $HealthCheckConfigType only if the $Loadbalncertype = ELB. I am not sure how to create this logic in Powershell function parameter section.
To do that in the param definition specifically, you could use DynamicParam to create a dynamic parameter, but that's a lot of work and probably overkill.
The most straightforward method I can think of if you must leave $LoadBalancerType as a [string] is to use [ValidateScript()] like so:
param(
[ValidateSet("ELB","ALB")]
$LoadBalancerType ,
[ValidateScript( { $LoadBalancerType -eq 'ELB' } )]
[ValidateSet("Ping","HTTPGet")]
$HealthCheckConfigType
)
This will give a crappy error message, which you could override with a well-placed throw:
[ValidateScript( { $LoadBalancerType -eq 'ELB' -or $(throw 'A better error message') } )]
Another option is to change your $LoadBalancerType parameter into separate switch parameters, and use them to define parameter sets:
[CmdletBinding(DefaultParameterSet='ALB')]
param(
[parameter(Mandatory=$true)]
[string]$Poolname,
[Parameter(Mandatory, ParameterSetName = 'ALB')]
[Switch]$ALB ,
[Parameter(Mandatory, ParameterSetName = 'ELB')]
[Switch]$ELB ,
[Parameter(Mandatory, ParameterSetName = 'ELB')
[ValidateSet("Ping","HTTPGet")]
$HealthCheckConfigType
)
This lets the parameter parser enforce this restriction, and you can see it in the automatically generated parameter sets by calling Get-Help on your function.
And, even though this isn't the usual way, in your case if you name the parameter sets with the names of the values you wanted, you could recreate $LoadBalancerType without conditionals:
$LoadBalancerType = $PSCmdlet.ParameterSetName
(assuming of course, that the only possible parameter sets are load balancer names directly; be careful with this)
But if you never really needed that string value; i.e. if you were only ever going to do:
if ($LoadBalancerType -eq 'ALB') {
} elseif ($LoadBalancerType -eq 'ELB') {
}
or something like that, then you don't need to recreate it, just do:
if ($ALB) {
} elseif ($ELB) {
}
Alternatively, you don't have to do this check in the param block at all; you can do it in your function body, or begin/process blocks where appropriate.
Use a DynamicParam block: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-6#dynamic-parameters
I don't have a ton of experience with them myself, but here is the code sample from that MS doc. You can process and add any parameters you need:
function Get-Sample {
[CmdletBinding()]
Param ([String]$Name, [String]$Path)
DynamicParam
{
if ($path -match ".HKLM.:")
{
$attributes = New-Object -Type `
System.Management.Automation.ParameterAttribute
$attributes.ParameterSetName = "__AllParameterSets"
$attributes.Mandatory = $false
$attributeCollection = New-Object `
-Type System.Collections.ObjectModel.Collection[System.Attribute]
$attributeCollection.Add($attributes)
$dynParam1 = New-Object -Type `
System.Management.Automation.RuntimeDefinedParameter("dp1", [Int32],
$attributeCollection)
$paramDictionary = New-Object `
-Type System.Management.Automation.RuntimeDefinedParameterDictionary
$paramDictionary.Add("dp1", $dynParam1)
return $paramDictionary
}
}
}
Another option would be to not muck with DynamicParam and in your code body, simply ignore $HealthCheckConfigType if $LoadBalancerType does not equal ELB, but if you want to use parameter validators to check this, DynamicParam is the answer.
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key,
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value
)
}
I'm calling it like so, assume I've added values to the dictionary (removed for brevity here)
$dict = New-Object 'System.Collections.Generic.Dictionary[string,string]'
$dict.GetEnumerator() | Format-File -Path $updatePath
Here's my conundrum.
The above works perfectly. However, the following does not, note the difference in the key/value parameter
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string] $path=$(throw "path is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $key=$(throw "key is mandatory ($($MyInvocation.MyCommand))"),
[Parameter(Mandatory = $true, Position = 2, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[string] $value=$(throw "value is mandatory ($($MyInvocation.MyCommand))")
)
}
The above throws an exception. It appears to be getting the default value when the function is first called, but when processing, the key/value parameters are set properly.
It makes a bit of sense as to why the key/value wouldn't be set at the time of the function call, but this also means my mental model is off.
So my question is two-fold.
What is the parameter binding process for functions of this nature, and
How does one verify the input of values that have come in from the pipeline? Manually check in the begin block, or is there another method?
If you have links to describe this all in greater detail, I'm happy to read up on it. It just made me realize my mental model of the process is flawed and I'm hoping to fix that.
What is the parameter binding process for functions of this nature?
In the Begin block, pipeline bound parameters will be $null or use their default value if there is one. This makes some sense, considering that the pipelining of values hasn't started yet.
In the Process block, the parameter will be the current item in the pipeline.
In the End block, the parameter will be the last value from the Process block, unless there was an exception in validating the parameter, in which case it will use the default (or $null).
How does one verify the input of values that have come in from the pipeline?
You can't check in the Begin block.
The best way is to use [Validate attributes, as you have with [ValidateNotNullOrEmpty()].
Your examples with using throw as a default value are useful in some situations but they are a clever workaround. The thing is, you don't need them since you already declared the parameter as Mandatory.
Instead of using a default value, you can use [ValidateScript( { $value -eq 'MyString' } )] for example.
Since the error message from [ValidateScript()] sucks, you can combine the techniques:
function Format-File {
param(
[Parameter(Mandatory = $true, Position = 1, ValueFromPipelineByPropertyName = $true)]
[ValidateNotNullOrEmpty()]
[ValidateScript( {
($_.Length -le 10) -or $(throw "My custom exception message")
} )]
[string] $key
)
}
Using [ValidateScript()] works whether it's a pipeline parameter or not.