update hashtable in enumerator and have it recognize that add? - powershell

What is the best way to handle doing an add to a hashtable inside of enumeration of that hashtable so that it includes that added item in the enumeration?
I havo something lke:
foreach ($key in $groups.GetEnumerator() | Sort-Object Name -descending) {
if (something) {
groups.add("test","test2")
}
}
I want it to use the new added item in the enumeration instead of calling the function that has this in it over and over, which was killing my memory resources and cuasing issues with affecting what was getting written out to export-csv call.

I think powershell throws an error when you update a hash you are walking with foreach.
Write your hash's keys into an array:
$stack = #($groups.keys)
Then "shift" a key off that list to work it:
while ($stack) {
$item,$stack = $stack
DoStuffHere()
}
And finally, in DoStuffHere, you'll need to add an item to the HASH and push the new key onto the stack so it gets worked.
$groups.add($newkey,$newval)
$stack = $stack,$newkey
This way, you have no worries about powershell's automatic behavior not working the way you want.

Related

Is there a way to add a method to built-in/native powershell object (or type?/class¿)?

Is there a way to add a method to built-in/native powershell object (or type?/class¿)?
In particular, I'm looking at you [hashtable], but I suppose my question is also a general one... for instance, I could see wanting to add functionality to all my arrays...
For instance, I would like all my [hashtable] objects to have the method: .AddAndOverwrite(..) which would replace the value of the key if it exists; otherwise it creates a new key.
The only way I seem to be able to do this is to:
create an empty hashtable, $HashTableNew
add the ScriptMethod(s) to $HashTableNew (i.e. .AddAndOverwrite(..))
then when I use it, make a copy of $HashTableNew
$SomeOtherHashTable = $HashTableNew.PSObject.Copy()
This just seems like not "the way"...
Note: I will admit, this is not the best example use of a data type extension method (as #SantiagoSquarzon points out)... but it is a simple one, and it allows for a simple example in the accepted answer; so I'm intentionally leaving it as is, rather than changing question / the extension method to .foo() returning widgets...
There is indeed a better and easier way to update a type as a whole by using Update-TypeData.
Here is an example that add an .AddOrOverwrite method to the hashtable.
$TypeParam = #{
TypeName = 'System.Collections.Hashtable'
MemberType = 'ScriptMethod'
MemberName = 'AddOrOverwrite'
Value = { Param($Key, $Value) $this.$key = $Value }
}
Update-TypeData #TypeParam -Force
$SomeHashTable.AddOrOverwrite('aaa','2222222')
$this, in the scriptblock of the method definition, correspond to the object reference that is targeted, in this case, the hashtable.
-Force will overwrite the definition every time without error stating the type was already added.
That method is not super useful as it does something that the hashtable manage pretty well on its own by just using assignment but it demonstrates how to do it.
Bonus example
Here's an example on how you would apply this principle and create 2 script properties (readonly) for a string so you can convert to base 64 back and forth.
$TypeParam = #{
TypeName = 'System.String'
MemberType = 'ScriptProperty'
Force = $true
}
Update-TypeData #TypeParam -MemberName 'Base64' -Value { [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($this)) }
Update-TypeData #TypeParam -MemberName 'Base64Decoded' -Value { [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($this)) }
# Encode the string to base 64 (Output: U29tZVN0cmluZw==)
"SomeString".Base64
# Decode the string from Base64 (Output: SomeString)
"U29tZVN0cmluZw==".Base64Decoded
References
Msdocs - About-Types
Dr Scripto - Easily Update Powershell Type Data by Using a Cmdlet

Complex data types, byReference arguments and data not changing as expected

