79

Powershell's array notation has rather bizarre, albeit documented, behavior for slicing the end of arrays. This section from the official documentation sums up the bizarreness rather well:

Negative numbers count from the end of the array. For example, "-1" refers to the last element of the array. To display the last three elements of the array, type:

$a[-3..-1]

However, be cautious when using this notation.

$a[0..-2]

This command does not refer to all the elements of the array, except for the last one. It refers to the first, last, and second-to-last elements in the array.

The following code confirms the bizarreness:

$a = 0,1,2,3
$a[1..-1]

Which indeed outputs this bizarre result:

1
0
3

So, the question is, what is the idiomatic way to slice with one index relative the start and another relative the end of the array?

Please tell me it's something better than this ugly mess:

$a[1..($a.Count-1)]

Edit:

Another way to describe what I'm looking for is this: The idiomatic Powershell equivalent of this python expression:

a=1,2,3,4
a[1:-1]

Which, of course, evaluates to (2,3)

4
  • 1
    I believe what one considers bizarre or strange may appear completely normal to another. Case in point, I read through your snippets without reading your rationale -- just to see how I naturally interpret the expressions -- and the way Powershell behaves seems to be aligned with what I expected. Just saying -- that opinions will differ. Invariably. Commented May 13, 2020 at 10:14
  • 2
    @amn Your comment prompted me to reread this as well. Indeed the way PowerShell behaves aligns with what I now expect. Apparently my opinion differs even with whomever I was five years ago.
    – alx9r
    Commented May 13, 2020 at 13:31
  • 1
    I know how you feel, I think. Powershell parser was throwing curveball after curveball at me, as if to tell me I am never going to understand how exactly Powershell works, but at least I beat it, or so it feels. There are still quirks I am a bit unsure about -- array literals (yes, I have read this piece), to name one -- but the parser seems more or less digested. And yeah, old opinions -- gee, I often cringe at my earlier questions, answers and comments :/ Here is hoping this won't be one of them! Commented May 14, 2020 at 10:24
  • 3
    I agree with Old OP, this is NOT a slicing operator and it's a bizarre result. But unlike old me I know that ".." is a range generator so it does make sense. I'm just disappointed that Powershell doesn't have an array slice operator. Commented Dec 7, 2021 at 13:18

10 Answers 10

76

If you want to get n elements from the end of an array simply fetch the elements from -n to -1:

PS C:\> $a = 0,1,2,3
PS C:\> $n = 2
PS C:\> $a[-$n..-1]
2
3

Edit: PowerShell doesn't support indexing relative to both beginning and end of the array, because of the way $a[$i..$j] works. In a Python expression a[i:j] you specify i and j as the first and last index respectively. However, in a PowerShell .. is the range operator, which generates a sequence of numbers. In an expression $a[$i..$j] the interpreter first evaluates $i..$j to a list of integers, and then the list is used to retrieve the array elements on these indexes:

PS C:\> $a = 0,1,2,3
PS C:\> $i = 1; $j = -1
PS C:\> $index = $i..$j
PS C:\> $index
1
0
-1
PS C:\> $a[$index]
1
0
3

If you need to emulate Python's behavior, you must use a subexpression:

PS C:\> $a = 0,1,2,3
PS C:\> $i = 1; $j = -1
PS C:\> $a[$i..($a.Length+$j-1)]
1
2
5
  • Hmm...this seems to just be indexing relative the end of the array (ie. same idea as the first example in the documentation $a[-3..-1]). What I'm really looking for is an idiomatic way to index from both beginning and end of an array in once slice. I have edited my question to hopefully make this more clear.
    – alx9r
    Commented Feb 11, 2015 at 18:16
  • 1
    @alx9r See updated answer. : in Python and .. in PowerShell do fundamentally different things, so AFAICS you're out of luck. Commented Feb 11, 2015 at 18:43
  • @matt That composition doesn't work if the array isn't sorted.
    – alx9r
    Commented Feb 11, 2015 at 20:20
  • @AnsgarWiechers Thanks for the precise answer. It's all clear now. 0..-2 evaluates to 0,-1,-2 which is completely consistent with the documented bizarre slicing behavior.
    – alx9r
    Commented Feb 11, 2015 at 20:24
  • If you need from the beginning to some near end index, I think $a[-$a.Count..-2] is worth a mention. This also offers an alternative for emulating Python's behavior at the end: $a[(-$a.Count+$i)..($j-1)]. I think that's a tad more intuitive to look at since it has a simple operation at each end, but that may be a matter of opinion.
    – jpmc26
    Commented Mar 9, 2017 at 0:29
30

Although not as neat as you might want but is cleaner in the way PowerShell works ...

@( 1,2,3,4 | Select-Object -Skip 1 )

returns ...

2
3
4

Surrounding the whole expression with @(...) eliminates the unwrapping of 1-element arrays, so that :

@( 1,2 | Select-Object -Skip 1 ) -is [Array]

returns

