1

I have a Powershell function in which I am trying to allow the user to add or remove items from a list by typing the word "add" or "remove" followed by a space-delimited list of items. I have an example below (slightly edited, so you can just drop the code into a powershell prompt to test it "live").

$Script:ServerList = @("Server01","Server02","Server03")
Function EditServerList (){
$Script:ServerList = $Script:ServerList |Sort -Unique
Write-host -ForegroundColor Green $Script:ServerList
$Inputs = $args
If ($Inputs[0] -eq "start"){
    $Edits =  Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
    #"# EditServerList $Edits
    #   EditServerList $Edits.split(' ')
    EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"})
    EditServerList start
} Elseif ($Inputs[0] -eq "add"){
    $Script:ServerList += $Inputs |where {$_ -NotLike $Inputs[0]}
    EditServerList start
} Elseif ($Inputs[0] -eq "remove"){
    $Script:ServerList = $Script:ServerList |Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})}
    EditServerList start
} Else {
    Write-Host -ForegroundColor Red "ERROR!"
    EditServerList start
}
}
EditServerList start

As you can see, the function takes in a list of arguments. The first argument is evaluated in the If/Then statements and then the rest of the arguments are treated as items to add or remove from the list.

I have tried a few different approaches to this, which you can see commented out in the first IF evaluation.

I have two problems.

  1. When I put in something like "add Server05 Server06" (without quotes) it works, but it also drops in the word "add".
  2. When I put in "remove Server02 Server03" (without quotes) it does not edit the array at all.

Can anybody point out where I'm going wrong, or suggest a better approach to this?

mklement0
  • 382,024
  • 64
  • 607
  • 775
JDDellGuy
  • 33
  • 4

2 Answers2

2

To address the title's generic question up front:

  • When you pass an array to a function (and nothing else), $Args receives a single argument containing the whole array, so you must use $Args[0] to access it.

    • There is a way to pass an array as individual arguments using splatting, but it requires an intermediate variable - see bottom.
  • To avoid confusion around such issues, formally declare your parameters.


Try the following:

$Script:ServerList = @("Server01", "Server02", "Server03")

Function EditServerList () {
  # Split the arguments, which are all contained in $Args[0],
  # into the command (1st token) and the remaining
  # elements (as an array).
  $Cmd, $Servers = $Args[0]
  If ($Cmd -eq "start"){
    While ($true) {
      Write-host -ForegroundColor Green $Script:ServerList
      $Edits =  Read-Host "Enter `"add`" or `"remove`" followed by a space-delimited list of server names"
      #"# Pass the array of whitespace-separated tokens to the recursive
      # invocation to perform the requested edit operation.
      EditServerList (-split $Edits)
    }
  } ElseIf ($Cmd -eq "add") {
    # Append the $Servers array to the list, weeding out duplicates and
    # keeping the list sorted.
    $Script:ServerList = $Script:ServerList + $Servers | Sort-Object -Unique
  } ElseIf ($Cmd -eq "remove") {
    # Remove all specified $Servers from the list.
    # Note that servers that don't exist in the list are quietly ignored.
    $Script:ServerList = $Script:ServerList | Where-Object { $_ -notin $Servers }
  } Else {
    Write-Host -ForegroundColor Red "ERROR!"
  }
}

EditServerList start
  • Note how a loop is used inside the "start" branch to avoid running out of stack space, which could happen if you keep recursing.

  • $Cmd, $Servers = $Args[0] destructures the array of arguments (contained in the one and only argument that was passed - see below) into the 1st token - (command string add or remove) and the array of the remaining arguments (server names).

    • Separating the arguments into command and server-name array up front simplifies the remaining code.

    • The $var1, $var2 = <array> technique to split the RHS into its first element - assigned as a scalar to $var1 - and the remaining elements - assigned as an array to $var2, is commonly called destructuring or unpacking; it is documented in Get-Help about_Assignment Operators, albeit without giving it such a name.

  • -split $Edits uses the convenient unary form of the -split operator to break the user input into an array of whitespace-separated token and passes that array to the recursive invocation.

    • Note that EditServerList (-split $Edits) passes a single argument that is an array - which is why $Args[0] must be used to access it.

    • Using PowerShell's -split operator (as opposed to .Split(' ')) has the added advantage of ignoring leading and trailing whitespace and ignoring multiple spaces between entries.
      In general, operator -split is preferable to the [string] type's .Split() method - see this answer of mine.

  • Not how containment operator -notin, which accepts an array as the RHS, is used in Where-Object { $_ -notin $Servers } in order to filter out values from the server list contained in $Servers.