I have a function that can either initialize itself and return an ordered dictionary with initial values, or if a collection is provided as an argument, it manipulates that collection. And that collection is a key within a parent collection.
In my actual code I am seeing an odd behavior, where a key in the initial collection is initially $Null or has a specified value, but when I try to revise that value I do NOT get an error, but I also do not get a changed value. However, when I try creating a minimally functional example to post here, it does work correctly, both in the console and the IDE.
So, given this code
function Write-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
foreach ($key in $Collection.Keys) {
try {
$type = $Collection.$key.GetType().FullName
} catch {
$type = 'NULL'
}
Write-Host "$key $type $($Collection.$key)"
}
Write-Host
}
function Manage-Data {
param (
[System.Collections.Specialized.OrderedDictionary]$Collection
)
if (-not $Collection) {
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
return $initialCollection
} else {
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
}
}
CLS
$parentContainer = New-Object System.Collections.Specialized.OrderedDictionary
$parentContainer.Add('data', (Manage-Data))
Write-Data $parentContainer.data
Manage-Data -Collection $parentContainer.data
Write-Data $parentContainer.data
is there any obvious scenario where either of the lines revising values would not throw an error, but would also not change the value? For example, if there are actually more functions doing other things with that initialized collection object before the attempt to revise data? Or perhaps more generally, since I am depending on the default byReference behavior of complex objects, is there some situation where this behavior breaks down and I am effectively modifying a new complex object when I think I am modifying the original? Or is the fact that I am having problems with a simple data type within the complex type potentially the issue?
For what it is worth, the idea here is to basically be able to use Dependency Injection, but with functions rather than classes, and also mimic to some extent the the concept of a constructor and a method in a class, but again in a function. And that has generally been working well, if a little messy, which has reenforced in my mind that I need to move to classes eventually. But this particular issue has me worried that I will see the same problem in classes, and unless I can understand it now I will have issues. But since I can't seem to recreate the issue in a simplified example, I seem to be unable to figure anything out.
It occurs to me that one thing I haven't tried is to actually get the memory address of the collection I think I am modifying, or even of the individual key, so I can verify I actually am changing the same data that I initialized. But HOW to get the memory address of a variable escapes me, and is maybe not possible in PowerShell or .NET?
"the default byReference behavior of complex objects" concerns the properties of the object not the object itself:
The difference between (if):
[System.Collections.Specialized.OrderedDictionary]$initialCollection = [Ordered]#{
initialNull = $null
initialString = 'initial string'
}
and (else)
$Collection.initialNull = 'No longer null'
$Collection.initialString = 'New String'
Is that the later (else) statements indeed change the values of the parent values as $Collection refers to the same object as $parentContainer.data but the former (if) statement creates a new $initialCollection in the scope of the Manage-Data function which isn't visible in the parent (even if you assign it to $Collection, it would create a new object reference in the scope of the Manage-Data function).
You might return $initialCollection but then, how are you handling the different returns (either a $initialCollection or enumerable null ) in your parent function? Therefore I would just return $initialCollection for both conditions and reassign the object (where the properties are still by reference and only the $parentContainer.data reference will change/reset):
$parentContainer.data = Manage-Data -Collection $parentContainer.data
Potential problems
In other words, the potential issue in your Manage-Data function lies in the fact that parent function needs a different approach in calling it based on the condition if (-not $Collection) which is actually defined within the function. (What will be the value of this condition, as the caller already need to act differently on the condition?)
This leaves two pitfalls:
You call the function in the assumption that the argument is not a collection but it actually is:
$parentContainer = [Ordered]#{ data = [Ordered]#{} }
$parentContainer.Add('data', (Manage-Data))
In this case you get an error:
MethodInvocationException: Exception calling "Add" with "2" argument(s): "Item has already been added. Key in dictionary: 'data' Key being added: 'data'"
And the opposite (which is less obvious): you call the function in the assumption that the argument is a collection but it is actually not:
$parentContainer = [Ordered]#{}
Manage-Data $ParentContainer.Data
This will leave an unexpected object on the pipeline:
(See: PowerShell Pipeline Pollution)
Name Value
---- -----
initialNull
initialString initial string
And doesn't add anything to the $parentContainer object:
$parentContainer # doesn't return anything as it doesn't contain anything
Suggestions
See about scopes
Enable Set-StrictMode -Version Latest.
This will show you that a property is potentially empty.
Use ([ref]$MyVar).Value = 'new value' to replace a value in a parent scope.
(not related to the question) Use the IDictionary interface: [Collections.IDictionary]$Collection to accept a more general collection type in your functions.

Get index of object in Generic/List based on value of a property

I can populate a Generic.List with instances of my class, each with a unique value in the ID property, like this.
$packages = [System.Collections.Generic.List[object]]::New()
class Package {
# Properties
[String]$ID
# Constructor
Package ([String]$id) {
$this.ID = $id
}
}
foreach ($i in 65..90) {
$packages.Add([Package]::New("$([char]$i)"))
}
Now I want to get the index of a particular item, along the lines of $packages.IndexOf('C'). But doing this in C# seems to require the use of a lambda, and PowerShell doesn't seem to support that. I would rather not have to initialize my own index variable and iterate through all the items in the list checking their IDs and incrementing the index along the way. But, maybe this is that grey area between using PowerShell with only native PS cmdlets and just using C#, where you miss out on the sugar in both extremes and have to make your own?
You can use a [scriptblock] as a predicate:
$packages.FindIndex({$args[0].ID -eq 'C'})
# or
$packages.FindIndex({param($item) $item.ID -eq 'C'})
Assuming your package IDs are unique, consider using a dictionary instead of a list:
$packages = [System.Collections.Generic.Dictionary[string,Package]]::new([StringComparer]::InvariantCultureIgnoreCase)
class Package {
# Properties
[String]$ID
# Constructor
Package ([String]$id) {
$this.ID = $id
}
}
foreach ($i in 65..90) {
$ID = "$([char]$i)"
$packages.Add($ID, [Package]::new($ID))
}
Now you don't need to worry about the position of the item, anymore, now you can simply remove one by ID:
$wasRemoved = $packages.Remove("C")
Remove() returns $true when it successfully removes an entry, $false when the key wasn't found

