Powershell pass complex object By Value, not By Reference - powershell

I am trying to process some data in an ordered dictionary, then add that to another ordered dictionary, and I can do that by reinitializing my temporary dictionary, like this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
However, I feel like reinitializing is a bit inefficient/inelegant, and I would rather just .Clear() that temporary variable. Which leads to this...
$collection = [Collections.Specialized.OrderedDictionary]::new()
$tempCollection = [Collections.Specialized.OrderedDictionary]::new()
foreach ($id in 1..5) {
foreach ($char in [Char]'a'..[Char]'e') {
$letter = ([Char]$char).ToString()
if ($id % 2 -eq 0) {
$letter = $letter.ToUpper()
}
$int = [Int][Char]$letter
$tempCollection.Add($letter, $int)
}
$collection.Add($id, $tempCollection)
$tempCollection.Clear()
}
foreach ($id in $collection.Keys) {
Write-Host "$id"
foreach ($key in $collection.$id.Keys) {
Write-Host " $key : $($collection.$id.$key)"
}
}
The problem is that while simple objects like string, int, char, etc are passed by value, all complex objects like a dictionary are passed by reference. So I pass the SAME dictionary in every iteration of $collection.Add($id, $tempCollection) and the final state of $tempCollection is cleared, so the result is 5 empty members of $collection.
I know I can force something that is normally passed By Value to be By Reference using [Ref] as outlined here. And [Ref] is just an accelerator for System.Management.Automation.PSReference. So what I need is a way to force an argument By Value, but neither [Val] nor [ByVal] works, and searching for System.Management.Automation.PSValue doesn't seem to return anything useful either. The PSReference doco linked above says
This class is used to describe both kinds of references:
a. reference to a value: _value will be holding the value being referenced.
b. reference to a variable: _value will be holding a PSVariable
instance for the variable to be referenced.
which makes me think I can get to the Value somehow, but for the life of me I can't grok HOW. Am I on the right track, and just missing something, or am I misunderstanding this documentation completely?
Cloning also seems like a potential solution, i.e. $collection.Add($id, $tempCollection.Clone()), but Ordered Dictionaries don't implement ICloneable. .CopyTo() also isn't an option, since it doesn't necessarily maintain the order of the elements. Nor does .AsReadOnly() since
The AsReadOnly method creates a read-only wrapper around the current
OrderedDictionary collection. Changes made to the OrderedDictionary
collection are reflected in the read-only copy. Nor does OrderedDictionary implement .copy() as PSObject does.
I also tried making a new variable, like this...
$newCollection = $tempCollection
$collection.Add($id, $newCollection)
$tempCollection.Clear()
And that doesn't work either. So it seems that complex objects by reference seems to apply to more than just passed arguments.
It seems almost like my Ordered Dictionary choice/need is the root of the problem, but it seems like needing a unconnected copy of an Ordered Dictionary would not be such an edge case that it isn't supported.

Related

Unable to Return [BindingList[RandomType]] from ScriptProperty

I have a complex class that dynamically adds members to itself based on a file loaded via Import-Clixml.
Boiling the class down to the problematic part leaves us with this (Take note of the commented line used to prove success up to that point):
class TestClass {
[hashtable]$_data = #{}
[void]DefineData([object]$data) {
$this._data['Data'] = $data
$this | Add-Member -MemberType ScriptProperty -Name 'ScriptProperty' -Value {
#$this._data['Data'].GetType() | Out-Host
return $this._data['Data']
}
}
}
In the following code, there are 4 statements for assigning a value to $OriginalValue. Leave 3 of these statements commented and uncomment the one you want to try. When executed, the code should result in $ReturnValue containing the same value as $OriginalValue, but in the case of assigning $OriginalValue an instance of [BindingList[RandomType]], $ReturnValue is $null.
$ClassVar = [TestClass]::new()
$OriginalValue = [System.ComponentModel.BindingList[string]]::new()
#$OriginalValue = #{}
#$OriginalValue = [PSCustomObject]#{ Name = 'Value' }
#$OriginalValue = "Test String"
$OriginalValue.GetType()
$ClassVar.DefineData($OriginalValue)
$ReturnValue = $ClassVar.ScriptProperty
$ReturnValue.GetType()
Yes, I can hack my way around the problem by storing instances of [BindingList[RandomType]] in a [hashtable], but could someone explain what is going on, or even better yet, how to fix the code for all data types?
As explained in comments, the problem is not the BindingList but the output from your Script Block being enumerated. Since your BindingList has no elements when you call .DefineData($OriginalValue) then enumerating a list with no elements via .ScriptProperty results in null value:
(& { [System.ComponentModel.BindingList[string]]::new() }).GetType()
# Errors with:
# InvalidOperation: You cannot call a method on a null-valued expression.
A simple workaround is to wrap the output in a single element array before outputting, for this you can use the comma operator ,.
(& { , [System.ComponentModel.BindingList[string]]::new() }).GetType()
# Output type is preserved and the wrapping array is lost due to enumeration
So, your class method could look as follows considering the hashtable property is not needed:
class TestClass {
[void] DefineData([object] $data) {
$this.PSObject.Properties.Add(
[psscriptproperty]::new(
'ScriptProperty',
{ , $data }.GetNewClosure()
)
)
}
}

