Is it possible to have [Nullable[bool]] in a bool[] in PowerShell - powershell

Is it possible to have [Nullable[bool]] in a bool[] in PowerShell? I tried different solution, approaches but fail to get proper $null, $true, $false for a parameter? Also it seems that [cmdletbinding] changes how things works as well.
enum BoolNull {
null = $null
true = $true
false = $false
}
function Test-Array0 {
param (
[bool[]] $thisValue
)
if ($thisValue -eq $null) {
Write-Output 'Null found'
}
}
function Test-Array1 {
param (
[bool[]] $thisValue
)
foreach ($value in $thisValue) {
if ($value -eq $null) {
Write-Output 'Null found'
}
}
}
function Test-Array2 {
[CmdletBinding()]
param (
[bool[]] $thisValue
)
if ($thisValue -eq $null) {
Write-Output 'Null found'
}
}
function Test-Array {
[CmdletBinding()]
param (
[AllowEmptyCollection()] [AllowNull()][ValidateSet($null, $true, $false)] $thisValue
)
if ($thisValue -eq $null) {
Write-Output 'Null found'
}
}
# this works
function Test-Test {
[CmdletBinding()]
param (
[nullable[bool]] $thisValue
)
if ($thisValue -eq $null) {
Write-Output 'Null found'
}
}
function Test-Array5 {
param (
[boolnull[]] $thisValue
)
foreach ($value in $thisValue) {
if ($value -eq 'null') {
Write-Output 'Null found'
}
}
}
Test-Array0 -thisValue $null #this works
Test-Array -thisValue $null # this doesn't work
Test-Array -thisValue $null, $null, $true # this doesn't work
Test-Array1 -thisValue $null
Test-Array2 -thisValue $null # this
Test-Test -thisValue $null # this works
Test-Array5 -thisValue null, true, null # this works but is completely useless