Use Hashtable with switch case in PowerShell

I would need to make some decisions in PowerShell based on values in a hashtable.
Basically I need to assign users' UPN in Active Directory according to an attribute, the company one, and thought to create a hashtable containing keys and values like this
Key Value
Company1 #Company1.com
Company2 #Company2.com
The issue I am facing is I don't know how to tell PowerShell to use a value rather than another based on the company attribute, basically I don't know how to cycle/check the company attribute against the key in the hashtable.
I've tried to use switches like
$a -match $hashTable
or
$a -contains $hashtable
with little success.
I think a hashtable is what I need here, but I am of course open to any suggestion like using external files for the match as the number of the companies I need to match is rather high.
While Mathias already presented a proper solution I'd like to add some more explanation.
Hashtables and switch statements are essentially two different solutions to the same problem: transform an input value to a corresponding output.
A → "foo"
B → "bar"
C → "baz"
...
Hashtables are the simpler (and faster) approach, where you look up the result to the input value in a pre-defined table. Their advantage is that you can implement the associations in one place (e.g. a declaration section of your code where you define all your (static) data) and use them in a very simple manner anywhere else in your code:
$domains = #{
'Company1' = '#Company1.com'
'Company2' = '#Company2.com'
}
$name = 'foo'
$company = 'Company1'
$addr = $name + $domains[$company]
switch statements are more complicated in their handling, but also a lot more versatile. For instance, they allow different kinds of comparisons (wildcard, regular expression) in addition to simple lookups. They also allow providing default values/actions if a value is not listed. With hashtables you need to handle those cases with an additional if/else statement like Mathias showed, unless you're fine with the empty value that hashtables return when the lookup doesn't find a match.
$name = 'foo'
$company = 'Company1'
$domain = switch ($company) {
'Company1' { '#Company1.com' }
'Company2' { '#Company2.com' }
default { throw 'Unknown company' }
}
$addr = $name + $domain
-match and -contains are not "switches" but operators.
The -match operator takes a string as its left hand side argument and compares it to a regular expression pattern, which your $hashtable is a poor substitute for.
The -contains operator takes a collection (an array or list) as its left hand side argument and compares it to a scalar value on the right, to see if the collection, well contains the scalar value. Also not immediately applicable to your hashtable.
You can use the ContainsKey() method to test whether a given key exists, then use an indexer ($table[$key]) to extract the value:
$UPNSuffix = if($HashTable.ContainsKey($User.Company)){
# Company name exists in hashtable extract UPN suffix
$HashTable[$User.Company]
}
else {
# Not found, return default UPN suffix
"#default.company1.com"
}
Alternatively use the -contains operator on the Keys of the hashtable in place of ContainsKey():
$UPNSuffix = if($HashTable.Keys -contains $User.Company){
# Company name exists in hashtable extract UPN suffix
$HashTable[$User.Company]
}
else {
# Not found, return default UPN suffix
"#default.company1.com"
}

How can I call a method on multiple objects in PowerShell?

I am working with an XAML GUI created in VisualStudio, and I am trying to set up some of the code for the checkboxes. Instead of doing each one separately, I was hoping to do it dynamically since the functions are all the same.
I have searched, but was only able to find a reference to an Invoke-Method function that was written.
Below is what I have so far, just to explain better what I am trying to accomplish.
The Get-Variable line comes back with a half-dozen variables that I am trying to call the same method on.
$vars = Get-Variable WPFVarA*
Foreach ( $var in $vars ) {
$var.Add_Checked.Invoke({$Global:TicketCount++})
$var.Add_Unchecked.Invoke({$Global:TicketCount--})
}
If the checkboxes are wrapped, say with a stackpanel (could be anything) you can create a routed even handler to handle all events, like this (untested):
#Bubble up event handler
[Windows.RoutedEventHandler]$Script:CheckedEventHandler = {
$Global:TicketCount++
}
[Windows.RoutedEventHandler]$Script:UncheckedEventHandler = {
$Global:TicketCount--
}
$StackPanel.AddHandler([Windows.Controls.CheckBox]::CheckedEvent, $CheckedEventHandler)
$StackPanel.AddHandler([Windows.Controls.CheckBox]::UncheckedEvent, $UncheckedEventHandler)