Powershell: order of the Cmdlet Binding Attributes - powershell

a simple Function:
function Just-Test
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[ValidateSet('yes','no')]
[string]$inputen,
[Parameter(Mandatory=$true)]
[ValidateScript(
{
if(!(Test-Connection -ComputerName $_ -count 1 -quiet))
{
throw "no conn to server!"
}
else
{
$true
}
})]
[ValidatePattern('^19')]
[string]$comp
)
}
Then I call the function:
Just-Test -inputen 'yes' -comp '172.168.0.1'
First: there is no computer '172.168.0.1', second: the pattern is wrong, so I get the error:
Just-Test : Cannot validate argument on parameter 'comp'. The argument "172.168.0.1" does not match the "^19" pattern. Supply an argument that matches "^19" and try the command again.
Then I change the code and put [ValidationPattern] directly after ...Mandatory..:
function Just-Test
{
[CmdletBinding()]
Param(
[Parameter(Mandatory=$true)]
[ValidateSet('yes','no')]
[string]$inputen,
[Parameter(Mandatory=$true)]
[ValidatePattern('^19')]
[ValidateScript(
{
if(!(Test-Connection -ComputerName $_ -count 1 -quiet))
{
throw "no conn to server!"
}
else
{
$true
}
})]
[string]$comp
)
}
Of course I get an error:
Just-Test : Cannot validate argument on parameter 'comp'. no conn to server!
but the wrong pattern of the comp-name ('172...') would be totally ignored??
Why?

I believe it would just use the last Validate attribute it sees, which would be why changing the order they are defined in would change the behavior. Really there's no reason you can't put both checks in the ValidateScript code if you are using that anyway.

Related

Powershell to start/stop with arg

I've written the following function in test.ps1 and I would like to make a choise when running thsi script to start/stop/.. :
function getState($SeviceName) {
$server = #('host_1', 'host_2')
# get status
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName
}
I would like to provide $ServiceName as argument (with stdin) how can I do it? => somthing like choose 1 to start 2 to stop ...
To use switch/case in Powershell
$doAction = {"Stop-Service", "Start-service"}
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName | $doAction}
How do I use the switch to select start or stop?
Here's a function that will do what you're asking for:
function Get-State {
[CmdletBinding()]
[OutputType('System.ServiceProcess.ServiceController')]
param(
[Parameter(Position = 0, Mandatory)]
[ValidateSet('Start', 'Stop', 'Get')]
[string] $Action,
[Parameter(Position = 1, ValueFromPipeline, Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ServiceName
)
begin {
$serverList = #('host_1', 'host_2')
}
process {
foreach ($server in $serverList) {
try {
$svc = Get-Service -ComputerName $server -Name $ServiceName -ErrorAction Stop
} catch {
throw "Failed to find service $ServiceName on $server! $PSItem"
}
switch ($Action) {
'Start' { $svc | Start-Service -PassThru }
'Stop' { $svc | Stop-Service -Force -PassThru }
default { $svc }
}
}
}
}
It utilizes advanced function features and attributes to take pipeline input (stdin in your words). I'd suggest reading this documentation.
You can add argument to a script by adding parameters to it.
On the top of your script file put:
Param
(
[parameter()]
[String[]]$YourArgumentVariable
[parameter()]
[switch] $MySwitch
)
With a function it goes right after the function definition. So in your case:
function getState($SeviceName) {
Param
(
[parameter()]
[String[]]$server
[parameter()]
[switch] $MySwitch
)
# get status
$server | % {Write-Host "verify: $_"; Get-Service -ComputerName $_ -Name SeviceName
}
A switch basically sets a boolean to true or false.
So in this if you call the script with -MySwitch it will set the variable $MySwitch to true. Else it will remain false.
Don Jones has written a good getting started article on paramters that I would recommend you checking out.
Do note that there are loads of things you can define in the paramter. Like if you want to make sure it is always filled you can set
[parameter(Mandatory=$true)]
This is just one of many examples of what you can do with paramters.

PowerShell ValidateSet on Boolean Parameter

I am attempting to use ValidateSet with a boolean parameter however I cannot get it to function as expect.
An example to replicate the issue:
function Set-Boolean
{
[CmdletBinding()]
[OutputType([Bool])]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[ValidateSet($false,$true)]
[Bool] $Bool
)
Process
{
$Bool
}
}
At run time PowerShell expands the $true and $false variables upon tab completion to True and False respectively however this then cause the parameter validation to fail as only $true, $false, 1 and 0 are valid boolean types.
I tried changing ValidateSet to [ValidateSet('$false','$true')] and the tab completion works as expected however the parameter validation still fails as the parameter is expecting the strings '$false' and '$true'.
I could change ValidateSet to [ValidateSet([bool]0,[bool]1)] and have it function as I expect but I find 0 and 1 a poorer user experience over $true and $false for completion of a boolean parameter.
The expected output of the function should be True when $true is chosen and False when $false is chosen.
I have implemented a workaround in my code but I want to know how I can use ValidateSet with a boolean parameter, if it is possible, to enable the use of tab completion for the user.
Joining some answers, the best option will depend on the needs, if the parameter is type boolean it can be replaced by one of type switch, but if you want to specify the value to see in the code or by style; It can be done as follows.
function Write-SwitchParam {
[CmdletBinding()]
param (
[Switch] #[Switch] or [System.Management.Automation.SwitchParameter]
$SwitchParam
)
if($SwitchParam)
{
Write-Host "Switch is present. Do Positive Action."
}
else {
Write-Host "Switch isn't present. Do Negative Action."
}
}
function Write-BooleanParam {
[CmdletBinding()]
Param(
[parameter(Position=0, ValueFromPipeline=$true)]
[ValidateNotNullOrEmpty()]
[ValidateSet($true, $false, 0, 1)]
$BoolParam = $true
)
Begin{
}
Process{
$value = [System.Convert]::ToBoolean($BoolParam)
Write-Host $value
}
End{
}
}
Write-Host "TESTS BOOLEAN PARAM" -ForegroundColor Magenta
Write-BooleanParam -BoolParam $true
Write-BooleanParam -BoolParam $false
Write-Host
Write-BooleanParam $false
Write-BooleanParam $true
Write-Host
Write-BooleanParam True
Write-BooleanParam False
Write-Host
Write-BooleanParam 0
Write-BooleanParam 1
Write-Host
Write-BooleanParam
Write-Host
$true, $false, "True", "False", 1, 0 | Write-BooleanParam
Write-Host
Write-Host "TESTS SWITCH PARAM" -ForegroundColor Magenta
Write-SwitchParam
Write-SwitchParam -SwitchParam
You can use ValidateSet for autocompletion without specifying the input variable type. Though, you cannot use 0 and 1 as input in this case:
function Set-Boolean
{
[CmdletBinding()]
[OutputType([Bool])]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[ValidateSet($false,$true)]
$Bool
)
Process
{
[System.Convert]::ToBoolean($Bool)
}
}
This gives the tab completion for the user, as desired using ValidateSet, but is achieved by registering an argument completer instead (especially in the ISE);
function Set-Boolean
{
[CmdletBinding()]
[OutputType([Bool])]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[ValidateNotNullOrEmpty()]
[Bool] $Bool
)
Process
{
$Bool
}
}
Register-ArgumentCompleter -CommandName 'Set-Boolean' -ParameterName 'Bool' -ScriptBlock {
[System.Management.Automation.CompletionResult]::new(
'$False',
'$False',
'ParameterValue',
'[bool] False'
)
[System.Management.Automation.CompletionResult]::new(
'$True',
'$True',
'ParameterValue',
'[bool] True'
)
}