As for what you tried:

  • EditServerList ($Edits.split(' ') |Where {$_ -NotLike "add","remove"}) (a) mistakenly attempts to remove the command name from the argument array, even though the recursive invocations require it, but (b) actually fails to do so, because the RHS of -like doesn't support arrays. (As an aside: since you're looking for exact strings, -eq would have been the better choice.)

  • Since you're passing the arguments as an array as the first and only argument, $Inputs[0] actually refers to the entire array (command name + server names), not just to its first element (the command name).

    • You got away with ($Inputs[0] -eq "add") - even though the entire array was compared - because the -eq operator performs array filtering if its LHS is an array, returning a sub-array of matching elements. Since add was among the elements, a 1-element sub-array was returned, which, in a Boolean context, is "truthy".

    • However, your attempt to weed out the command name with where {$_ -NotLike $Inputs[0]} then failed, and add was not removed - you'd actually have to compare to $Inputs[0][0] (sic).

  • Where {$_ -NotLike ($Inputs |Where {$_ -Notlike $Inputs[0]})} doesn't filter anything out for the following reasons:

    • ($Inputs |Where {$_ -Notlike $Inputs[0]}) always returns an empty array, because, the RHS of -Notlike is an array, which, as stated, doesn't work.
    • Therefore, the command is the equivalent of Where {$_ -NotLike @() } which returns $True for any scalar on the LHS.

Passing an array as individual arguments using splatting

Argument splatting (see Get-Help about_Splatting) works with arrays, too:

> function foo { $Args.Count } # function that outputs the argument count.

> foo @(1, 2)  # pass array
1  # single parameter, containing array

> $arr = @(1, 2); foo @arr # splatting: array elements are passed as indiv. args.
2

Note how an intermediate variable is required, and how it must be prefixed with @ rather than $ to perform the splatting.

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Hi mklement0, First, off, thanks for the very detailed answer. I appreciate the thought you put into your response. Can you help me understand a bit more what's going on with the following line you posted? $Cmd, $Servers = $Args[0] With two variables on the left of $Args[0], is the effect that $Cmd takes on $Args[0] and $Servers takes on the rest of the array? Meaning that if I were to put in the following... $Cmd1, $Cmd2, $Servers = $Args[0] ...that $Cmd1 would get $Args[0], $Cmd2 would get $Args[1], and $Servers woudl get whatever is left? – JDDellGuy Feb 21 '17 at 01:07
  • @JDDellGuy: My pleasure; you are correct about how the assignment works; please see my update, which added an explanation and a link to the documentation. – mklement0 Feb 21 '17 at 02:43
1

I'd use parameters to modify the ServerList, this way you can use a single line to both add and remove:

Function EditServerList {
    param(
        [Parameter(Mandatory=$true)]
        [string]$ServerList,
        [array]$add,
        [array]$remove
    )

    Write-Host -ForegroundColor Green "ServerList Contains: $ServerList"

    $Servers = $ServerList.split(' ')

    if ($add) {
        $Servers += $add.split(' ')
    }

    if ($remove) {
        $Servers = $Servers | Where-Object { $remove.split(' ') -notcontains $_ }
    }

    return $Servers
}

Then you can call the function like this:

EditServerList -ServerList "Server01 Server02 Server03" -remove "Server02 Server03" -add "Server09 Server10"

Which will return:

Server01
Server09
Server10
henrycarteruk
  • 12,708
  • 2
  • 36
  • 40