Problems returning hashtable - powershell

So if I have the following code:
function DoSomething {
$site = "Something"
$app = "else"
$app
return #{"site" = $($site); "app" = $($app)}
}
$siteInfo = DoSomething
$siteInfo["site"]
Why doesn't $siteInfo["site"] return "Something"?
I can state just....
$siteInfo
And it will return
else
Key: site
Value: Something
Name: site
Key: app
Value: else
Name: app
What am I missing?

In PowerShell, functions return any and every value that is returned by each line in the function; an explicit return statement is not needed.
The String.IndexOf() method returns an integer value, so in this example, DoSomething returns '2' and the hashtable as array of objects as seen with .GetType().
function DoSomething {
$site = "Something"
$app = "else"
$app.IndexOf('s')
return #{"site" = $($site); "app" = $($app)}
}
$siteInfo = DoSomething
$siteInfo.GetType()
The following example shows 3 ways to block unwanted output:
function DoSomething {
$site = "Something"
$app = "else"
$null = $app.IndexOf('s') # 1
[void]$app.IndexOf('s') # 2
$app.IndexOf('s')| Out-Null # 3
# Note: return is not needed.
#{"site" = $($site); "app" = $($app)}
}
$siteInfo = DoSomething
$siteInfo['site']
Here is an example of how to wrap multiple statements in a ScriptBlock to capture unwanted output:
function DoSomething {
# The Dot-operator '.' executes the ScriptBlock in the current scope.
$null = .{
$site = "Something"
$app = "else"
$app
}
#{"site" = $($site); "app" = $($app)}
}
DoSomething

#Rynant VERY helpful post, thank you for providing examples on hiding function output!
My proposed solution:
function DoSomething ($a,$b){
#{"site" = $($a); "app" = $($b)}
}
$c = DoSomething $Site $App

Related

Possible to repeat an alias among different parameter sets in a powershell function?

Suppose I have:
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('a')]
[int]$Apple,
[Parameter(Mandatory,ParameterSetName='y')]
[Alias('b')]
[int]$Banana,
[Parameter(Mandatory,ParameterSetName='x')]
[Alias('b')]
[int]$Cherry
)
"Apple $Apple"
"Banana $Banana"
"Cherry $Cherry"
}
I'd like to be able to call f -Apple 1 -b 3 because including Apple means I'm certainly using Parameter Set x, but powershell complains that the alias b is declared multiple times.
Is it entirely impossible, or am I just missing a trick?
The non-trivial function I'm trying to write is a convenience wrapper for multiple external functions that have their own aliases, some of which can be the same for different named parameters, but the set of mandatory parameters would never be ambiguous.
I couldn't get it to work using regular params, but I found a workaround by defining Banana and Cherry as dynamic params. This way the alias b is only defined once, so PowerShell won't complain.
function f {
[CmdletBinding(DefaultParameterSetName='x')]
param(
[Parameter(Mandatory, ParameterSetName='x')]
[Alias('a')]
[int]$Apple
)
DynamicParam {
# If param 'Apple' exists, define dynamic param 'Cherry',
# else define dynamic param 'Banana', both using alias 'b'.
if( $PSBoundParameters.ContainsKey('Apple') ) {
$paramName = 'Cherry'
$paramSetName = 'x'
} else {
$paramName = 'Banana'
$paramSetName = 'y'
}
$aliasName = 'b'
$parameterAttribute = [System.Management.Automation.ParameterAttribute]#{
ParameterSetName = $paramSetName
Mandatory = $true
}
$aliasAttribute = [System.Management.Automation.AliasAttribute]::new( $aliasName )
$attributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$attributeCollection.Add( $parameterAttribute )
$attributeCollection.Add( $aliasAttribute )
$dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
$paramName, [Int32], $attributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add($paramName, $dynParam)
$paramDictionary
}
process {
"--- Parameter Set '$($PSCmdlet.ParameterSetName)' ---"
if( $PSBoundParameters.ContainsKey('Apple') ) {
"Apple $Apple"
}
if( $PSBoundParameters.ContainsKey('Banana') ) {
# Dynamic params require special syntax to read
$Banana = $PSBoundParameters.Banana
"Banana $Banana"
}
if( $PSBoundParameters.ContainsKey('Cherry') ) {
# Dynamic params require special syntax to read
$Cherry = $PSBoundParameters.Cherry
"Cherry $Cherry"
}
}
}
Calling the function:
f -Apple 1 -b 3
f -b 2
Output:
--- Parameter Set 'x' ---
Apple 1
Cherry 3
--- Parameter Set 'y' ---
Banana 2

Is it possible to create specific constant elements of a PowerShell array?