I am trying to convert the "Return First recurring character in a string problem" from python to powershell

I have completed coding this same problem in python and was trying to generate a similar logic or atleast achieve a similar result in powershell.
Python Code-
def FRC(str):
h = {}
for ch in str:
if ch in h:
return ch
else:
h[ch] = 0
return '\0'
print(FRC("abcdedcba"))
I have tried a few possible codes and was able to only enumerate the array of characters to count their occurrences. Thank you for any suggestions.
Update1 - The code I have worked on is as follows:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #()
$teststring = $teststring.ToCharArray()
foreach ($letter in $teststring)
{
if($letter -contains $hash){
return $letter
}else {
$hash = $hash + $letter
}
return "\0"
}
}
get-duplicatechar("saahsahh")
You could use the (.Net) HashSet class for this, which Add method (besides adding the value,) returns true if the element is added to the HashSet<T> object and false if the element is already present.
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$str
)
$h = [System.Collections.Generic.HashSet[char]]::new()
foreach ($ch in $str.ToCharArray()) {
if(!$h.add($ch)) { return $ch }
}
}
Here's a working version using your code as base:
function get-duplicatechar {
[CmdletBinding()]
param (
[string]$teststring
)
$hash = #{}
$CharArray = $teststring.ToCharArray()
foreach ($letter in $CharArray) {
if($letter -in $hash.Keys) {
$letter
break
}
else {
$hash[$letter] = $null
}
}
}
One problem is that you are strongly typing $teststring to be a string, so when you add a character array later PowerShell just converts it into a string and thus $teststring remains a string (try $teststring.GetType() after $teststring = $teststring.ToCharArray() to see this for yourself).
One way to solve this is to do what I did and use a different variable for the character array. You could also solve it by changing the variable to a character array directly by replacing [string]$teststring with [char[]]$teststring, that way any strings input to the function will be automatically cast as a character array.
The next mistake is using -contains where you need -in. The letter doesn't contain the array, you're looking for the letter in the array, just like you did in Python.
You can drop the return keyword entirely, PowerShell does not need it. Any output in your function will be output from the function automatically.
You also call your collection variable "hash", but you made an array. I changed it to be an actual hashtable, just like in your Python code. I also changed the way we add to it to more closely reflect what you did in Python. There are many ways to do this, this is just one. Notice we'll need to add ".Keys" in our if-statement as well so we check for keys matching our letter.
I think that's it, ask if anything is unclear.

How do I populate an unknown number of variables from user input dynamically in powershell?