This is a limitation of the bool type. When you strictly-type the parameter, it can only take $true, $false, 0, and 1. In order to achieve what you want, you can use a [ValidateSet] attribute:
[CmdletBinding()]
param(
[Parameter(Position = 0, Mandatory)]
[ValidateSet($null, $true, $false)]
[object] $ThisValue
)
As a side-note, there used to be a bug with powershell (might still be present) where comparing $null on the right side will cause nothing to be returned, causing logic to fall out of the statement, so it's best to compare on the left:
if ($null -eq $ThisValue) {
After testing your example, I was unable to replicate your problem, however:
function Test-Nullable {
[CmdletBinding()]
param(
[nullable[bool]] $Value
)
if ($null -eq $Value) {
'Yes'
} else {
$Value
}
}
and in array format:
function Test-Nullable {
[CmdletBinding()]
param(
[nullable[bool][]] $Value
)
foreach ($bool in $Value) {
if ($null -eq $bool) {
'Yes'
} else {
$bool
}
}
}
Test-Nullable 5, 3, $null, $true, $false, 0
True
True
Yes
True
False
False

Related

Why $Null validation failed in powershell function?

team!
I have validation script for parameter $Data.
It fail when it get $null.
whats wrong?
[CmdletBinding()]
Param (
[Parameter( HelpMessage = "PsObject data." )]
[AllowNull()]
[AllowEmptyCollection()]
[AllowEmptyString()]
[ValidateScript({
if ( ( $_ -eq $null ) -or ( $_ -eq '' ) ){
$true
}
else {
(( !( $_.GetType().IsValueType ) ) -and ( $_ -isnot [string] ))
}
})]
$Data,
...
$UserObject = Show-ColoredTable -Data $null -View 'SamAccountName', 'Name', 'DistinguishedName', 'Enabled' -Title "AD User list" -SelectMessage "Select AD user(s) to disable VPN access: " -AddRowNumbers -AddNewLine -SelectField "SamAccountName"
Most validation attributes are incompatible with [AllowNull()] because the first thing they all check - before your custom validation is invoked - is whether the input object is $null or not.
Move the validation logic inside the function body:
[CmdletBinding()]
Param (
[Parameter( HelpMessage = "PsObject data." )]
[AllowNull()]
[AllowEmptyCollection()]
[AllowEmptyString()]
$Data
)
# validate $Data here before the rest of the script/command

Powershell only returns 2 values, using "return x,y"

I have a weird problem. I have a function that is supposed to return 1 or 2 values, a letter and a number.
For some reason, it only works when I specify the return as
return $x, $y
but it doesn't work like this:
return $x
return $y
The code:
$ModelsDesktop = #("Dimension","Optiplex")
$ModelsLaptop = #("Latitude","Venue")
<#
Returns L or D depending on the Computer Name. Sets U if the model is uncertain.
#>
Function Get-TypeByComputerName{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName
)
Process {
if ($ComputerName -like "*-L-*" -or $ComputerName -like "*-LT-*") {
$ModelType = "L"
}
elseif ($ComputerName -like "*-D-*" -or $ComputerName -like "*-WRK-*") {
$ModelType = "D"
}
else {
$ModelType = "U" #unsure
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Model. Sets U if the model is uncertain.
#>
Function Get-TypeByModel{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if (($ModelsLaptop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "L"
}
elseif (($ModelsDesktop | %{($Model) -like("*$_*")}) -contains $true) {
$ModelType = "D"
}
else {
$ModelType = "U"
}
return $ModelType
}
}
<#
Returns L or D depending on the Computer Name and Model. Sets a flag if the model is uncertain.
#>
Function Get-Type{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$ComputerName,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Model
)
Process {
if ((($ComputerName | Get-TypeByComputerName) -eq ($Model | Get-TypeByModel)) -and (($ComputerName | Get-TypeByComputerName) -ne "U")) {
$ModelType = ($ComputerName | Get-TypeByComputerName)
}
elseif (($ComputerName | Get-TypeByComputerName) -ne "U") {
$ModelType = ($ComputerName | Get-TypeByComputerName)
$Flag = 1
}
elseif (($Model | Get-TypeByModel) -ne "U") {
$ModelType = ($Model | Get-TypeByModel)
$Flag = 1
}
else {
$ModelType = "D"
$Flag = 1
}
return $ModelType
return $Flag
}
}
The value:
$test = New-Object psobject -Property #{ComputerName="crd-l-02-00001";Model="opti 343"}
Output with 2 return statements (as in the previous code):
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
Output with the corrected "return $ModelType, $Flag":
$test
ComputerName Model
------------ -----
crd-l-02-00001 opti 343
PS C:\Users\u0096902> (Get-Type -ComputerName $test.ComputerName -Model $test.Model)
L
1
What am I missing? Can't seem to figure it out. It only seems to return the first "return", but I don't know why.
This example code seems to work perfectly fine:
function get-multiplereturnvalues {
"Return value 1"
"Return value 2"
}
$return = get-multiplereturnvalues
$return[0] # Outputs "Return value 1"
$return[1] # Outputs "Return value 2"
"In computer programming, a return statement causes execution to leave the current subroutine and resume at the point in the code immediately after where the subroutine was called, known as its return address."
Once you call return you are saying that you are finished with that function.
From Get-Help about_Return:
LONG DESCRIPTION
The Return keyword exits a function, script, or script block. It can be
used to exit a scope at a specific point, to return a value, or to indicate
that the end of the scope has been reached.
Your first Return is forcing immediate exit from the function, so the second one never gets a chance to run.

How to reuse/extend functions on Powershell?

I'm trying to develop 2 functions with Powershell. The first, will check my database status (online/offline). The second function should loop on the first function until a certain state is achieve.
function Get-DBStatus
{
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
)
try
{
$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch
{
Write-Error -Message ('An error has occured while ...')
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false)
{
$dbStatus
}
# <<< function Get-DbStatusOnlyIf
# <<< same parameters as the function above
# <<< get the desired status as a new parameter
# <<< loop the function above until the desired status is achieved or a timeout is reached
}
I'm new to Powershell and I think I shouldn't repeat myself rewriting the same parameters from the first function into the second one since they're dependent. However, I might be wrong, thus the question.
Thank you for your assistance!
You have to rewrite this parameters on your second function and pass them through or add another paramter to your first function that will do the looping. I would go with the second solution.
Try something like that
function Get-DBStatus {
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
$WaitForStatus, #or something like that
[int]$Timeout=10
)
do {
try {
#$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch {
Write-Error -Message ('An error has occured while ...')
return
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false) {
if ($WaitForStatus){
if ($dbStatus -eq $WaitForStatus) {
$dbStatus
$EndLoop = $true
}
else {
Write-Host -NoNewline "." #only for test
Start-Sleep -Seconds 1
$Timeout -= 1
}
}
else{
$dbStatus
$EndLoop = $true
}
}
}
until ($EndLoop -or $Timeout -eq 0)
}
or with recursion
function Get-DBStatus {
<# .. removed help section for brevity .. #>
[CmdletBinding()]
[OutputType([System.Object])]
param
(
[Parameter(Mandatory = $true)]
[String]$ServerName,
[Parameter(Mandatory = $true)]
[String]$ServerUser,
[Parameter(Mandatory = $true)]
[String]$ServerPassword,
[Parameter(Mandatory = $true)]
[String]$DatabaseName,
$WaitForStatus, #or something like that
[int]$timeout = 3
)
if ($WaitForStatus) {
$start = Get-Date
while (((get-date) - $start).TotalSeconds -lt $timeout) {
$res = Get-DBStatus -ServerName $ServerName -ServerUser $ServerUser -ServerPassword $ServerPassword -DatabaseName $DatabaseName
if ($WaitForStatus -eq $res) {
return $res
}
Start-Sleep -Seconds 1
}
}
else {
try {
$params = #{ ... }
$dbStatus = Invoke-SqlConnection #params | Where-Object {$_.Name -match $AltDBName }
}
catch {
Write-Error -Message ('An error has occured while ...')
}
if ([String]::IsNullOrEmpty($dbStatus) -eq $false) {
$dbStatus
}
}
}

Can you combine CmdletBinding with unbound parameters?

(Powershell 5)
I have the following coalesce function:
(UPDATE: Removed the "optimized" continue call in the process block.)
function Find-Defined {
begin {
$ans = $NULL;
$Test = { $_ -ne $NULL };
}
process {
if ( $ans -eq $NULL ) {
$ans = $_ |? $Test | Select -First 1;
}
}
end {
if ( $ans -ne $NULL ) {
return $ans;
}
else {
$Args `
|% { if ( $_ -is [Array] ) { $_ |% { $_ } } else { $_ } } `
|? $Test `
| Select -First 1 `
| Write-Output `
;
}
}
}
And this works plenty well for me, on command lines like the following:
$NULL, $NULL, 'Legit', 1, 4 | Find-Defined;
$NULL, $NULL | Find-Defined $NULL, #( $NULL, 'Value' ), 3;
$NULL, $NULL | Find-Defined $NULL $NULL 3 4;
You may notice that I encapsulated the decision logic in a ScriptBlock variable. This was because I wanted to parameterize it and I started out trying this.
[CmdletBinding()]param( [ScriptBlock] $Test = { $_ -ne $NULL } );
However, the minute I added CmdletBinding I started to get errors. The binding wanted to try to cast everything in the argument section as a ScriptBlock, so I added
[CmdletBinding(PositionalBinding=$False)]
And then it complained that the unbound arguments couldn't be bound, and so I added:
param( [parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)][Object[]] $Arguments ...
And whatever I did afterwards added a new error. If I removed the $Test parameter, just localizing it to see what I could do, then I started getting the error I had when developing the first generation:
The input object cannot be bound to any parameters for the command either
because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
... even though I had a process block.
In the end simply removing the param statement, put it back to its flexible function that I liked.
I still would like to broaden this function to accept both a ScriptBlock test and unbound parameters (as well as Common parameters like -Verbose, if that's possible). That way I could have a general algorithm for string coalescing as well:
$Test = { -not [string]::IsNullOrEmpty( [string]$_ ) };
$NULL,$NULL,'','' | Find-Defined -Test $Test $NULL,'','This should be it' 'Not seen'
Am I missing something?
I believe this solve the problem you are trying to solve, but with a bit of a different implementation. When using CmdletBinding everything must be declared. So you need one parameter for the pipeline input and one for the "unbound" parameters.
Based on you question I wrote these test cases:
Describe 'Find-Defined' {
it 'should retun Legit' {
$NULL, $NULL, 'Legit', 1, 4 | Find-Defined | should be 'Legit'
}
it 'should retun Value' {
$NULL, $NULL | Find-Defined $NULL, #( $NULL, 'Value' ), 3 | should be 'Value'
}
it 'should retun 3' {
$NULL, $NULL | Find-Defined $NULL $NULL 3 4 | should be '3'
}
it 'Should return "This should be it"' {
$Test = { -not [string]::IsNullOrEmpty( [string]$_ ) };
$NULL,$NULL,'','' | Find-Defined -Test $Test $NULL,'','This should be it' 'Not seen' | should be 'This should be it'
}
}
Here is my solution, which passes all of the above cases.
function Find-Defined {
[CmdletBinding()]
param (
[ScriptBlock] $Test = { $NULL -ne $_},
[parameter(Mandatory=$False,ValueFromPipeline =$true)]
[Object[]] $InputObject,
[parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)]
[Object[]] $Arguments
)
begin {
$ans = $NULL;
function Get-Value {
[CmdletBinding()]
param (
[ScriptBlock] $Test = { $_ -ne $NULL },
[parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)]
[Object[]] $Arguments,
$ans = $NULL
)
$returnValue = $ans
if($null -eq $returnValue)
{
foreach($Argument in $Arguments)
{
if($Argument -is [object[]])
{
$returnValue = Get-Value -Test $Test -Arguments $Argument -ans $returnValue
}
else
{
if ( $returnValue -eq $NULL ) {
$returnValue = $Argument |Where-Object $Test | Select-Object -First 1;
if($null -ne $returnValue)
{
return $returnValue
}
}
}
}
}
return $returnValue
}
}
process {
$ans = Get-Value -Test $Test -Arguments $InputObject -ans $ans
}
end {
$ans = Get-Value -Test $Test -Arguments $Arguments -ans $ans
if ( $ans -ne $NULL ) {
return $ans;
}
}
}

Store Arguments and Variables And Use With Command

I am re-writing the Write-Progress function to work better with my script and to accomplish this I am joining all the arguments into a string and then trying to use it on the command however it is not working.
Function
Function Update-Progress {
Param (
[String] $Activity,
[Bool] $Completed,
[String] $CurrentOperation,
[Int] $ID,
[Int] $ParentID,
[Int] $PercentComplete,
[Int] $SecondsRemaining,
[Int] $SourceID,
[String] $Status
)
If ($htDisplay.WriteProgress.Enable -EQ $True -AND $htDisplay.WriteProgress.StopWatch.Elapsed.TotalMilliseconds -ge 500) {
$Parameters = New-Object System.Text.StringBuilder
IF (isNull($Activity)) { Write-Error "Activity is Required" } Else { $Null = $Parameters.Append("-Activity `"$Activity`"") }
IF (!(isNull($Completed))) { $Null = $Parameters.Append(" -Completed") }
IF (!(isNull($CurrentOperation))) { $Null = $Parameters.Append(" -CurrentOperation `"$CurrentOperation`"") }
IF (!(isNull($ID))) { $Null = $Parameters.Append(" -ID `"$ID`"") }
IF (!(isNull($ParentID))) { $Null = $Parameters.Append(" -ParentID `"$ParentID`"") }
IF (!(isNull($PercentComplete))) { $Null = $Parameters.Append(" -PercentComplete `"$PercentComplete`"") }
IF (!(isNull($SecondsRemaining))) { $Null = $Parameters.Append(" -SecondsRemaining `"$SecondsRemaining`"") }
IF (!(isNull($SourceID))) { $Null = $Parameters.Append("-SourceID `"$SourceID`"") }
IF (!(isNull($Status))) { $Null = $Parameters.Append(" -Status `"$Status`"") }
"Write-Progress $Parameters"
Write-Progress $Parameters
$htDisplay.WriteProgress.StopWatch.Reset()
$htDisplay.WriteProgress.StopWatch.Start()
}
}
Command Calling Function
Update-Progress -ID 1 -Activity "Preloading threads" -Status "Starting Job $($htConfig.MultiThread.Jobs.count)"
Error - The Progress bar displays
-Activity "Preloading Threads" -ID "1" -Status "Starting Job #"
Write-Host of the exact command shows the following so the syntax is correct, just need to figure out how to process the variable as all the parameters instead of just the activity param.
Write-Progress -Activity "Preloading threads" -ID "1" -Status "Starting Job 2"
to accomplish this I am joining all the arguments into a string and then trying to use it on the command however it is not working.
That's a terrible idea - use splatting with the $PSBoundParameters automatic variable instead - it already contains everything you need:
Function Update-Progress {
Param (
[String] $Activity,
[Bool] $Completed,
[String] $CurrentOperation,
[Int] $ID,
[Int] $ParentID,
[Int] $PercentComplete,
[Int] $SecondsRemaining,
[Int] $SourceID,
[String] $Status
)
If ($htDisplay.WriteProgress.Enable -EQ $True -AND $htDisplay.WriteProgress.StopWatch.Elapsed.TotalMilliseconds -ge 500) {
Write-Progress #PSBoundParameters
$htDisplay.WriteProgress.StopWatch.Reset()
$htDisplay.WriteProgress.StopWatch.Start()
}
}
I would probably go for a Switch rather than Bool for boolean parameters. You can grab the exact Param block from the original command with:
[System.Management.Automation.ProxyCommand]::GetParamBlock($(Get-Command Write-Progress))
And end up with getting input validation and positional parameters corresponding to what Write-Process expects:
Function Update-Progress {
param(
[Parameter(Mandatory=$true, Position=0)]
[string]${Activity},
[Parameter(Position=1)]
[ValidateNotNullOrEmpty()]
[string]${Status},
[Parameter(Position=2)]
[ValidateRange(0, 2147483647)]
[int]${Id},
[ValidateRange(-1, 100)]
[int]${PercentComplete},
[int]${SecondsRemaining},
[string]${CurrentOperation},
[ValidateRange(-1, 2147483647)]
[int]${ParentId},
[switch]${Completed},
[int]${SourceId}
)
If ($htDisplay.WriteProgress.Enable -EQ $True -AND $htDisplay.WriteProgress.StopWatch.Elapsed.TotalMilliseconds -ge 500) {
Write-Progress #PSBoundParameters
$htDisplay.WriteProgress.StopWatch.Reset()
$htDisplay.WriteProgress.StopWatch.Start()
}
}
Ok, Thanks to someone else here I have found the answer. It was inline expansion and I needed to use a hash table variable.
Source: Inline expansion of powershell variable as cmdlet parameter?
Function Update-Progress {
Param (
[String] $Activity,
[Bool] $Completed,
[String] $CurrentOperation,
[Int] $ID,
[Int] $ParentID,
[Int] $PercentComplete,
[Int] $SecondsRemaining,
[Int] $SourceID,
[String] $Status
)
If ($htDisplay.WriteProgress.Enable -EQ $True -AND $htDisplay.WriteProgress.StopWatch.Elapsed.TotalMilliseconds -ge 500) {
IF (isNull($Activity)) { Write-Error "Activity is Required" } Else { $Parameters = #{ Activity=$Activity } }
IF (!(isNull($Completed))) { $Parameters += #{ Completed=$Completed } }
IF (!(isNull($CurrentOperation))) { $Parameters += #{ CurrentOperation=$CurrentOperation } }
IF (!(isNull($ID))) { $Parameters += #{ ID=$ID} }
IF (!(isNull($ParentID))) { $Parameters += #{ ParentID=$ParentID } }
IF (!(isNull($PercentComplete))) { $Parameters += #{ PercentComplete=$PercentComplete } }
IF (!(isNull($SecondsRemaining))) { $Parameters += #{ SecondsRemaining=$SecondsRemaining } }
IF (!(isNull($SourceID))) { $Parameters += #{ SourceID=$SourceID } }
IF (!(isNull($Status))) { $Parameters += #{ Status=$Status } }
Write-Progress #Parameters
$htDisplay.WriteProgress.StopWatch.Reset()
$htDisplay.WriteProgress.StopWatch.Start()
}
}