(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;
}
}
}
Related
My understanding is that a complex object like a hash table is always passed by reference, while simple objects like strings or booleans are passed by value. I have been using this "fact" to do dependency injection with functions, and it has seemed to work until I need to pass the dependency on to a further function.
So I decided to do a simplified test to see where things are going wrong. And now even the dependency injection seems not to work. In this code, my expectation was that since I initially define $state as the return value from Primary in it's "Initialize" mode, I could then simply pass that on to further functions by reference, and when looking at $state at completion I would see all four times, as well as the id.
function Primary {
param (
[parameter(Mandatory=$true,
ParameterSetName = 'initialize')]
[String]$id,
[parameter(Mandatory=$true,
ParameterSetName = 'process')]
[Hashtable]$state
)
if ($id) {
Write-Host 'initialize Primary'
$primary = [Ordered]#{}
$primary.Add('id', $id)
$primary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$primary -init
} else {
Write-Host 'process Primary'
$primary = $true
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$state
}
return $primary
}
function Secondary {
param (
[parameter(ParameterSetName = 'initialize')]
[Switch]$init,
[parameter(Mandatory=$true)]
[Hashtable]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Primary -id:'Test'
Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
No such luck, I only see the primaryInit time. And yet, in my much more complex program, it seems as if I AM getting $state passed by reference. So, I wonder what is different in this very simple example, that it's not behaving as intended? Or am I misunderstanding what is happening, and some more in my production code I am creating a behavior that I misunderstand to be innate behavior?
I also tried an even more simplified version, to remove the part calling functions from within functions.
function ByReference {
param (
[Hashtable]$state
)
$state.Add('now', (Get-Date))
}
$state = [Ordered]#{
id = 'test'
}
ByReference $state
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
This also shows $state being passed by Value, such that the changes aren't seen when looking at the variable in Main.
EDIT: Based on #Daniel 's link, I revised to
function Primary {
param (
[parameter(Mandatory=$true,
ParameterSetName = 'initialize')]
[String]$id,
[parameter(Mandatory=$true,
ParameterSetName = 'process')]
[Ref]$state
)
if ($id) {
Write-Host 'initialize Primary'
$primary = [Ordered]#{}
$primary.Add('id', $id)
$primary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$primary -init
} else {
Write-Host 'process Primary'
$primary = $state
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$state
}
return $primary
}
function Secondary {
param (
[parameter(ParameterSetName = 'initialize')]
[Switch]$init,
[parameter(Mandatory=$true)]
[Ref]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Primary -id:'Test'
Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
And this is still not showing anything but the very first time. That said, I can get this to work.
Function Test($data)
{
$data.Test = "New Text"
}
$var = #{}
Test -data $var
$var
Which got me thinking maybe this only works if you aren't using a param() block. So I tried removing the param block and using function Primary ([String]$id, [Ref]$state) {}. Still no joy.
The other thing I notice is that all the examples create the variable in main. I am creating the variable in the init mode of my method. Could it be that the variable needs to be global or script scope? I tried using scope modifiers when initially defining $primary but thiat isn't working either.
EDIT 2: So it seems the key is you must not type the byRef argument.
So this works.
function Test-Primary {
param (
[String]$id,
$state = [Ordered]#{}
)
if ($id) {
Write-Host 'initialize Primary'
$state.Add('id', $id)
$state.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state -init
return $state
} else {
Write-Host 'process Primary'
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state
}
}
function Test-Secondary {
param (
[Switch]$init,
$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Test-Primary -id:'Test'
Test-Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
A little odd, as I would wish that you could either specifically type the parameter, so ensure you don't change type, but I guess that's just another reason to start using Classes. Functions are "sloppy" because they are intended for down and dirty stuff I guess.
EDIT: OK, progress, but it seems I am still trying to do something odd. I have come to the conclusion that, when doing these multi-"mode" functions where one mode returns a value and the other mode uses a passed by reference variable, you need a different name for the return value. Add to that I need for [Ref] both when calling the function and declaring the variable in the function. I then assumed that I could use [Ref] even with a hash table, as a way to remind myself that this is a by reference value. And then the need for .Value to actually deal with the variable got me to this.
function Test-Primary {
param (
[String]$id,
[Ref]$state
)
if ($id) {
Write-Host 'initialize Primary'
$testPrimary = [Ordered]#{}
$testPrimary.Add('id', $id)
$testPrimary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:([Ref]$testPrimary) -init
return $testPrimary
} else {
Write-Host 'process Primary'
$state.Value.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state
}
}
function Test-Secondary {
param (
[Switch]$init,
[Ref]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Value.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Value.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Test-Primary -id:'Test'
Test-Primary -state:([Ref]$state)
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
As you figured out hash tables don't need to be passed by reference to change its properties. But to show you how [Ref] actually works, take a look at the following example:
function Test-RefParameter
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[Ref] $Output
)
$Output.Value = "output from test"
}
$testVar = "before test"
Write-Information "Current value: '$testVar'" -InformationAction Continue
Test-RefParameter -Output ([Ref]$testVar)
Write-Information "Current value: '$testVar'" -InformationAction Continue
Which displays:
Current value: 'before test'
Current value: 'output from test'
So the key is to use .Value when assigning the value to the [Ref] parameter
If I have the following (simplified Setup)
class.ps1
class Test {
Test() {
$MyInvocation | Show-Object
}
[void] Call() {
$MyInvocation | Show-Object
<#
here $MyInvocation.ScriptName, PSCommandPath show main.ps1 NOT
util.ps1 even though it is called from Util.ps1
#>
}
}
Util.ps1
Write-Host "Calling from Util.ps1"
$MyInvocation | Show-Object
Function Test-Util {
[CmdletBinding()]
Param()
Write-Host "Calling from Test-Util in Util.ps1"
$MyInvocation | Show-Object
}
Function Test-Class {
[CmdletBinding()]
Param()
write-host "Testing Class Test from Util.ps1"
$Test = [Test]::new()
Write-Host "Testing Class.Call() from Util.ps1"
$Test.Call()
}
Function Test-SubUtilTest {
[CmdletBinding()]
Param()
Test-SubUtil
}
SubUtil.ps1
Write-Host "Calling from SubUtil.ps1"
$MyInvocation | Show-Object
Function Test-SubUtil {
[CmdletBinding()]
Param()
Write-Host "Calling from Test-Util in Util.ps1"
$MyInvocation | Show-Object
<#
here $MyInvocation.ScriptName, PSCommandPath show Util.ps1 NOT
main.ps1 as it is called from Util.ps1
#>
}
Main.ps1
. C:\Users\jenny\Class.ps1
. C:\Users\jenny\Util.ps1
. C:\Users\jenny\SubUtil.ps1
Write-Host "From Main.ps1"
$MyInvocation | Show-Object
write-host "Calling Test-Util from util.ps1"
Test-Util
Write-Host "Calling Test-Class from util.ps1"
Test-Class
write-host "Calling Test-SubUtil from Util.ps1"
Test-SubUtilTest
$Test = [Test]::new()
and
$Test.Call()
are both executed from util.ps1
yet the $MyInvocation only shows me main.ps1
How do I from either the Constructor of the class or one of its methods determine the ps1 file its calling code originates from when its in this kind of a nested dot source setup
I have tried swapping to & instead of . and I've also tried just moving the dot source of the class.ps1 to the util.ps1 file but it still tells me main.ps1 is the source.
Plus the real class file is a singleton meant to be used across multiple dot sourced util.ps1 files and I'm not sure if I can dot source the same class file in multiple files that are each dot sourced into main.ps1 (not that moving the dot source into the single utility file in this example made a difference)
Finally I'm using Show-Object from the PowerShellCookbook module.
It seems odd that it works for a function based nested call but not for a call to a class
You could use the stack to determine the caller:
class Test {
Test() {
Get-PSCallStack | Select-Object -First 1 -Skip 1 -ExpandProperty "Location" | Write-Host
}
[void] Call() {
Get-PSCallStack | Select-Object -First 1 -Skip 1 -ExpandProperty "Location" | Write-Host
}
}
mhu deserves full credit and the official answer check mark for giving me the answer. But I figured I'd post my fixed classes with the implemented behavior for anyone else who happens to need to do what I had to
Enum LogEntryTypes {
Information
Error
Verbose
Warning
}
Log.psm1
Class Log {
[string] $Name
hidden [string] $FullPath
hidden Log([string] $Name, [string] $Path) {
$this.Name = $Name
$this.FullPath = (Join-Path -Path $Path -ChildPath "$($this.Name).log")
}
[Log] Start([bool] $Append) {
if (-not $Append) { remove-item -Path $this.FullPath -Force -Verbose }
$this.Information("$($this.Name) Logging Started to $($this.FullPath)")
return $this
}
[void] Stop() { $this.Information("$($this.Name) Logging Ended to $($this.FullPath)") }
Information([string] $Message) { $this.Information($Message,$null) }
Information([string] $Message, [object] $Data) { $this.Write($Message,$Data,[LogEntryTypes]::Information) }
Warning([string] $Message) { $this.Warning($Message,$null) }
Warning([string] $Message, [object] $Data) { $this.Write($Message,$Data,[LogEntryTypes]::Warning) }
Error([string] $Message) { $this.Error($Message,$null) }
Error([string] $Message, [object] $Data) { $this.Write($Message,$Data,[LogEntryTypes]::Error) }
Verbose([string] $Message) { $this.Verbose($Message,$null) }
Verbose([string] $Message, [object] $Data) { $this.Write($Message,$Data,[LogEntryTypes]::Verbose) }
[void] hidden Write([string] $Message, [object] $Data, [LogEntryTypes] $EntryType) {
$Message = $Message -replace '"', '`"'
"[$($EntryType.ToString().ToUpper())]<$([DateTime]::Now)>:`tMessage->$Message" | Add-Content -Path $this.FullPath
if ($Data -ne $null) { "[$($EntryType.ToString().ToUpper())]<$([DateTime]::Now)>:`tData->$Data" | Add-Content -Path $this.FullPath }
"Write-$EntryType `"[$($EntryType.ToString().ToUpper())]<$([DateTime]::Now)>:`tMessage->$Message`"" | Invoke-Expression
if ($Data -ne $null) { "Write-$EntryType `"[$($EntryType.ToString().ToUpper())]<$([DateTime]::Now)>:`tData->$Data`"" | Invoke-Expression }
}
}
Logger.ps1
using namespace System.Collections.Generic
using module ".\Log.psm1"
Class Logger {
hidden static [Dictionary[String,Log]] $_Logger
static [string] $Path
static [bool] $Append
static Logger() { [Logger]::_Logger = [Dictionary[string,Log]]::new() }
hidden static [string] GetCallingScriptName() { return ((Get-PSCallStack | Where-Object {$_.Location -notlike "$(((Get-PSCallStack | Select-Object -First 1 -ExpandProperty Location) -split "\.ps1")[0])*" } | Select-Object -First 1 -ExpandProperty "Location") -split "\.ps1")[0] }
static [Log] Get() { return [Logger]::Get($false) }
static [Log] Get([bool] $Force) {
$Name = [Logger]::GetCallingScriptName()
if ($null -eq [Logger]::_Logger[$Name] -or $Force) {
[Logger]::_Logger[$Name] = [Log]::new($Name,[Logger]::Path).Start([Logger]::Append)
}
return [Logger]::_Logger[$Name]
}
static Setup([string] $Path) { [Logger]::Setup($Path,$true) }
static Setup([string] $Path, [bool] $Append) {
[Logger]::Path = $Path
[Logger]::Append = $Append
}
}
thanks to mhu it's pretty simple I can now use this class from any script file by calling
[Logger]::Get().<Entry Type Method>() the Get() either opens the existing log created for the script or creates a new log
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
I've been trying to compare two hashes of hashes against each other. Unfortunately even with the great help from here I struggled to pull together what I was trying to do.
So, I've resorted to searching the internet again and I was lucky enough to find Edxi's code (full credit to him and Dbroeglin who looks to have written it originally)https://gist.github.com/edxi/87cb8a550b43ec90e4a89d2e69324806 His compare-hash function does exactly what I needed in terms of its comparisons. However, I'd like to report of the full path of the hash in the final output. So I've tried to update the code to allow for this. My thinking being that I should be able to take the Key (aka path) while the code loops over itself but I'm clearly going about it in the wrong manner.
Am I going about this with the right approach, and if not, would someone suggest another method please? The biggest problem I've found so far is that if I change the hash, my code changes don't work e.g. adding 'More' = 'stuff' So it becomes $sailings.Arrivals.PDH083.More breaks the path in the way I've set it.
My version of the code:
$Sailings = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '10:11'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port1'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'OnTime'
'Other' = #{'Data' = 'Test'}
}
}
}
$Flights = #{
'Arrivals' = #{
'PDH083' = #{
'GoingTo' = 'Port'
'Scheduled' = '09:05'
'Expected' = '10:20'
'Status' = 'Delayed'
}
}
'Departures' = #{
'PDH083' = #{
'ArrivingFrom' = 'Port11'
'Scheduled' = '09:05'
'Expected' = '09:05'
'Status' = 'NotOnTime'
'Other' = #{'Data' = 'Test_Diff'}
}
}
}
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string] $path,
[boolean] $trackpath = $True
)
write-host "PAth received as: $path"
function New-Result($Key, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Key
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
[Object[]]$Results = $Left.Keys | ForEach-Object {
if ($trackpath ) {
write-host "Working on Path: " $path + $_
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $path"
New-Result $path $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
if ($path -ne $null -or $path -ne "/") {
$path = $path + "/" + $_
write-host "Sending Path to function as: $path"
}
Compare-Hashtable $Left[$_] $Right[$_] $path
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
$path = $path + "/" + $_
write-host "Not a hash so must be a value at path:$path"
New-Result $path $LValue "!=" $RValue
}
else {
Write-Host "Before changing path: $path "
if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
$path = $path.Substring(0, $path.lastIndexOf('/'))
Write-Host "After changing path: $path "
}
}
}
}
# if (($path.Substring(0, $path.lastIndexOf('/'))).length >0) {
# Tried to use this to stop error on substring being less than zero
# but clearly doesnt work when you add more levels to the hash
$path = $path.Substring(0, $path.lastIndexOf('/'))
# } else { $path = $path.Substring(0, $path.lastIndexOf('/')) }
}
$Results += $Right.Keys | ForEach-Object {
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $_ $Null "=>" $Right[$_]
}
}
if ($Results -ne $null) { $Results }
}
cls
Compare-Hashtable $Sailings $Flights
outputs
key lvalue side rvalue
--- ------ ---- ------
/Arrivals/PDH083/Expected 10:11 != 10:20
/Arrivals/GoingTo Port1 != Port
/Departures/PDH083/ArrivingFrom Port1 != Port11
/Departures/PDH083/Status OnTime != NotOnTime
/Departures/PDH083/Other/Data Test != Test_Diff
I won't do that much string manipulation on the $Path but threat $Path as an array and join it to a string at the moment you assign it as a property (key = $Path -Join "/") to the object:
function Compare-Hashtable {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[Hashtable]$Left,
[Parameter(Mandatory = $true)]
[Hashtable]$Right,
[string[]] $path = #(),
[boolean] $trackpath = $True
)
write-host "Path received as: $path"
function New-Result($Path, $LValue, $Side, $RValue) {
New-Object -Type PSObject -Property #{
key = $Path -Join "/"
lvalue = $LValue
rvalue = $RValue
side = $Side
}
}
$Left.Keys | ForEach-Object {
$NewPath = $Path + $_
if ($trackpath ) {
write-host "Working on Path: " $NewPath
}
if ($Left.ContainsKey($_) -and !$Right.ContainsKey($_)) {
write-host "Right key not matched. Report path as: $NewPath"
New-Result $NewPath $Left[$_] "<=" $Null
}
else {
if ($Left[$_] -is [hashtable] -and $Right[$_] -is [hashtable] ) {
Compare-Hashtable $Left[$_] $Right[$_] $NewPath
}
else {
$LValue, $RValue = $Left[$_], $Right[$_]
if ($LValue -ne $RValue) {
New-Result $NewPath $LValue "!=" $RValue
}
}
}
}
$Right.Keys | ForEach-Object {
$NewPath = $Path + $_
if (!$Left.ContainsKey($_) -and $Right.ContainsKey($_)) {
New-Result $NewPath $Null "=>" $Right[$_]
}
}
}
cls
Compare-Hashtable $Sailings $Flights
side-note: Write Single Records to the Pipeline (SC03), see: Strongly Encouraged Development Guidelines. In other words, don't do the $Results += ... but intermediately put any completed result in the pipeline for the next cmdlet to be picked up (besides, it unnecessarily code)...
Using Pester, I'm mocking an advanced function which takes, amongst other parameters, a switch. How do I create a -parameterFilter for the mock which includes the switch parameter?
I've tried:
-parameterFilter { $Domain -eq 'MyDomain' -and $Verbose -eq $true }
-parameterFilter { $Domain -eq 'MyDomain' -and $Verbose }
-parameterFilter { $Domain -eq 'MyDomain' -and $Verbose -eq 'True' }
to no avail.
Try this:
-parameterFilter { $Domain -eq 'MyDomain' -and $Verbose.IsPresent}
-Verbose is a common parameter, which makes this a bit more tricky. You never actually see a $Verbose variable in your function, and the same applies to the parameter filter. Instead, when someone sets the common -Verbose switch, what actually happens is the $VerbosePreference variable gets set to Continue instead of SilentlyContinue.
You can, however, find the Verbose switch in the $PSBoundParameters automatic variable, and you should be able to use that in your mock filter:
Mock someFunction -parameterFilter { $Domain -eq 'MyDomain' -and $PSBoundParameters['Verbose'] -eq $true }
The following appears to work fine:
Test.ps1 - This just contains two functions. Both take the same parameters but Test-Call calls through to Mocked-Call. We will mock Mocked-Call in our tests.
Function Test-Call {
param(
$text,
[switch]$switch
)
Mocked-Call $text -switch:$switch
}
Function Mocked-Call {
param(
$text,
[switch]$switch
)
$text
}
Test.Tests.ps1 - This is our actual test script. Note we have two mock implementations for Mocked-Call. The first will match when the switch parameter is set to true. The second will match when the text parameter has a value of fourth and the switch parameter has a value of false.
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"
Describe "Test-Call" {
It "mocks switch parms" {
Mock Mocked-Call { "mocked" } -parameterFilter { $switch -eq $true }
Mock Mocked-Call { "mocked again" } -parameterFilter { $text -eq "fourth" -and $switch -eq $false }
$first = Test-Call "first"
$first | Should Be "first"
$second = Test-Call "second" -switch
$second | Should Be "mocked"
$third = Test-Call "third" -switch:$true
$third | Should Be "mocked"
$fourth = Test-Call "fourth" -switch:$false
$fourth | Should Be "mocked again"
}
}
Running the tests shows green:
Describing Test-Call
[+] mocks switch parms 17ms
Tests completed in 17ms
Passed: 1 Failed: 0