32

I'm struggling a bit to remove the first line (item ID) of an array.

$test.GetType()

IsPublic IsSerial Name                                     BaseType                                                                                                      
-------- -------- ----                                     --------                                                                                                      
True     True     Object[]                                 System.Array

To list all the options I tried ,$test | gm and it clearly states:

Remove         Method                void IList.Remove(System.Object value)                                                                                              
RemoveAt       Method                void IList.RemoveAt(int index)

So when I try $test.RemoveAt(0) I get the error:

Exception calling "RemoveAt" with "1" argument(s): "Collection was of a fixed size."At line:1 char:1
+ $test.RemoveAt(1)
+ ~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : NotSupportedException

So I finally found here that my array needs to be of the type System.Object to be able to use $test.RemoveAt(0). Is it best practice to declare all the arrays in the beginning of the script as a list? Or is it better to convert the arrays with $collection = ({$test}.Invoke()) to a list later on when this functionality is needed?

What are the pro's and cons of both types? Thank you for your help.

DarkLite1
  • 13,637
  • 40
  • 117
  • 214

12 Answers12

59

An alternative option is to use Powershell's ability to assign multiple variables (see this other answer).

$arr = 1..5
$first, $rest= $arr

$rest
2
3
4
5

It's been a feature of Powershell for over a decade. I found this functionality from an MSDN blog post:

Prid
  • 1,272
  • 16
  • 20
Henry Ward
  • 644
  • 6
  • 6
  • 1
    This is the best solution by far! I'll be using this one from now on, thanks. – DarkLite1 Nov 19 '18 at 10:08
  • 2
    This method is unreliable if you want `$rest` to have a consistent data type. If the `$arr` has only 2 elements, `$rest` will be a scalar, otherwise array or even null – coldfix Mar 06 '19 at 01:23
  • 1
    Unless `$rest` is already an array type, in which case it will remain an array type. So `$first,$arr = $arr` should work just fine. You can also force the issue with: `$first,[object[]]$rest = $arr` – Willem M. Dec 24 '20 at 11:56
  • Or to simulate "shift left": `$null, $arr= $arr` – runec Dec 30 '20 at 09:57
  • This does not work if $arr has 0 or 1 element. `$arr=@(1); $first, $arr = $arr; $arr.gettype()` results $arr being null instead @(). It may not work if $arr has two elements, it results $arr being a scalar as comment #2 said. – puravidaso Sep 16 '21 at 14:31
33

Arrays are fixed-size, like the error says. RemoveAt() is an inherited method that doesn't apply to normal arrays. To remove the first entry in the array, you could overwrite the array by a copy that includes every item except the first, like this:

$arr = 1..5

$arr
1
2
3
4
5

$arr = $arr[1..($arr.Length-1)]

$arr
2
3
4
5

If you need to remove values at different indexes then you should consider using a List. It supports Add(), Remove() and RemoveAt():

#If you only have a specific type of objects, like int, string etc. then you should edit `[System.Object] to [System.String], [int] etc.
$list = [System.Collections.Generic.List[System.Object]](1..5)

$list
1
2
3
4
5

$list.RemoveAt(0)

$list
2
3
4
5

See my earlier SO answer and about_Arrays for more details about how arrays work.

Community
  • 1
  • 1
Frode F.
  • 52,376
  • 9
  • 98
  • 114
  • 2
    `$arr[1..($arr.Length-1)]` does not work for arrays of length 1 (one would expect it to return empty array, but it does not). Use this if that may be a problem: `$upperBound = (1, ($args.length-1) | measure-object -max).maximum; $userProvidedArgs = $args[1..$upperBound]` – t.animal Jun 14 '18 at 13:53
  • 1
    Good point. I would've tried `$arr[1..([math]::max(1,($arr.length-1)))]` – Frode F. Jun 14 '18 at 22:16
  • 3
    I suggest using `$arr[1..$arr.length]`, this is not only shorter but also works better in case `$arr` has only a single element. Then your method will not strip the element, this one does. – coldfix Mar 06 '19 at 01:25
  • this is a very interesting solution converting to list (easy)... I guess this would be a great solution co-working with a casting (when we know the definitive object we want) for further work with .net library code... – ZEE Oct 22 '20 at 10:21
22

You can use Select-Object -Skip <count> to omit the first count item(s):

PS C:\> 1..3 | Select-Object -Skip 1
2
3
PS C:\>

PS C:\> 1 | Select-Object -Skip 1
PS C:\>
Ron MacNeil
  • 590
  • 6
  • 9
  • Thank you, this is exactly what I was looking for. Using -Skip seems easier to read than messing with lengths and indices. – Tatiana Racheva Oct 25 '18 at 21:28
  • 1
    I really liked the suggestion because of the code clarity, but this approach drove me into some troubles: when the array has two objects, the result is not an array with one object, but the the last object remaining; when the array has only one object, the result is `$null`. – Zé Cláudio Feb 22 '19 at 17:03
  • 2
    Hi @ZéCláudio Powershell's default behaviour is to flatten single-item sequences to scalars and empty sequences to `$null`. To force an array in all cases, wrap the sequence in the [@(...) array subexpression operator](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-6#array-subexpression-operator--). – Ron MacNeil Mar 18 '19 at 21:58
19

This will allow you to remove every occurrence of an arbitrary element from an array without resorting to a more sophisticated .NET object.

$x=<array element to remove>
$test = $test | Where-Object { $_ -ne $x }

Alternatively,

