105

Given a list of items in PowerShell, how do I find the index of the current item from within a loop?

For example:

$letters = { 'A', 'B', 'C' }

$letters | % {
  # Can I easily get the index of $_ here?
}

The goal of all of this is that I want to output a collection using Format-Table and add an initial column with the index of the current item. This way people can interactively choose an item to select.

0

8 Answers 8

88

I am not sure it's possible with an "automatic" variable. You can always declare one for yourself and increment it:

$letters = { 'A', 'B', 'C' }
$letters | % {$counter = 0}{...;$counter++}

Or use a for loop instead...

for ($counter=0; $counter -lt $letters.Length; $counter++){...}
Sign up to request clarification or add additional context in comments.

7 Comments

Example: > "A,B,C,D,E,F,G" -split "," | % { $i = 0 } { if ( $i -gt 3 ) { $_ }; ++$i } Output: E F G
It worked like a charm when I had to rename files with a counter~ dir | % {$i = 46}{ move-item $_ ("ARM-{0:00000}.pdf" -f $i++)}
The $letters | % {$counter... option is safe even if $letters is only one object and not an array where as the for option doesn't handle that properly.
Thanks for this - I'd never seen that syntax with two sets of curly braces following foreach. It solved the problem of matching up two arrays like this: $selected = $true,$false,$true; @('first','second','third') | % {$i=0}{If($selected[$i]){$_};$i++ } to return 'first' and 'third'
It's a "shortcut" for foreach-object -Begin {first block} -Process {second block}. The first block is run once before processing, the second is run for each item to process. see get-help foreach-item -full for more info
|
77

.NET has some handy utility methods for this sort of thing in System.Array:

PS> $a = 'a','b','c'
PS> [array]::IndexOf($a, 'b')
1
PS> [array]::IndexOf($a, 'c')
2

Good points on the above approach in the comments. Besides "just" finding an index of an item in an array, given the context of the problem, this is probably more suitable:

$letters = { 'A', 'B', 'C' }
$letters | % {$i=0} {"Value:$_ Index:$i"; $i++}

Foreach (%) can have a Begin sciptblock that executes once. We set an index variable there and then we can reference it in the process scripblock where it gets incremented before exiting the scriptblock.

2 Comments

Also you probably don't want to look up each index of an array item while iterating those items. That'd be linear search for every item; sounds like making one iteration O(n^2) :-)
IndexOf works if your sure you always have an array but the $letters | % {$i=0}... method is safe even if $letters ends up being only one object and not an array.
56

For PowerShell 3.0 and later, there is one built in :)

foreach ($item in $array) {
    $array.IndexOf($item)
}

1 Comment

This is finding the index of the first item matching every item in the array, not iterating with an index value. This is slower, and if the array contains duplicate items you will get the wrong indexes.
18
0..($letters.count-1) | foreach { "Value: {0}, Index: {1}" -f $letters[$_],$_}

2 Comments

This is clean enough, but it's too bad there's no direct method that doesn't involve searching for the index in the array.
It's too bad Powershell doesn't expose an automatic variable that counts which iteration we're on.
8

I found Cédric Rup's answer very helpful but if (like me) you are confused by the '%' syntax/alias, here it is expanded out:

$letters = { 'A', 'B', 'C' }
$letters | ForEach-Object -Begin {$counter = 0} -Process {...;$counter++}

1 Comment

I think this syntax has the most clarity between all others
2

Also, you can use this pattern:

$letters | Select @{n="Index";e={$letters.IndexOf($_)}}, Property1, Property2, Property3

1 Comment

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
1

(PowerShell 7)

My solution is to create utility functions in profile script to make the call-site shorter.

I have two solution variants, solution 1 is more flexible and solution 2 is more succint. You may use one, or both!


Solution 1

function ConvertTo-ArrayIndexed {
    <#
        .SYNOPSIS
            Indexed variant of `ForEach-Object`.
        .PARAMETER Transform
            ScriptBlock function: ([Int]$Index, [Object]$Element) -> [Object]$TransformedElement
        .EXAMPLE
            'a', ('b','c'), 'd' | ConvertTo-ArrayIndexed {"Element #$($args[0])`: $($args[1]) | Type: $($args[1].getType())"}
            Element #0: a | Type: string
            Element #1: b c | Type: System.Object[]
            Element #2: d | Type: string
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][AllowNull()]
        [Object]$InputObject,
        [Parameter(Mandatory, Position = 0)]
        [ScriptBlock]$Transform
    )
    begin {
        $index = 0
    }
    process {
        . $Transform ($index++) $InputObject
    }
}

Example 1

"a", ("b","c"), "d" | %i{ param($i, $e) "Element $i is $e" }

Output:

Element 0 is a
Element 1 is b c
Element 2 is d

 


Solution 2

function ConvertTo-ArrayIndexed2 {
    <#
        .SYNOPSIS
            Indexed variant of `ForEach-Object`.
        .PARAMETER Transform
            ScriptBlock function with context: ([Int]$Index, [Object]$Element) -> [Object]$TransformedElement
            Two local variables are defined: `$i` for index, and `$_` for element.
        .EXAMPLE
            'a', ('b','c'), 'd' | ConvertTo-ArrayIndexed2 {"Element #$i`: $_ | Type: $($_.getType())"}
            Element #0: a | Type: string
            Element #1: b c | Type: System.Object[]
            Element #2: d | Type: string
        .EXAMPLE
            ConvertTo-ArrayIndexed2 -InputObject 'a',('b','c'),'d' {"Element #$i`: $_ | Type: $($_.getType())"}
            Element #0: a System.Object[] d | Type: System.Object[]
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)][AllowNull()]
        [Object]$InputObject,
        [Parameter(Mandatory, Position = 0)][ArgumentCompletions('{}')]
        [ScriptBlock]$Transform
    )
    begin {
        $index = 0
    }
    process {
        $variables = @([PSVariable]::new('i', $index++), [PSVariable]::new('_', $InputObject))
        $Transform.InvokeWithContext($null, $variables)
    }
}

Example 2

"a", ("b","c"), "d" | %i{ "Element $i is $_" }

Output:

Element 0 is a
Element 1 is b c
Element 2 is d

 


Notes

I added function aliases:

New-Alias -Name %i -Value ConvertTo-ArrayIndexed
New-Alias -Name mapIndexed -Value ConvertTo-ArrayIndexed

Modify the function and alias as you wish. I came from a background where I understand such a function as mapIndexed, but %i is more succinct and it goes well with the existing % alias for ForEach-Object.

Also note that "ForEach" is a "reserved verb", but if you don't mind I think an alias named ForEach-ObjectIndexed is pretty nice as well.

1 Comment

TIL that [scriptblock] can be a type of argument.
-4

For those coming here from Google like I did, later versions of Powershell have a $foreach automatic variable. You can find the "current" object with $foreach.Current

3 Comments

most people use $_ for the current object. Also, OP's question was about finding the index of the current object, not the object itself.
$_ doesn't work in a try-catch block, specifically if there's a catch, since then $_ or $PSItem become $error[0]. So $foreach.Current works perfectly to reference the actual item in that scenario.
Of course, you can assign $_ to another variable on entering the loop, if you're in the try-catch scenario, but it's nice not to have to bother.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.