Let's say I have array Grid that was initialized with
$Grid = #(#(1..3), #(4..6), #(7..9))
and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?
So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0] # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly # True
$Grid[0][0] = "test"
$Grid[0][0] # test
I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:
$test = [Array]::AsReadOnly(#(1,2,3,4))
$test[0]=1 # Errors
$test = "test"
$test # test
$Grid = #(#(1..3), #(4..6), #(7..9))
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0][0] = 1 # Errors
$Grid[0][0] = "test"
$Grid[0][0] # test
I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.
You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:
$grid =
[array]::AsReadOnly(#(
,[array]::AsReadOnly(#(1,2,3))
,[array]::AsReadOnly(#(3,2,1))
))
(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)
Now $grid should behave as you expect:
$grid[0] # 1,2,3
$grid[0][0] # 1
$grid[0][0] = 4 # error
$grid[0] = 4 # error
If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:
using namespace System.Collections
class Grid {
hidden [int[,]] $data
hidden [bool[,]] $mask
Grid([int]$width,[int]$height){
$this.mask = [bool[,]]::new($width, $height)
$this.data = [int[,]]::new($width, $height)
}
[int]
Get([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
return $this.data[$x,$y]
}
Set([int]$x,[int]$y,[int]$value)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
if(-not $this.mask[$x,$y])
{
$this.data[$x,$y] = $value
}
else
{
throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
}
}
Freeze([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $true
}
Unfreeze([int]$x,$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $false
}
hidden [bool]
CheckBounds([int]$x,[int]$y)
{
return (
$x -ge $this.data.GetLowerBound(0) -and
$x -le $this.data.GetUpperBound(0) -and
$y -ge $this.data.GetLowerBound(1) -and
$y -le $this.data.GetUpperBound(1)
)
}
}
Now you can do:
$grid = [Grid]::new(5,5)
$grid.Set(0, 0, 1) # Set cell value
$grid.Get(0, 0) # Get cell value
$grid.Freeze(0, 0) # Now freeze cell 0,0
$grid.Set(0, 0, 2) # ... and this will now throw an exception
$grid.Set(0, 1, 1) # Setting any other cell still works
If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

Powershell class method return being cast to new type

I am working on a new PowerShell class that takes the path to an XML file in my old format and converts it to my new format. The constructor takes the path argument and calls a method, which should return a custom object with either the XML or an error message explaining the absence of the XML. What is odd is that the XML portion of the custom object is XML before the method returns, but immediately after it has been cast to a string.
So this class
class pxT_DefinitionsMigrater {
# Properties
$XML = [xml.xmlDocument]::New()
$Errors = [System.Collections.Generic.List[string]]::new()
# Constructor
pxT_DefinitionsMigrater ([string]$xmlPath) {
if ($oldDefinition = $this.readXMLFile($xmlPath).xml) {
$this.XML = $oldDefinition.xml
Write-Host "pxT_DefinitionsMigrater: $($oldDefinition.xml.GetType().FullName)"
} else {
$this.Errors = $oldDefinition.error
}
}
# Methods
[psCustomObject] readXMLFile ([string]$xmlPath) {
$readXMLFile = [psCustomObject]#{
xml = $null
error = $null
}
$fileStream = $null
$xmlreader = $null
$importFile = [xml.xmlDocument]::New()
$xmlReaderSettings = [xml.xmlReaderSettings]::New()
$xmlReaderSettings.closeInput = $true
$xmlReaderSettings.prohibitDtd = $false
try {
$fileStream = [io.fileStream]::New($xmlPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
$xmlreader = [xml.xmlreader]::Create($fileStream, $xmlReaderSettings)
$importFile.Load($xmlreader)
} catch [System.Management.Automation.MethodInvocationException] {
if ($_.Exception.Message -match ': "(?<string>.*)"$') {
$readXMLFile.error = "Error loading XML; $($matches['string'])" # removes 'Exception calling "Load" with "1" argument(s):' from message
} else {
$readXMLFile.error = "Error loading XML; $($_.Exception.Message)"
}
} catch {
$readXMLFile.error = "Error loading XML; $($_.Exception.FullName) - $($_.Exception.Message)"
} finally {
if ($xmlreader) {
$xmlreader.Dispose()
}
if ($readXMLFile.error) {
$readXMLFile.xml = $null
} else {
$readXMLFile.xml = $importFile
}
}
Write-Host "readXMLFile: $($readXMLFile.xml.GetType().FullName)"
return $readXMLFile
}
}
Will echo
readXMLFile: System.Xml.XmlDocument
pxT_DefinitionsMigrater: System.String
I am contemplating moving the XML load code into the ctor since that is the only place it is needed, but the fact that the XML is getting cast to string seems like something I need to understand. What is causing this? And what do I need to do to make this work with the method, should I want to?

how to create conditional arguments in a function?

I have the following function that hides a password middle chars and leaves 1st and last char visible
function Hide-ConnectionStringPassword {
param(
[parameter(Mandatory,ValueFromPipeline)]
[System.Data.SqlClient.SqlConnectionStringBuilder]$ConnectionString
)
[string]$FistChar = $ConnectionString.Password[0]
[string]$LastChar = $ConnectionString.Password[($ConnectionString.Password.Length - 1)]
[string]$Stars = '*' * ($ConnectionString.Password.Length - 2)
$ConnectionString.Password = $FistChar + $Stars + $LastChar
return $ConnectionString.ConnectionString
}
Usage:
Hide-ConnectionStringPassword 'Connection Timeout=120;User Id=UID1;Data Source=datasource.com;Password=password12!553;'
output:
Data Source=datasource.com;User
ID=UID1;Password=p************3;Connect Timeout=120
I have other connection strings that are JSON formatted, so the casting to sqlbuilder will not work on this type of input
{"Authentication
Kind":"UsernamePassword","Username":"someID1","Password":"Yu#gh456!ts","EncryptConnection":true}
One thing i could do is something like this:
$Json = '{"Authentication Kind":"UsernamePassword","Username":"someID1","Password":"Yu#gh456!ts","EncryptConnection":true}'
$Sql = $Json | ConvertFrom-Json
$Sql.Password
with
$Sql.gettype().name
i get
PSCustomObject
i would like to apply that in the function such that it checks string input is type pscustomobject so that it doesnt cast it as sqlbuilder
pseudocode:
function Hide-ConnectionStringPassword {
if ($input.gettype().name -ne 'PSCustomObject')
{
param(
[parameter(Mandatory,ValueFromPipeline)]
[System.Data.SqlClient.SqlConnectionStringBuilder]$ConnectionString
)}
else
{
param(
[parameter(Mandatory,ValueFromPipeline)]
$ConnectionString
)}
[string]$FistChar = $ConnectionString.Password[0]
[string]$LastChar = $ConnectionString.Password[($ConnectionString.Password.Length - 1)]
[string]$Stars = '*' * ($ConnectionString.Password.Length - 2)
$ConnectionString.Password = $FistChar + $Stars + $LastChar
return $ConnectionString.ConnectionString
}
You could just remove the hard typing in the parameter. Then move the if-else logic to the script Process {} or Begin {} script blocks.
function Hide-ConnectionStringPassword {
param(
[parameter(Mandatory,ValueFromPipeline)]
$ConnectionString
)
if ($ConnectionString.GetType().Name -ne "PSCustomObject") {
$ConnectionString = $ConnectionString -as [System.Data.SqlClient.SqlConnectionStringBuilder]
}
[string]$FistChar = $ConnectionString.Password[0]
[string]$LastChar = $ConnectionString.Password[($ConnectionString.Password.Length - 1)]
[string]$Stars = '*' * ($ConnectionString.Password.Length - 2)
$ConnectionString.Password = $FistChar + $Stars + $LastChar
return $ConnectionString.ConnectionString
}

How do I correctly mock my Function to return a custom property with Pester?

I'm a bit new to PowerShell and specifically Pester testing. I can't seem to recreate a scenario for the function I am making Pester test.
Here is the code:
$State = Get-Status
if(State) {
switch ($State.Progress) {
0 {
Write-Host "Session for $Name not initiated. Retrying."
}
100{
Write-Host "Session for $Name at $($State.Progress) percent"
}
default {
Write-Host "Session for $Name in progress (at $($State.Progress)
percent)."
}
}
I've mocked Get-Status to return true so that the code path would go inside the if block, but then the result doesn't have any value for $State.Progress.
My test would always go into the default block in terms of code path. I tried
creating a custom object $State = [PSCustomObject]#{Progress = 0} to no avail.
Here is part of my Pester test:
Context 'State Progress returns 0' {
mock Get-Status {return $true} -Verifiable
$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
}
There are a couple of issues:
Per 4c's comments, your Mock may not be being called because of scoping (unless you have a describe block around your context not shown). If you change Context to Describe and then use Assert-VerifiableMocks you can see that the Mock does then get called.
You can't verify the output of code that uses Write-Host because this command doesn't write to the normal output stream (it writes to the host console). If you remove Write-Host so that the strings are returned to the standard output stream, the code works.
You can use [PSCustomObject]#{Progress = 0} to mock the output of a .Progress property as you suggested, but I believe this should be inside the Mock of Get-Status.
Here's a minimal/verifiable example that works:
$Name = 'SomeName'
#Had to define an empty function in order to be able to Mock it. You don't need to do this in your code as you have the real function.
Function Get-Status { }
#I assumed based on your code all of this code was wrapped as a Function called Confirm-Session
Function Confirm-Session {
$State = Get-Status
if ($State) {
switch ($State.Progress) {
0 {
"Session for $Name not initiated. Retrying."
}
100{
"Session for $Name at $($State.Progress) percent"
}
default {
"Session for $Name in progress (at $($State.Progress) percent)."
}
}
}
}
#Pester tests for the above code:
Describe 'State Progress returns 0' {
mock Get-Status {
[PSCustomObject]#{Progress = 0}
} -Verifiable
#$State = [PSCustomObject]#{Progress = 0}
$result = Confirm-Session
it 'should be' {
$result | should be "Session for $Name not initiated. Retrying."
}
it 'should call the verifiable mocks' {
Assert-VerifiableMocks
}
}
Returns:
Describing State Progress returns 0
[+] should be 77ms
[+] should call the verifiable mocks 7ms