$x=<array index to remove>
$test = $test | Where-Object { $_ -ne $test[$x] }

This will do the same, but will only remove one of the elements. If there are duplicates, they will remain. This can also be done via the index style as above.

$x=<array element to remove>
$skip=$true
$test = $test | ForEach-Object { if (($_ -eq $x) -and $skip) { $skip=$false } else { $_ } }

Also based on one of the comments, if you're not confident that $test is an array, it might be prudent to put $test=@($test) first.

  • Upvoted, this did the trick for me. And it's nice and concise. – Spikee Jul 11 '17 at 08:47
  • 1
    This can behave incorrectly if the array contains duplicate values. – Ron MacNeil Nov 16 '17 at 00:02
  • 1
    Pay attention: when only 1 element is left, the type is String instead of Object[]! The code can be fixed by adding this to the end of the example: `if ($test -is [string]) { # it's a string when it has only 1 item $test= ,$tes t# make it an array again (with only 1 item) } ` – Jan Sep 07 '20 at 14:18
  • Bravo! Easy and effective! – Oleh Tarasenko Feb 22 '21 at 17:26
5

Just to update - there's an issue with @Frode F. answer

If the number of elements in array is more than 1

$arr = $arr[1..($arr.Length-1)]

If the number of elements is 1, then this doesn't remove the element

if($arr.Length -le 1) {
    $arr = @()
}
else {
    $arr = $arr[1..($arr.length - 1)]
}
Community
  • 1
  • 1
RaceBase
  • 18,428
  • 47
  • 141
  • 202
3

I think it's going to depend on the circumstances. If you only need to remove that first element once, then you can use array slicing:

$arr = $arr[1..($arr.length-1)]

If you're going to do it repeatedly, then you should start with an arraylist or generic collection. If it's a large array, you might want to just put the expression that's creating it into a scriptblock and do an .invoke() on that rather than letting the pipeline create an array and then convert that to a collection.

mjolinor
  • 66,130
  • 7
  • 114
  • 135
2

removeat() works with arraylists (like $error):

[collections.arraylist]$a = get-process
$a.removeat(0)
js2010
  • 23,033
  • 6
  • 64
  • 66
0

Excuse the late answer, but I was struggling with this also. For my intents and purposes (writing to a text file), I realized that since the array was a fixed size -- instead of removing it I could just set the value to string.empty.

$results = SQLQuery -connectionString $connectionString  -query $query;
$results[0] = '';
foreach ($r in $results) {
    Add-Content $skus $r[0]; 
}

For me this got rid of the header that I didn't want in my flat file. Hope this helps someone else out there.

personaelit
  • 1,633
  • 3
  • 31
  • 59
0

If we have the case when big array(or ArrayList) must be performed by some parts - I used some lifehack:

#$bigArray with thousands of items
while($bigArray.Count -gt 0)
{
    if($bigArray.Count -gt 100)
    {
    $k = 100
    }else {$k = $bigArray.Count}
    $part = $bigArray | select -First $k
#now we can make some operations with this $part
#in the end of loop we should exclude this part from big array
    if($bigArray.Count -gt 100)
    {
        $bigArray = $bigArray | select -Last ($bigArray.Count - $k)
    }else {$bigArray = @()}#in this step we have been handle last part of array and we should null him for stop loop
}

And now we can handle big array by some parts(by 100 items)

Alexander Shapkin
  • 1,126
  • 1
  • 13
  • 11
0

Combination of other answers with my minor changes (using it as a global function from my $profile):

function global:sudo {
    $runArgs = $args

    if($runArgs.Length -le 1) {
        $runArgs = ""
    }
    else {
        $runArgs = $runArgs[1..($runArgs.length - 1)]
        $runArgs = $runArgs -join " "
    }
    
    Start-Process $args[0] -ArgumentList $runArgs -Verb runas    
}

So, both sudo notepad and sudo notepad c:\windows\abc.txt would work, and arguments should be passed gracefully (did not really check thoroughly though)

Dmitrii Chichuk
  • 674
  • 7
  • 15
0

If you want to delete a row by a known index x (base 0), one could also just copy the lower and the upper part of the array.

Delete row x from arry

$LastElement = $arry.GetUpperBound(0)

if ($LastElement -ge 0) {

    if ($x -eq 0) {
        $lowerRange = @()
    } else {
        $lowerRange = @($arry[0..($x-1)])
    }

    if ($x -eq $LastElement) {
        $upperRange = @()
    } else {
        $upperRange = @($arry[($x+1)..$LastElement])
    }

    $arry = @($lowerRange; $upperRange)
}
jamacoe
  • 519
  • 4
  • 16
0

There is a more simple and elegant method to do this. This answer also works with arrays that have a single element (unlike some of the other answers previously given). It also works at any desired index.

$arr = @(1,2,3)
$arr -ne $arr[0] #remove first element: @(2,3)
$arr -ne $arr[1] #remove second element: @(1,3)
$arr -ne $arr[-1] #remove last element: @(1,2)
DarkLite1
  • 13,637
  • 40
  • 117
  • 214
crisc2000
  • 1,082
  • 13
  • 19
  • 2
    Thank you for the addition but be careful with this as it will remove more than just the first element. This code `$arr = @(1,2,3,1); $arr = $arr -ne $arr[0]; Write-Host $arr` returns `@(2,3)` and not `@(2,3,1)` as expected. – DarkLite1 Oct 05 '21 at 06:20
  • ah, true. sorry I didn't check my answer with duplicate values. – crisc2000 Oct 05 '21 at 09:10