I am trying to figure out how to populate an unknown number of variables based on user input (writing a script that obtains certificates from a CA, and sometimes these certificates contain more than one name (SANs) and it is impossible to know how many so this needs to be dynamic).
I know I start with setting up params like this:
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True)]
[string[]]$SANs
)
And then I need to somehow take those values and assign them to $san1, $san2, $san3 and so on.
Being new to programming, I am not even sure what to call this. Would you use a foreach loop to somehow populate these variables?
ForEach ($SAN in $SANs) {
what do I do here?
}
The end result is a need to populate a string with these variables like dns=$san1&dns=$san2&dns=$san3 etc...
Functions and scripts can take parameters. The parameter block in your example looked like...
function foo {
Param([string[]]$SANs)
}
That parameter, $SANs, is an array of strings. A single string would look like this...
$stuff = 'barf'
An array of strings looks like this...
$stuff = #('barf', 'toot', 'ruff', 'meow')
So far so good? If you need to get each of the things in the array, you'd use a loop...
foreach ($thing in $stuff) { write-output $thing }
...for example...
$san_declaration
foreach ($thing in $stuff) {
if ($san_declaration.length -eq 0) {
$san_declaration = "dns=${thing}"
} else {
$san_declaration += "&dns=${thing}"
}
}
Now, if you (not that you asked) happen to be calling Get-Certificate, just remember the SANs parameter is a string array. In that case, you'd just pass in the string array instead of creating the string like you were doing.
Get-Certificate -DnsName $stuff

Powershell: Turn period delimited string into object properties

I have a string that looks something like this:
$string = "property1.property2.property3"
And I have an object, we'll call $object. If I try to do $object.$string it doesn't interpret it that I want property3 of property2 of property1 of $object, it thinks I want $object."property1.property2.property3".
Obviously, using split('.') is where I need to be looking, but I don't know how to do it if I have an unknown amount of properties. I can't statically do:
$split = $string.split('.')
$object.$split[0].$split[1].$split[2]
That doesn't work because I don't know how many properties are going to be in the string. So how do I stitch it together off of n amounts of properties in the string?
A simple cheater way to do this would be to use Invoke-Expression. It will build the string and execute it in the same way as if you typed it yourself.
$string = "property1.property2.property3"
Invoke-Expression "`$object.$string"
You need to escape the first $ since we don't want that expanded at the same time as $string. Typical warning: Beware of malicious code execution when using Invoke-Expression since it can do anything you want it to.
In order to avoid this you would have to build a recursive function that would take the current position in the object and pass it the next breadcrumb.
Function Get-NestedObject{
param(
# The object we are going to return a propery from
$object,
# The property we are going to return
$property,
# The root object we are starting from.
$rootObject
)
# If the object passed is null then it means we are on the first pass so
# return the $property of the $rootObject.
if($object){
return $object.$property
} else {
return $rootObject.$property
}
}
# The property breadcrumbs
$string = '"Directory Mappings"."SSRS Reports"'
# sp
$delimetedString = $String.Split(".")
$nestedObject = $null
Foreach($breadCrumb in $delimetedString){
$nestedObject = Get-NestedObject $nestedObject $breadcrumb $settings
}
$nestedObject
There are some obvious places where that function could be hardened and documented better but that should give you an idea of what you could do.
What's the use case here? You can split the string as you've described. This will create an array, and you can count the number of elements in the array so that n is known.
$string = "property1.property2.property3"
$split = $string.split('.')
foreach($i in 0..($split.Count -1)){
Write-Host "Element $i is equal to $($split[$i])"
$myString += $split[$i]
}

how to copy(insert) hash reference to another hash reference in perl?

recently started doing perl. I'm reading old code and trying to rewrite some stuff. I've got a question here about hash references.
#declar anon hash ref that will be returned
my $store_hash = {};
foreach my $item (#list)
{
#this will iterate based on list
my $ret_hash = ops_getval($item, $user)
#do some magic here to map $ret_hash into $store_hash
}
ops_getval is a function that returns a type of ref hash. I want to insert those values into $store_hash. How should I approach this? Can I directly do
$store_hash = ops_getval($var1,$var2)
Much appreciated!
I think the standard way to do this is:
#$store_hash{ keys %$ret_hash } = values %$ret_hash;
This merges all of the hashes returned by all of the calls to ops_getval into $store_hash.
An alternate approach that might be clearer to the eye, possibly at the cost of a lot of redundant data copying:
%$store_hash = (%$store_hash, %$ret_hash);
You would do something like:
$store_hash->{$item} = $ret_hash
In general:
$hashref->{$key} = $value
See here for more: http://perldoc.perl.org/perlref.html#Using-References
To be clear, you can use a loop and get this done.
foreach ( keys%{ $ret_hash } ){
$store_hash->{ $_ } = $ret_hash->{ $_ } ;
}