Implement ForEach Iterator in Powershell - powershell

I have created a custom class in PowerShell which I would like to be able to access via the foreach command.
In PHP, I can do this by using implements iterator in my class declaration and implementing some variables and functions. Is there something similar in PowerShell? How do I let my PowerShell class be accessed by foreach?
Note: I'm using PowerShell Core

You can extend some IEnumerable implementation like this:
class Test : System.Collections.Generic.List[PSObject]
{
Test($items)
{
$items -split ';' |% { $this.Add($_) }
}
}
$test = New-Object Test('a;1;x;y;z;2;3;4')
foreach ($item in $test)
{
Write-Host $item
}
The result:
a
1
x
y
z
2
3
4

Related

How to extend and override Add in a collection class

Background
I have a data object in PowerShell with 4 properties, 3 of which are strings and the 4th a hashtable. I would like to arrange for a new type that is defined as a collection of this data object.
In this collection class, I wish to enforce a particular format that will make my code elsewhere in the module more convenient. Namely, I wish to override the add method with a new definition, such that unique combinations of the 3 string properties add the 4th property as a hashtable, while duplicates of the 3 string properties simply update the hashtable property of the already existing row with the new input hashtable.
This will allow me to abstract the expansion of the collection and ensure that when the Add method is called on it, it will retain my required format of hashtables grouped by unique combinations of the 3 string properties.
My idea was to create a class that extends a collection, and then override the add method.
Code so far
As a short description for my code below, there are 3 classes:
A data class for a namespace based on 3 string properties (which I can reuse in my script for other things).
A class specifically for adding an id property to this data class. This id is the key in a hashtable with values that are configuration parameters in the namespace of my object.
A 3rd class to handle a collection of these objects, where I can define the add method. This is where I am having my issue.
Using namespace System.Collections.Generic
Class Model_Namespace {
[string]$Unit
[string]$Date
[string]$Name
Model_Namespace([string]$unit, [string]$date, [string]$name) {
$this.Unit = $unit
$this.Date = $date
$this.Name = $name
}
}
Class Model_Config {
[Model_Namespace]$namespace
[Hashtable]$id
Model_Config([Model_Namespace]$namespace, [hashtable]$config) {
$this.namespace = $namespace
$this.id = $config
}
Model_Config([string]$unit, [string]$date, [string]$name, [hashtable]$config) {
$this.namespace = [Model_Namespace]::new($unit, $date, $name)
$this.id = $config
}
}
Class Collection_Configs {
$List = [List[Model_Config]]#()
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$u = $newConfig.Unit
$d = $newConfig.Date
$n = $newConfig.Name
$id = $newConfig.id
$checkNamespaceExists = $this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this.List | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }).id += $id
}
Else {
$this.List.add($newConfig)
}
}
}
Problem
I would like the class Collection_Configs to extend a built-in collection type and override the Add method. Like a generic List<> type, I could simply output the variable referencing my collection and automatically return the collection. This way, I won't need to dot into the List property to access the collection. In fact I wouldn't need the List property at all.
However, when I inherit from System.Array, I need to supply a fixed array size in the constructor. I'd like to avoid this, as my collection should be mutable. I tried inheriting from List, but I can't get the syntax to work; PowerShell throws a type not found error.
Is there a way to accomplish this?
Update
After mklement's helpful answer, I modified the last class as:
Using namespace System.Collections.ObjectModel
Class Collection_Configs : System.Collections.ObjectModel.Collection[Object]{
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$newConfigParams = $newConfig.namespace
$u = $newConfigParams.Unit
$d = $newConfigParams.Date
$n = $newConfigParams.Name
$id = $newConfig.id
$checkNamespaceExists = $this.namespace | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this | Where { $u -eq $_.namespace.Unit -and $d -eq $_.namespace.Date -and $n -eq $_.namespace.Name }).id += $id
}
Else {
([Collection[object]]$this).add($newConfig)
}
}
}
Which seems to work. In addition to the inheritance, had to do some other corrections regarding how I dotted into my input types, and I also needed to load the collection class separately after the other 2 classes as well as use the base class's add method in my else statement.
Going forward, I will have to do some other validation to ensure that a model_config type is entered. Currently the custom collection accepts any input, even though I auto-convert the add parameter to model_config, e.g.,
$config = [model_config]::new('a','b','c',#{'h'='t'})
$collection = [Collection_Configs]::new()
$collection.Add($config)
works, but
$collection.Add('test')
also works when it should fail validation. Perhaps it is not overriding correctly and using the base class's overload?
Last update
Everything seems to be working now. The last update to the class is:
using namespace System.Collections.ObjectModel
Class Collection_Configs : Collection[Model_Config]{
[void] Add ([Model_Config]$newConfig ){
$checkNamespaceExists = $null
$namespace = $newConfig.namespace
$u = $namespace.Unit
$d = $namespace.Date
$n = $namespace.Name
$id = $newConfig.id
$checkNamespaceExists = $this.namespace | Where { $u -eq $_.Unit -and $d -eq $_.Date -and $n -eq $_.Name }
If ($checkNamespaceExists){
($this | Where { $u -eq $_.namespace.Unit -and $d -eq $_.namespace.Date -and $n -eq $_.namespace.Name }).id += $id
}
Else {
[Collection[Model_Config]].GetMethod('Add').Invoke($this, [Model_Config[]]$newConfig)
}
}
}
Notice in the else statement that ....GetMethod('Add')... is necessary for Windows PowerShell, as pointed out in the footnote of mklement0's super useful and correct answer. If you are able to work with Core, then mklement0's syntax will work (I tested).
Also mentioned by mklement0, the types need to be loaded separately. FYI this can be done on the commandline for quick provisional testing by typing in the model_namespace and model_config classes and pressing enter before doing the same for Collection_Configs.
In summary this will create a custom collection type with custom methods in PowerShell.
It is possible to subclass System.Collections.Generic.List`1, as this simplified example, which derives from a list with [regex] elements, demonstrates:[1]
using namespace System.Collections.Generic
# Subclass System.Collections.Generic.List`1 with [regex] elements.
class Collection_Configs : List[regex] {
# Override the .Add() method.
# Note: You probably want to override .AddRange() too.
Add([regex] $item) {
Write-Verbose -Verbose 'Doing custom things...'
# Call the base-class method.
([List[regex]] $this).Add($item)
}
}
# Sample use.
$list = [Collection_Configs]::new()
$list.Add([regex] 'foo')
$list
However, as you note, it is recommended to derive custom collections from base class System.Collections.ObjectModel.Collection`1:
using namespace System.Collections.ObjectModel
# Subclass System.Collections.ObjectModel`1 with [regex] elements.
class Collection_Configs : Collection[regex] {
# Override the .Add() method.
# Note: Unlike with List`1, there is no .AddRange() method.
Add([regex] $item) {
Write-Verbose -Verbose 'Doing custom things...'
# Call the base-class method.
([Collection[regex]] $this).Add($item)
}
}
As for the pros and cons:
List`1 has more built-in functionality (methods) than ObjectModel`1, such as .Reverse(), Exists(), and .ForEach().
In the case of .ForEach() that actually works to the advantage of ObjectModel`1: not having such a method avoids a clash with PowerShell's intrinsic .ForEach() method.
Note that in either case it is important to use the specific type that your collection should be composed of as the generic type argument for the base class: [regex] in the example above, [Model_Config] in your real code (see next section).
If you use [object] instead, your collection won't be type-safe, because it'll have a void Add(object item) method that PowerShell will select whenever you call the .Add() method with an instance of a type that is not the desired type (or cannot be converted to it).
However, there's an additional challenge in your case:
As of PowerShell 7.3.1, because the generic type argument that determines the list element type is another custom class, that other class must unexpectedly be loaded beforehand, in a separate script, the script that defines the dependent Collection_Configs class.
This requirement is unfortunate, and at least conceptually related to the general (equally unfortunate) need to ensure that .NET types referenced in class definitions have been loaded before the enclosing script executes - see this post, whose accepted answer demonstrates workarounds.
However, given that all classes involved are part of the same script file in your case, a potential fix should be simpler than the one discussed in the linked post - see GitHub issue #18872.
[1] Note: There appears to be a bug in Windows PowerShell, where calling the base class' .Add() method fails if the generic type argument (element type) happens to be [pscustomobject] aka [psobject]: That is, while ([List[pscustomobject]] $this).Add($item) works as expected in PowerShell (Core) 7+, an error occurs in Windows PowerShell, which requires the following reflection-based workaround: [List[pscustomobject]].GetMethod('Add').Invoke($this, [object[]] $item)
There were a few issues with the original code:
The Using keyword was spelled incorrectly. It should be using.
The $List variable in the Collection_Configs class was not declared with a type. It should be [List[Model_Config]]$List.
The Add method in the Collection_Configs class was missing its return type. It should be [void] Add ([Model_Config]$newConfig).
The Add method was missing its opening curly brace.

Passing a Array of information to a separate script in powershell

I have a script that grabs a series of information from SQL. It then parses the information and passes it to a series of arrays. I want to then pass each array to a separate script.
I've seen Start-job should be able to do this but form my testing it didn't seem to work. This is what I have tried. Each Script individually works, and I am currently just using CVS's to pass the information.
Once the information is in the script I need to be able to call specific properties from each object. I did get it to just print the array as a string, but I couldn't call anything specific.
Invoke-Sqlcmd -Query $Q1 -ServerInstance $I -Database $DB | Export-Csv "$Files\Employees.csv"
$emps = Import-Csv "$Files\Employees.csv"
$newaccounts = #()
$deacaccounts = #()
$changedusers = #()
if(Test-Path -Path "$Files\Employees.csv"){
foreach ($emp in $emps) {
if ($emp.emp_num.trim() -ne $emp.EmpNum) {
$newaccounts += $emp
}
if ($emp.emp_num.trim() -eq $emp.EmpNum) {
if ($emp.fname -ne $emp.GivenName -and $emp.lname -ne $emp.SurName) {
$deacaccounts += $emp
$newaccounts += $emp
}
else ($emp.dept -ne $emp.DepartmentNumber -or $emp.job_title -ne $emp.JobTitle) {
$changedusers += $emp
}
}
}
}
Start-job -path "script" -argumentlist (,$deacaccounts)
Start-job -path "script" -argumentlist (,$changedusers)
Start-job -path "script" -argumentlist (,$newaccounts )
EDIT:
The Information passed to the scripts would be multiple lines of employee data. I need to be able to grab that info in the "Sub" scripts and perform actions based on them.
EX:
Deacaccounts =
fname
Lname
empnum
ted
kaz
1234
sam
cart
245
If you really need background jobs - it turns out that you don't - note that Start-Job doesn't have a -Path parameter; you'd have use -ScriptBlock { & "$script" } instead.
To simply invoke the script in the foreground, in sequence, use the following (script representing your .ps1 file path(s)):
& "script" $deacaccounts
& "script" $changedusers
& "script" $newaccounts
Note: &, the call operator, is only needed if the script / executable path is quoted and/or contains variable references (or subexpresions); e.g., a script with path c:\foo\bar.ps1 may be invoked without &; e.g.
c:\foo\bar.ps1 $deacaccounts
Note that your script(s) will receive a single argument each, containing an array of values.
If instead, you wanted to pass the array elements as individual (positional) arguments, you'd have to use splatting, where you use sigil # instead of $ to pass your variable (e.g.,
& "script" #deaccounts).
If you need to enumerate the arrays and pass each object individually as a parameter, use the following:
foreach ($obj in $deaccounts) { & "script" $obj }
foreach ($obj in $changedusers) { & "script" $obj }
foreach ($obj in $newaccounts) { & "script" $obj }
If each object should be splatted positionally based on its property values:
foreach ($obj in $deaccounts) {
$vals = $obj.psobject.Properties.Value
& "script" #vals
}
# ... ditto for $changeduser and $newaccounts
If each object should be splatted by property names, based on both property names and values, you need to convert each object to a hashtable first:
foreach ($obj in $deaccounts) {
$params = #{}
foreach ($prop in $obj.psobject.Properties) {
$params[$prop.Name] = $prop.Value
}
& "script" #params
}
# ... ditto for $changeduser and $newaccounts
As an aside: Incrementally extending arrays in a loop with += is inefficient, because a new array must be created behind the scenes in every iteration, because arrays are of fixed size.
In general, a much more efficient approach is to use a foreach loop as an expression and let PowerShell itself collect the outputs in an array: [array] $outputs = foreach (...) { ... } - see this answer.
In case you need to create arrays manually, e.g to create multiple ones, such as in your case, use an efficiently extensible list type, such as [System.Collections.Generic.List[object]] - see this answer.

Issue with Class Instances in Powershell

I have been working on a project in Powershell that leverages the Object Oriented nature of the language.
Here's what I am trying to do:
I have created several classes with distinct properties. We can call one of these classes 'ClassA'.
One of the properties of these classes is another class. We can call this 'ClassB'.
I have created several instances of each of these classes.
I need to set parameters of the class, which is a parameter of another class. So, for example: ClassAInstance.ClassBInstance.Property1
I am using a class method to update these parameters.
What i am finding is that when I set one of these lowest level properties, it is updating that parameter on all the instances of the class that previously exist.
What's weird is that the properties of ClassA instances (that are not type ClassB) get updated fine without affecting any existing ClassA instances. The issue is only with properties of ClassB instances, which are properties of ClassA instances.
Any idea why this might be happening?
I apologize for the lack of specifics. It's tough because the code has become pretty complicated, and there is also quite a bit of proprietary information contained within.
Class XBlock
{
[string]$Name
}
Class YBlock: XBlock
{
[string]$Name = 'Y1'
[float]$High_Scale = 100
}
Class XClass
{
[string]$Name = ''
[System.Collections.ArrayList]$CodeBlock
SetXParams()
{
$blocknames = New-Object System.Collections.Arraylist
for ($i = 1; $i -le $this.XBlocks; $i++)
{
$blocknames.add("X$i")
}
$blocknames | %{
$this.$_ = New-Object XBlock
$pattern = -join ('Attribute_Instance Name="', $_, '/High_Scale')
$match = $this.CodeBlock | Select-String -Pattern $pattern -Context 0, 2 -ErrorAction SilentlyContinue
If ($match.count -gt 0)
{
$this.$_.High_Scale = ($match[0].Context.PostContext[1]).split('=').split(' ')[7]
}
}
}
}
class YClass: XClass
{
[int]$YBlocks = 1
[YBlock]$Y1
}
Class XModule
{
[string]$Name = ''
[string]$Class = ''
[System.Collections.ArrayList]$CodeBlock
[int]$XBlocks = 0
[System.Collections.ArrayList]$CodeBlock
SetModuleParams([XModule]$XModule)
{
$this.Name = $XModule.Name
$this.CodeBlock = $XModule.CodeBlock
$this.ClassObject = ($Global:AllClasses | ? { $_.Name -eq $this.Class }).PSObject.Copy()
}
SetYParams()
{
$blocknames = New-Object System.Collections.Arraylist
for ($i = 1; $i -le $this.XBlocks; $i++)
{
$blocknames.add("Y$i")
}
$blocknames | %{
$this.$_ = New-Object YBlock
$this.$_ = $this.ClassObject.$_.PSObject.Copy()
$pattern = -join ('(Attribute Instance Name="', $_, '.*High_Scale)')
$match = $this.CodeBlock | Select-String -Pattern '(Attribute Instance Name=".*High_Scale)' -Context 0, 2 #-ErrorAction SilentlyContinue -SimpleMatch $true
If ($match.count -gt 0)
{
$this.$_.High_Scale = ($match[0].Context.PostContext[1]).split('=').split(' ')[7]
}
}
}
}
In object oriented programming when you copy an object, like you are doing in Y1 = ($this.ClassObject.Y1).PSObject.Copy() that doesn't actually copy the object, that only makes a copy of the object reference (the memory address of where the object actually resides). That is why when you update properties on your seemingly new "Y" object, then set properties, you are also changing the properties of the original object. They both point to the same place in memory.

Using a variables string value in variable name

It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}

Determine functions defined between two points in script

How would I be able to determine what functions are defined between two points of a script?
e.g.
function dontCare()
{}
# Start here
function A()
{}
function B()
{}
# Know that A and B have been defined
I was thinking about using Get_ChildItem function:* and taking the difference at the two points, but that wouldn't work if the functions are already defined.
You could parse the script, list all functions and ase you logic on the result.
$content = Get-Content .\script.ps1
$tokens = [System.Management.Automation.PSParser]::Tokenize($content,[ref]$null)
for($i=0; $i -lt $tokens.Count; $i++)
{
if($tokens[$i].Content -eq 'function')
{
$tokens[$i+1]
}
}
In v, you can also use the AST, see the ISE function explorer add-on by Ravi:
http://www.ravichaganti.com/blog/?p=2518
Not sure how that would work: if you define functions in script you should know you define them. Are you dot-sourcing other script?
If not, than this should get you there (even if A and B were defined before script runs):
# NewScript.ps1
function dontCare()
{}
# Start here
$Me = (Resolve-Path -Path $MyInvocation.MyCommand.Path).ProviderPath
$defined = ls function: |
where { $_.ScriptBlock.File -eq $Me } |
foreach { $_.Name }
function A()
{}
function B()
{}
# Know that A and B have been defined
ls function: |
where {
$_.ScriptBlock.File -eq $Me -and
$defined -notcontains $_.Name
} |
foreach { $_.Name }
# end of script body, trying it...
.\NewScript.ps1
A
B
In case you are dotsourcing a script, it gets even easier:
# NewScript2.ps1
function dontCare2()
{}
# Start here
$He = (Resolve-Path -Path .\NewScript.ps1).ProviderPath
. $He | Out-Null
ls function: |
where {
$_.ScriptBlock.File -eq $He
} |
foreach { $_.Name }
# end of script body, trying it...
.\NewScript2.ps1
A
B
dontCare
For me only dot-sourcing scenario makes some sense (you use outside source so can not be sure what it defines) but I assumed you may need both... ;)
You could do something like this:
$exists = get-command -erroraction silentlycontinue A
You could then branch your logic depending on the result.