Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PowerShell: modify elements of array

My cmdlet get-objects returns an array of MyObject with public properties:

public class MyObject{
    public string testString = "test";
}

I want users without programming skills to be able to modify public properties (like testString in this example) from all objects of the array. Then feed the modified array to my second cmdlet which saves the object to the database.

That means the syntax of the "editing code" must be as simple as possible.

It should look somewhat like this:

> get-objects | foreach{$_.testString = "newValue"} | set-objects

I know that this is not possible, because $_ just returns a copy of the element from the array.

So you'd need to acces the elements by index in a loop and then modify the property.This gets really quickly really complicated for people that are not familiar with programming.


Is there any "user-friendly" built-in way of doing this? It shouldn't be more "complex" than a simple foreach {property = value}

like image 838
Blauman Avatar asked Nov 30 '22 17:11

Blauman


1 Answers

I know that this is not possible, because $_ just returns a copy of the element from the array (https://social.technet.microsoft.com/forums/scriptcenter/en-US/a0a92149-d257-4751-8c2c-4c1622e78aa2/powershell-modifying-array-elements)

I think you're mis-intepreting the answer in that thread.

$_ is indeed a local copy of the value returned by whatever enumerator you're currently iterating over - but you can still return your modified copy of that value (as pointed out in the comments):

Get-Objects | ForEach-Object {
    # modify the current item
    $_.propertyname = "value"
    # drop the modified object back into the pipeline
    $_
} | Set-Objects

In (allegedly impossible) situations where you need to modify a stored array of objects, you can use the same technique to overwrite the array with the new values:

PS C:\> $myArray = 1,2,3,4,5
PS C:\> $myArray = $myArray |ForEach-Object {
>>>    $_ *= 10
>>>    $_
>>>}
>>>
PS C:\> $myArray
10
20
30
40
50

That means the syntax of the "editing code" must be as simple as possible.

Thankfully, PowerShell is very powerful in terms of introspection. You could implement a wrapper function that adds the $_; statement to the end of the loop body, in case the user forgets:

function Add-PsItem 
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory,ValueFromPipeline,ValueFromRemainingArguments)]
        [psobject[]]$InputObject,

        [Parameter(Mandatory)]
        [scriptblock]$Process
    )

    begin {

        $InputArray = @()

        # fetch the last statement in the scriptblock
        $EndBlock = $Process.Ast.EndBlock
        $LastStatement = $EndBlock.Statements[-1].Extent.Text.Trim()

        # check if the last statement is `$_`
        if($LastStatement -ne '$_'){
            # if not, add it
            $Process = [scriptblock]::Create('{0};$_' -f $Process.ToString())
        }
    }

    process {
        # collect all the input
        $InputArray += $InputObject
    }

    end {
        # pipe input to foreach-object with the new scriptblock
        $InputArray | ForEach-Object -Process $Process
    }
}

Now the users can do:

Get-Objects | Add-PsItem {$_.testString = "newValue"} | Set-Objects

The ValueFromRemainingArguments attribute also lets users supply input as unbounded parameter values:

PS C:\> Add-PsItem { $_ *= 10 } 1 2 3
10
20
30

This might be helpful if the user is not used to working with the pipeline

like image 150
Mathias R. Jessen Avatar answered Dec 06 '22 08:12

Mathias R. Jessen