Is it possible to send 'out of band data' from a remote Invoke-Command call?

I have a system in which I'm remoting into a single machine at a time and running commands, scripts, etc. It would be useful to be able to effectively return log messages from the remote script in "realtime". Some code to get an idea of what I'm trying to do.
Note that both the local Log-*Msg functions log to a database (and tee to standard out/err as appropriate). Also note that we have analogous Log-*Msg methods on the remote side (loaded from a module) that are meant to pitched back across the wire and recorded in the DB as if the local Log-*Msg function was called.
Local Methods
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
if($_ -isnot [string]) { return $_ }
if($_.StartsWith('Log-VerboseMsg:')) {
Log-VerboseMsg $_.Replace("Log-VerboseMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-WarningMsg:')) {
Log-WarningMsg $_.Replace("Log-WarningMsg:", "") | Out-Null
return
}
if($_.StartsWith('Log-UserMsg:')) {
Log-UserMsg $_.Replace("Log-UserMsg:", "") | Out-Null
return
}
else { return $_ }
}
Example Remote Method
On the remote side I have a module that gets loaded with a few logging functions, here's one such function:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
"Log-VerboseMsg:$msg"
}
For the most part it works, I can do the following
$val = Exec-Remote -Session $PSSession {
Log-VerboseMsg 'A test log message!'
return $true
}
And have it do the right thing transparently.
However, it fails in the following scenario.
$val = Exec-Remote -Session $PSSession {
function Test-Logging {
Log-VerboseMsg 'A test log message!'
return $true
}
$aVariable = Test-Logging
Do-ALongRunningOperation
return $aVariable
}
The above will not return anything until the 'long running operation' completes.
My question to you is the following.
Is there a way for me to reliably do this in Powershell? In some form, if the approach I'm using is really that terrible, feel free to lambast me and explain why.
NOTE: connecting to the DB from the remote environment and recording the log messages will not always be possible, so while that approach could work, for my specific needs it isn't sufficient.
In PowerShell v5 you can use new information stream for this. You should modify local functions as following:
function Exec-Remote {
param(
[ValidateNotNull()]
[System.Management.Automation.Runspaces.PSSession]
$Session=$(throw "Session is mandatory ($($MyInvocation.MyCommand))"),
$argumentList,
$scriptBlock
)
if($argumentList -is [scriptblock]) {$scriptBlock = $argumentList}
if($scriptBlock -eq $null) { throw 'Scriptblock is required'}
# 6>&1 will redirect information stream to output, so Filter-RemoteLogs can process it.
Invoke-Command -Session $Session -ArgumentList $argumentList -scriptBlock $scriptBlock 6>&1 | Filter-RemoteLogs
}
Filter Filter-RemoteLogs {
# Function should be advanced, so we can call $PSCmdlet.WriteInformation.
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[PSObject]$InputObject
)
if(
# If it is InformationRecord.
($InputObject -is [Management.Automation.InformationRecord]) -and
# And if it come from informational steam.
($WriteInformationStream=$InputObject.PSObject.Properties['WriteInformationStream']) -and
($WriteInformationStream.Value)
) {
# If it is our InformationRecord.
if($InputObject.Tags-contains'MyLoggingInfomation') {
# Write it to log.
&"Log-$($InputObject.MessageData.LogType)Msg" $InputObject.MessageData.Message | Out-Null
} else {
# Return not our InformationRecord to informational stream.
$PSCmdlet.WriteInformation($InputObject)
}
} else {
# Return other objects to output stream.
$PSCmdlet.WriteObject($InputObject)
}
}
And remote logging functions should write to information stream:
function Log-VerboseMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Verbose'}) MyLoggingInfomation
}
function Log-WarningMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='Warning'}) MyLoggingInfomation
}
function Log-UserMsg {
param([ValidateNotNullOrEmpty()] $msg)
Write-Information ([PSCustomObject]#{Message=$msg;LogType='User'}) MyLoggingInfomation
}

Why does passing $null to a parameter with AllowNull() result in an error?

Consider the following code:
function Test
{
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[AllowNull()]
[String]
$ComputerName
)
process{}
}
Test -ComputerName $null
Based on the official documentation for AllowNull I was expecting that $ComputerName could either be [string] or $null. However, running the above code results in the following error:
[14,24: Test] Cannot bind argument to parameter 'ComputerName' because it is an empty
string.
Why doesn't passing $null for $ComputerName work in this case?
$null, when converted to [string], return empty string not $null:
[string]$null -eq $null # False
[string]$null -eq [string]::Empty # True
If you want to pass $null for [string] parameter you should use [NullString]::Value:
[string][NullString]::Value -eq $null # True
Test -ComputerName ([NullString]::Value)
You also need to add the [AllowEmptyString()] attribute if you plan on allowing nulls and empty strings.
function Test
{
[CmdletBinding()]
param
(
[parameter(Mandatory=$true)]
[AllowNull()]
[AllowEmptyString()]
[String]
$ComputerName
)
process{}
}
Test -ComputerName $null
As noted, when cast to a [string], $null becomes an empty string (""). To avoid this, one option is to type the param as an object - then in the body test first for $null, then check that $MyParam.GetType().Name is "String".

