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.

4
  • $SupportFolders variable is supposed to be an array of strings. For proof, check the following: "[DEBUG] SupportFolders [$($SupportFolders.GetType().BaseType)]: [$($SupportFolders -join ';')]." Commented Oct 24, 2017 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 {}. Commented Oct 24, 2017 at 21:51
  • Possible duplicate of How to pass results of Get-Childitem into a Scriptblock properly? Commented Oct 25, 2017 at 3:33
  • Possible duplicate of ArgumentList parameter in Invoke-Command don't send all array Commented Oct 25, 2017 at 5:27

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

4 Comments

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 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.
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.
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.
0

This is a creative solution that helped me solve my own unique situation. While this approach didn't work for me, a simpler but similar solution could be done by passing a variable with all sub-variables within. For me, I was running start-job commands instead of invoke-command and this approach simply wasn't working. What ended up working for both commands was injecting arrays within a hashtable that the scriptblock would later interpret:

$JobName = "MyJob"

$Options = @{
   var1 = $Array1
   var2 = $Array2
   var3 = $somestring
}

[scriptblock]$script = {
   $options = $args[0]
   $var1 = $options.var1
   $var2 = $options.var2
   $var3 = $options.var3

foreach ($item in $var1) {
       Try {
          $item | do-something
       }
       Catch {
          "" | select @{N="Col1";E={$null}},
                      @{N="Col2";E={$null}},
                      @{N="Col3";E={$item.property}},
                      @{N="Error";E={$error[0]}}
       }
   }
}

Start-Job -ScriptBlock $script -ArgumentList $Options -Name $JobName

To me, this was easier to understand and predict. And while I couldn't get it to work using original approach, I had no issues when running under start-job. Perhaps I gave up too quickly while debugging or one of the caveats of posted solution was issues with try/catch. It would probably address issues users reported with quotes. Either way, I thought I'd share.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.