0

I am working with PowerShell 4.0 and I am trying to pass a string array as one of the parameters for an Invoke-Command -ScriptBlock in which I am calling another PowerShell script on a remote server. When I do this, the string array seems to get flattened so that it appears as a single string value, rather than a string array.

Listed below is the 1st script, which is being called by a Bamboo deployment server that provides the initial parameters.

In the Debug section, the $SupportFolders string array is iterated by the FlowerBoxArrayText function and it properly writes the two folder paths to the console, as expected.

24-Oct-2017 14:59:33    *****************************************************************************
24-Oct-2017 14:59:33    **** E:\SRSFiles\SRSOutput
24-Oct-2017 14:59:33    **** E:\SRSFiles\SRSBad
24-Oct-2017 14:59:33    *****************************************************************************

Here is the initial part of the 1st script file, showing the input parameters, the string array creation and where I am calling the remote script via Invoke-Command;

 [CmdletBinding(DefaultParametersetName='None')]
 param (
    # Allows you to specify Install, Delete or Check.
    [ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
    # Allows you to specify the remote server name.
    [string] $ComputerName = "None",
    # Allows you to specify the username to use for installing the service.
    [string] $Username = "None",
    # Allows you to specify the password to use for installing the service.
    [string] $Password = "None",
    # Allows you to specify the location of the support folders for the service, if used. 
    [string] $SupportFoldersRoot = "None"    
)

Function CreateCredential() 
{
    $Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
    $Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass) 
    Return $Cred
}

Function FlowerBoxArrayText($TextArray, $TextColor="Yellow")
{
    Write-Host "*****************************************************************************" -ForegroundColor $TextColor
    foreach($TextLine in $TextArray) 
    {
        IndentedText $TextLine $TextColor
    }
    Write-Host "*****************************************************************************" -ForegroundColor $TextColor
}


Function IndentedText($TextToInsert, $TextColor="Yellow")
{
    Write-Host "**** $TextToInsert" -ForegroundColor $TextColor
}