Identify which parameter is passed when running a script

I have a simple code that accepts two parameters. The parameters are optional. Below is the code.
[CmdletBinding()]
Param(
[Parameter(Mandatory=$False)]
[string]$pA,
[Parameter(Mandatory=$False)]
[string]$pB
)
when running the script I want to know which parameter is passed. pA or pB.
$MyInvocation.BoundParameters
return a ps custom dictionary pair (key/value) with all passed parameter.
this is the content of a.ps1 file:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$False)]
[string]$pA,
[Parameter(Mandatory=$False)]
[string]$pB
)
$MyInvocation.BoundParameters
running this script gives:
PS C:\ps> a -pA pAparam
Key Value
--- -----
pA pAparam
then you can check what key is present:
[bool]($MyInvocation.BoundParameters.Keys -match 'pa') # or -match 'pb' belong your needs
As you cas $Pa and $Pb you can test if they are empty:
You can test with this function :
function func
{
[CmdletBinding()]
Param([Parameter(Mandatory=$False)]
[string]$pA,
[Parameter(Mandatory=$False)]
[string]$pB
)
if ($pA -eq [string]::Empty -and $pA -eq [string]::Empty)
{
Write-Host "Both are empty"
}
elseif ($pA -ne [string]::Empty)
{
Write-Host "Pa is not empty"
}
elseif ($pB -ne [string]::Empty)
{
Write-Host "Pb is not empty"
}
}
Clear-Host
func
Remains the problem that func -Pa "" will give same results as func But if you just want to test the presence of a parameter you can use the switch attribute.
You can find more about PowerShell scripts and function parameters with these links:
about_Functions
about_Functions_Advanced
about_Functions_Advanced_Parameters