Reputation: 1547
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.
Upvotes: 0
Views: 8275
Reputation: 1
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.
Upvotes: 0
Reputation: 32170
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:
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.
Upvotes: 3