True
3
  • Can't use this approach if you're working with typed arrays. Select-Object will return an Object[] so if you work with an interface that requires a specific type it'll fail. ([int[]] @(1,2,3) | Select-Object -Skip 1).GetType()
    – RiverHeart
    Commented Nov 15, 2020 at 3:15
  • If the resulting length is 1 item, you get the bare item instead of an array of length 1! Means that subsequent code has to check what happened and can't just say a[0]. Commented Sep 2, 2021 at 8:10
  • @RiverHeart Select works fine, you just have to cast the result (though it isn't elegant or concise): ([int[]]([int[]] @(1,2,3) | Select-Object -Skip 1 | Select-Object -SkipLast 1)).GetType()
    – brianary
    Commented Dec 5, 2022 at 19:26
7

Combine Select-Object -Skip and Select-Object -SkipLast like:

$a = 0,1,2,3
$a | Select-Object -Skip 1 | Select-Object -SkipLast 1

Returns:

1
2

Not as elegant as Python, but at least you don't have to use Count or Length, meaning this also works if the array isn't stored in a variable.

4

This could be the most idiomatic way to slice an array with both of its ends:

$array[start..stop] where stop is defined by taking the length of the array minus a value to offset from the end of the array:

$a = 1,2,3,4,5,6,7,8,9
$start = 2
$stop = $a.Length-3
$a[$start..$stop]

This will return 3 4 5 6 7

The start value starts counting with zero, so a start value of '2' gives you the third element of the array. The stop value is calculated with ($a.Length-3), this will drop the last two values because $a.Length-3 itself is included in the slice.

I have defined $start and $stop for clarity, obviously you can also write it like this:

$a = 1,2,3,4,5,6,7,8,9
$a[2..($a.Length-3)]

This will also return 3 4 5 6 7

3
$arr = @(10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

$arr | Select-Object -First 5 | Select-Object -Index (@(0..4) | Where-Object { $_ % 2 -eq 0}) 
$arr | Select-Object -Last 5
$arr | Select-Object -Unique
$arr | Sort-Object | Select-Object -Unique
$arr | Where-Object {  $_ % 5 -eq 0 } | Sort-Object | Select-Object -Unique
$arr | Select-Object -First ($arr.Count - 3)

Actually code speaks for itself. I event don't need to explain.

However,

  1. Provide the first five elements, but each second of those five. Equal to arr[:5:2] in Python
  2. Get the last five elements.
  3. Gives unique elements
  4. Firstly sort and then provide unique
  5. Gives only elements which equal 0 by applying modulo of 5, sort, unique.
  6. Provide the first count of elements in that array minus three elements only.
1
  • An explanation would be useful here.
    – Rastalamm
    Commented Aug 26, 2019 at 14:19
2

If you are looking for, say, the first three and last three elements in an array, with the results in an array, a little array addition will take care of the need.

[array]$A = (([int][char]'A')..([int][char]'Z')) | ForEach-Object {[char]$_}
$B = $A[0..2]+$A[-3..-1]
Clear-Host
Write-Host "Original List"
Write-Host $A -NoNewline -Separator ', '
Write-Host
Write-Host "First three and last three"
Write-Host $B -NoNewline -Separator ', '

Yields:

Original List
A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z
First three and last three
A, B, C, X, Y, Z
1

I believe this is the right way to do it. all other methods require more code.

$a[1..($a.Count-1)]

Also, if array is converted to string it becomes easy to get data as below:

$a = 0,1,2,3
[string]::Concat($a).Substring(1)
1
  • Thanks a lottt. Only this method worked for slicing ($_.publisher -split ',')[0] inside a get-appxpackage | select-object { ($_.publisher -split ',')[0] }. When you can't access the name itself, say when it's an expression like here it was, the other method of $<name>[ <start>..($<name>.length) ] fails miserably on its head. Commented Jul 3 at 6:48
1

Unfortunately piping your array to Select-Object -Skip $skipStart | Select-Object -SkipLast $skipEnd is the only foolproof idiomatic way to get the correct items out.

(@wensveen got there first with this strategy, but SO won't let me comment yet, and I think giving an explanation warrants the detail of a full reply.)

Calculating ranges doesn't work

Ranging from $a[3..$a.count-3] and so on doesn't work if your list is shorter than you expect: supposing $a has 4 items, the range you'd end up with is 3..1, i.e. @(3, 2, 1), three items in reverse order, whereas in the Python example a[3:-3] would return zero elements, because the ascending range that starts three from the start and ends three from the end is empty. (In Python, range order is an additional explicit parameter, a[-1:1:-1] permits a reverse-ordered result.)

Output format

As per usual, if you need to force the output to be an array, you can wrap the pipeline in a @(...) array coercion. @RiverHeart mentioned wanting to preserve the input array type e.g. Int32[], but as far as I can tell that doesn't work with ordinary indexing either, I might be missing something.

Getting the right items

Another brief mention: the numbers you use are sliiightly different for the skip operation than the range notation. If you're starting at index 1, you're also skipping 1 item from the start in a zero-indexed array, so that one's the same; but skipping 1 at the end is equivalent to index -2, or equally $a.count-2, for the second-last element.

Adding array indexes

Finally, adding range literals together is a nice feature. It doesn't generalise for the reasons above, it won't stop you from accidentally getting the same item twice, say, if your ranges overlap, but it's nice to be able to go $a[0..2+-3..-1] at an interactive prompt to get the first two and last three, more succinct than making and combining separate arrays and still quite clear. I guess if you want to thumb your nose at Python, that's one thing it doesn't do quite as simply!

0

For the case of shifting a parameter or two:

$__, $rest = $a

will result in $rest being $a[1..($a.Count-1)]

Except of course when $a has length 2.

0

PowerShell ignores most index out of bounds issues when using the [] operator so you can slightly shorten the statement in most cases, e.g.:

$a[1..$a.Count]

This will actually attempt to go one past the last element, but PowerShell doesn't care and the performance hit is negligible. It will generate an error if you hit the unlikely edge case where your array/collection is exactly its maximum allowed size.

If you know a reasonable upper limit on the collection length, you can shorten further by using that instead, e.g. $a[1..10]. Performance issues are only noticeable if the upper limit gets really big.

Whether this is a "good" idea is debatable, but it is shorter and knowing it may answer some "why does this script work with these errors?" type questions.

Not the answer you're looking for? Browse other questions tagged or ask your own question.