$Credential = CreateCredential
[string[]] $ResultMessage = @()
[string] $Root = $SupportFoldersRoot.TrimEnd("/", "\")
[string[]] $SupportFolders = @("$Root\SRSOutput", "$Root\SRSBad")

#Debug
Write-Host "**** Starting debug in ManageAutoSignatureProcessorService ****"
FlowerBoxArrayText $SupportFolders -TextColor "Green"
Write-Host "**** Ending debug in ManageAutoSignatureProcessorService ****"
#End Debug

$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock {
    param($_action,$_username,$_password,$_supportFolders) &"C:\Services\ManageService.ps1" `
    -Action $_action `
    -ComputerName DEV `
    -Name DevProcessor `
    -DisplayName 'DevProcessor' `
    -Description 'DevProcessor' `
    -BinaryPathName C:\Services\DevProcessor.exe `
    -StartupType Manual `
    -Username $_username `
    -Password $_password `
    -ServicePathName C:\Services `
    -SupportFolders $_supportFolders `
    -NonInteractive } -ArgumentList $Action,$Username,$Password,(,$SupportFolders)

if ($ResultMessage -like '*[ERROR]*') 
{
    FlowerBoxArrayText $ResultMessage -textColor "Red"
} 
else 
{
    FlowerBoxArrayText $ResultMessage -textColor "Green"
}

Then, in the ManageService.ps1 script file on the remote server, I have the following;

[CmdletBinding(DefaultParametersetName='None')]
    param (
    # Allows you to specify Install, Delete or Check.
    [ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
    # Allows you to specify the name of the remote computer.
    [string] $ComputerName = "None",
    # Allows you to specify the service name.
    [string] $Name = "None",
    # Allows you to specify the service display name.
    [string] $DisplayName = "None",
    # Allows you to specify the service description.
    [string] $Description = "None",
    # Allows you to specify the path to the binary service executable file.
    [string] $BinaryPathName = "None",
    # Allows you to specify how the service will start, either manual or automatic.
    [ValidateSet("Manual", "Automatic")][string] $StartupType = "Manual",
    # Allows you to specify the domain username that the service will run under.
    [string] $Username = "None",
    # Allows you to specify the password for the domain username that the service will run under.
    [string] $Password = "None",
    # Allows you to specify the path to the service install scripts and service files on the remote server.
    [string] $ServicePathName = "None",  
    # Allows you to specify the location of the support folders for the service, if used. The default value is an empty array
    [string[]] $SupportFolders = @(),    
    # Disables human interaction, and allows all tests to be run even if they 'fail'.
    [switch] $NonInteractive
)


Function CreateCredential() 
{
    $Pass = $Password | ConvertTo-SecureString -AsPlainText -Force
    $Cred = New-Object System.Management.Automation.PSCredential($Username, $Pass) 
    Return $Cred
}


[bool] $OkToInstall = $False
[string[]] $ResultMessage = @()

#Debug
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."

foreach ($Folder in $SupportFolders) 
{
    $ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Item: $Folder."
}
$Count = @($SupportFolders).Count
$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders Count: $Count ."
$ResultMessage = $ResultMessage += "[DEBUG] ***************************************"
#End Debug

The line,

$ResultMessage = $ResultMessage += "[DEBUG] SupportFolders: [$SupportFolders] ."

shows the following result from the $ResultMessage value that is returned to the calling script;

**** [DEBUG] SupportFolders: [E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad] .

Notice that the array is flattened out.

The foreach loop that follows also only prints out one value instead of two;

"E:\SRSFiles\SRSOutput E:\SRSFiles\SRSBad"

I have spent considerable time researching a solution but have yet to find an answer.

Any ideas?

EDIT 1 using @Bacon Bits suggestion;

$Options = @{'Action' = $Action
        'ComputerName' = 'DEV'
        'Name' = 'DevProcessor'
        'DisplayName' = 'DevProcessor'
        'Description' = 'Generate daily processes'
        'BinaryPathName' = 'C:\Services\DevProcessor\DevProcessor.exe'
        'StartupType' = 'Manual'
        'Username' = $Username
        'Password' = $Password
        'ServicePathName' = 'C:\Services\DevProcessor'
        'SupportFolders' = $SupportFolders
}

$ScriptBlock = {
param($Options)
& {
    param(
        $Action,
        $ComputerName,
        $Name,
        $DisplayName,
        $Description,
        $BinaryPathName,
        $StartupType,
        $Username,
        $Password,
        $ServicePathName,
        $SupportFolders,
        $NonInteractive
    )
    &powershell "C:\Services\DevProcessor\ManageService.ps1 $Action $ComputerName $Name $DisplayName $Description $BinaryPathName $StartupType $Username $Password $ServicePathName $SupportFolders"
} @Options;
}

$ResultMessage = Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock $ScriptBlock -ArgumentList $Options

If I run the code modified as it is listed above, I still get the flattened array for $SuppportFolders and the ManageService.ps1 script trips up over parameters that have spaces, even though they are quoted when I assign them.

The option to completely wrap the code in ManageService.ps1, as opposed to simply calling the remote script is not really viable because the ManagedService.ps1 script is fairly extensive and generic so I can call it from over 30 automation scripts in my deployment server.

I believe what @Bacon Bits is suggesting would work if it was feasible to wrap the ManageService script.

EiEiGuy
  • 1,447
  • 5
  • 18
  • 32
  • `$SupportFolders` variable is supposed to be an array of strings. For proof, check the following: `"[DEBUG] SupportFolders [$($SupportFolders.GetType().BaseType)]: [$($SupportFolders -join ';')]."` – JosefZ Oct 24 '17 at 20:22
  • @JosefZ Yes it is supposed to be an array of strings. However, it is getting flattened as it is passed to the script on the remote computer as a parameter to Invoke-Command -Scriptblock {}. – EiEiGuy Oct 24 '17 at 21:51
  • Possible duplicate of [How to pass results of Get-Childitem into a Scriptblock properly?](https://stackoverflow.com/questions/44634759/how-to-pass-results-of-get-childitem-into-a-scriptblock-properly) – David Wall Oct 25 '17 at 03:33
  • Possible duplicate of [ArgumentList parameter in Invoke-Command don't send all array](https://stackoverflow.com/questions/18743951/argumentlist-parameter-in-invoke-command-dont-send-all-array) – Moerwald Oct 25 '17 at 05:27

1 Answers1

3

To pass a single array, you can do this:

Invoke-Command -Session $Session -ScriptBlock $ScriptBlock -ArgumentList (,$Array);

However, that only works if you only need to pass a single array. It can all fall apart as soon as you start to pass multiple arrays or multiple complex objects.

Sometimes, this will work:

Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList (, $Array1), (, $Array2), (, $Array3);

However, it can be inconsistent in my experience. Sometimes it flattens the arrays out again.

What you can do is something similar to this answer.

{param($Options)& <# Original script block (including {} braces)#> @options }

Basically what we do is:

  1. Wrap the script in a scriptblock that accepts a single hashtable as an argument.
  2. Put all our arguments into the hashtable.
  3. Use the passed hashtable as a splat variable.

So it would be something like:

$Options = @{
    Action = 'Check';
    ComputerName = 'XYZ123456';
    Name = 'MyName';
    .
    .
    .
}

$ScriptBlock = {
    param($Options) 
    & {
        [CmdletBinding(DefaultParametersetName='None')]
        param (
        # Allows you to specify Install, Delete or Check.
        [ValidateSet("Install", "Delete", "Check")][string] $Action = "Check",
        # Allows you to specify the name of the remote computer.
        [string] $ComputerName = "None",
        # Allows you to specify the service name.
        [string] $Name = "None",
        .
        .
        .
        .
        #End Debug
    } @Options;
}

Invoke-Command -ComputerName RemoteServer -ScriptBlock $ScriptBlock -ArgumentList $Options;

Here's a trivial working example:

$Options = @{
    List1 = 'Ed', 'Frank';
    List2 = 5;
    List3 = 'Alice', 'Bob', 'Cathy', 'David'
}

$ScriptBlock = {
    param($Options) 
    & {
        param(
            $List1,
            $List2,
            $List3
        )
        "List1"
        $List1
        ''
        "List2"
        $List2
        ''
        "List3"
        $List3
    } @Options;
}

Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList $Options;

Output:

List1
Ed
Frank

List2
5

List3
Alice
Bob
Cathy
David

Note that I tested this on PowerShell v5. I no longer have a system with PowerShell v4 to test on.

Bacon Bits
  • 30,782
  • 5
  • 59
  • 66
  • I see that splatting is supported as of PowerShell 3 so that functionality should work. I had already started to create a version that used splatted params but wasn't sure how to make it work with Invoke-Command. I will test your recommendation out in the morning. Are you aware of any issues with this approach and having parameter values in the hash table that have spaces? – EiEiGuy Oct 24 '17 at 21:44
  • @EiEiGuy I'm not sure I understand what you're asking. However, if you have spaces in parameters you will often need to put them in quotes. – Bacon Bits Oct 25 '17 at 12:52
  • OK, so as I understand it, is that you are suggesting that I wrap the script that is on the remote server (the ManageService.ps1 script in my OP) within the $ScriptBlock code in this script (the calling script in my OP). The problem I have is that the ManageService.ps1 script is a fairly extensive service installation script that is reused by over 30 automation scrips similar to what I have described as the calling script in my post. – EiEiGuy Oct 25 '17 at 13:45
  • I do have parameters with spaces in quotes, as in quotes around the entire parameter value... such as $Description = "This is a description". However, when the hashtable it is parsed in the remote script, it still sees the quoted parameters as multiple values. – EiEiGuy Oct 25 '17 at 13:50