1

I'm trying to emulate MATLAB's 'end' indexing keyword (such as A[5:end]) in Powershell but I don't want to type the array name (such as $array) to access $array.length for

$array[0..($array.length - 2)]

as discussed in another Stackflow question. I tried $this

(0..7)[4..($this.Length-1)]

given ($this.Length-1) seems to be interpreted as -1 as the output shows

4
3
2
1
0
7

This makes me think $this is empty when used inside [] indexing an array. Is there a way for an array to refer to itself without explicitly repeating the variable name so I can call the methods of the array to derive the indices? This would be very handy for emulating logical indexing while taking advantage of method chaining (like a.b.c.d[4..end]).

Hoi Wong
  • 63
  • 5
  • 2
    `$this` is `$null` in this context and `$null -1` = `-1` since Powershell will implictely convert '$null' to 0 to allow the operation to be performed. '$this' is an automatic variable that is only valid in a class context, where it refers to the instance of the class. – Sage Pourpre Mar 23 '22 at 07:28
  • 1
    You can refer to the last item of an array using `-1`. That being said, if you do `$Array[4..-1]`, you will start from the 5th element and go backward, ending with the last element in your sequence, thus `4,3,2,1,0,7` sequence. If you want the last 4 elements in order, what you could do is this `$Array[-4..-1]`. Now, you will get the last 4 elements of the arrays without using `$Array.Length.` [source](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_arrays?view=powershell-7.2) – Sage Pourpre Mar 23 '22 at 07:41
  • 1
    Thanks. $this was a long shot to see if I get lucky (if Microsoft designed it like operator() or operator[] in C++). I tried negative indexing and everything answered in the other stackflow question quoted. I'm just very unhappy that Microsoft chose to mix automatic descending range with negative (cyclic) indexing. – Hoi Wong Mar 23 '22 at 07:44
  • If I do `$Array[-4..-1]`, I'll have to infer the `-4` from the array length, which circles me back to the question of how to access the array length without explicitly writing out the array name. – Hoi Wong Mar 23 '22 at 07:54
  • 1
    Indeed. You are right. Does using `$Array.length` cause any problems or it is just an annoyance ? I lack the Matlab, logical indexing background related to the question. If it work but the sheer length of having to put it a lot of times in your code bug you, you could put it in a variable (eg: `$end = $Array.Length `) or, if the length constantly is moving around, in a variable scriptblock (eg: `$end = {$Array.Length}`) so you calculate the new length every time but access it through `$end` (eg: `$Array[4..(&$end)]`). Ultimately, it is all deriving from `$array.length`. – Sage Pourpre Mar 23 '22 at 08:12
  • 1
    As shown in the question above, Powershell support indexing into unnamed variables like (0..7), which I cannot get the array length without first assigning it to a variable first. This is a huge annoyance when I'm cascading a few dot operators for a long one-liner and now I have to break it into 2~3 pieces. – Hoi Wong Mar 23 '22 at 08:21
  • MATLAB's logical indexing is simply using a boolean array to pick out the items you want based on conditions, like A[A>5] will pick out all elements in A larger than 5. I know there's Select in Powershell, but conceptually (and visually) it's a lot more concise if I can perform filtering (in higher-order-function or functional programming sense) with logical/boolean indexing. – Hoi Wong Mar 23 '22 at 08:35
  • I'm also worried about timing problems when the array change length in real time (such as `Get-ChildItem`). I don't even know if `$array[0..($array.length - 2)]` is safe from race conditions. The only safe way I can think of is to take a snapshot of the object and acquire the `.length` off the snapshot. – Hoi Wong Mar 23 '22 at 08:50

2 Answers2

2

PowerShell doesn't have any facility for referring to "the collection targeted by this index access operator", but if you want to skip the first N items of a collection/enumerable you can use Select -Skip:

0..7 |Select -Skip 4
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • 1
    Thanks for concisely describing my question in the proper powershell lingo! Now I know the answer is no. `select -skip` gets messy quickly when indexing with multi-segment ranges such as `(0..7)[(1..3)+(2..7)]` – Hoi Wong Mar 23 '22 at 21:03
1

To complement Mathias' helpful answer:

  • The automatic $this variable is not available inside index expressions ([...]), only in custom classes (to refer to the instance at hand) and in script blocks acting as .NET event delegates (to refer to the event sender).

  • However, for what you're trying to achieve you don't need a reference to the input array (collection) as a whole: instead, an abstract notation for referring to indices relative to the end of the input array should suffice, and ideally also for "all remaining elements" logic.

You can use negative indices to refer to indices relative to the end of the input array, but that only works with individual indices:

# OK: individual negative indices; get the last and the 3rd last item:
('a', 'b', 'c', 'd')[-3, -1] # -> 'b', 'd'

Unfortunately, because .. inside an index expression refers to the independent, general-purpose range operator, this does not work for range-based array slicing when negative indices are used as range endpoints:

# !! DOES NOT WORK: 
# *Flawed* attempt to get all elements up to and including the 2nd last,
# i.e. to get all elements but the last.
# 0..-2 evaluates to array 0, -1, -2, whose elements then serve as the indices.
('a', 'b', 'c', 'd')[0..-2] # -> !! 'a', 'd', 'c'

That is, the general range operation 0..-2 evaluates to array 0, -1, -2, and the resulting indices are used to extract the elements.

It is this behavior that currently requires an - inconvenient - explicit reference to the array inside the index expression for everything-except-the-last-N-elements logic, such as $array[0..($array.length - 2)] in your question in order to extract all elements except the last one.

GitHub issue #7940 proposes introducing new syntax that addresses this problem, by effectively implementing C#-style ranges:

While no syntax has been agreed on and no commitment has been made to implement this enhancement, borrowing C#'s syntax directly is an option:

Now Potential future syntax Comment
$arr[1..($arr.Length-2)] $arr[1..^1] From the 2nd el. through to the next to last.
$arr[1..($arr.Length-1)] $arr[1..] Everything from the 2nd el.
$arr[0..9] $arr[..9] Everything up to the 10th el.
$arr[-9..-1] $arr[^9..] Everything from the 9th to last el.

Note the logic of the from-the-end, 1-based index syntax (e.g., ^1 refers to the last element) when serving as a range endpoint: It is up-to-but-excluding logic, so that ..^1 means: up to the index before the last one, i.e. the second to last one.


As for the workarounds:

Using Select-Object with -Skip / -SkipLast is convenient in simple cases, but:

  • performs poorly compared to index expressions ([...]) (see below)
  • lacks the flexibility of the latter[1]
    • A notable limitation is that you cannot use both -Skip and -SkipLast in a single Select-Object call; GitHub issue #11752 proposes removing this limitation.

E.g., in the following example (which complements Mathias's -Skip example), which extracts all elements but the last:

# Get all elements but the last.
$arr | Select-Object -SkipLast 1
  • Array $arr is enumerated, i.e. its elements are sent one by one through the pipeline, a process known as streaming.

  • When captured, the streamed elements are collected in a regular, [object[]]-typed PowerShell array, even if the input array is strongly typed - however, this loss of strict typing also applies to extracting multiple elements via [...].

Depending on the size of your arrays and the number of slicing operations needed, the performance difference can be significant.


[1] Notably, you can use arbitrary expressions inside [...], which is discussed in more detail in this answer.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you for adding the details answering why the answer is a hard 'no' and also pointing me to where the mob with pitchforks (jokingly of course) are so I can join them (lol)! None of the known workarounds are pleasant though, either it makes the code clumsy (especially when indexing with multiple segments), run into potential timing problems (stale length counts) or performance issues (piping). – Hoi Wong Mar 23 '22 at 22:54