How do I pass a hashtable to a scriptblock? - powershell

I tried to pass a hashtable to a scriptblock like this where $arg3 is my hashtable. However, it failed. How do I do it in the correct way?
It doesn't seem like it is passing anything to the script block.
$commandParameters.ComputerName = $ComputerName
$commandParameters.ScriptBlock = {
param(
[Parameter()]
[switch]$arg1 = $false,
[Parameter()]
[array]$arg2,
[Parameter()]
[hashtable]$arg3
)
enter code here
Doing something here
}
Invoke-Command #commandParameters -ArgumentList $arg1, #($arg2), $arg3
=================================================================
I found the answer myself and it works for me. This is how I build the associative array and then pass it to the script block.
I am not sure why, but I was using the dot notation ($hash.a.b) to reference the hash table in a function and it works, but it doesn't work for a script block. It looks like I need to use [ ] (e.g. $hash[a][b])in the script block.
$compADGroups = #{}
foreach ( $adGroup in $adGroups ) {
if ( $compADGroups.$computerNameGroup -eq $null ) {
$compADGroups[$computerName] = #{}
$compADGroups[$computerName]["Group"] = #{}
$compADGroups[$computerName]["Group"] = $hashString
}
}
$session = New-PSSession -ComputerName 'Computer1'
Invoke-Command -Session $session -ArgumentList $compADGroups -ScriptBlock { param($compADGroups) $compADGroups[$env:computername]["Group"]}
Get-PSSession | Remove-PSSession

Make sure you're using Invoke-Command correctly.
$ScriptBlock = {
param(
[Parameter(Mandatory=$True, Position=1)]
[hashtable]$myHashTable
)
# Code here
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ([hashtable]$hashtable)
If you specify certain arguments for a scriptblock, make sure you also describe the position value, and more often than not whether it's mandatory. If you are trying to pass your hashtable in as the second argument in an implicitly defined array of arguments, write your scriptblock so that it takes the hashtable at that specific position.
For example,
$ScriptBlock= {
param(
[Parameter(Position=2)] # Take note of the position you set here
[hashtable]$myHashTable,
[Parameter(Position=1)]
[string]$myString,
[Parameter(Position=3)]
[int]$myInteger
)
# Do stuff
}
Invoke-Command -ScriptBlock $ScriptBlock -ArgumentList ($myString, $myHashTable, $myInteger);
# ^ variable is in second position

Related

PowerShell implementing -AsJob for a cmdlet

Is there a nice way to implement the switch parameter -AsJob in custom cmdlets, like Invoke-Command has?
The only way I thought about this is:
function Use-AsJob {
[CmdletBinding()]
[OutputType()]
param (
[Parameter(Mandatory = $true)]
[string]
$Message,
[switch]
$AsJob
)
# Wrap Script block in a variable
$myScriptBlock = {
# stuff
}
if ($AsJob) {
Invoke-Command -ScriptBlock $myScriptBlock -AsJob
}
else {
Invoke-Command -ScriptBlock $myScriptBlock
}
}
Is there a better approach? I couldn't find Microsoft docs on this, any lead helps.
If we make the following assumptions:
Command is a script function
Function does not rely on module state
Then you can use the following boilerplate for any command:
function Test-AsJob {
param(
[string]$Parameter = '123',
[switch]$AsJob
)
if ($AsJob) {
# Remove the `-AsJob` parameter, leave everything else as is
[void]$PSBoundParameters.Remove('AsJob')
# Start new job that executes a copy of this function against the remaining parameter args
return Start-Job -ScriptBlock {
param(
[string]$myFunction,
[System.Collections.IDictionary]$argTable
)
$cmd = [scriptblock]::Create($myFunction)
& $cmd #argTable
} -ArgumentList $MyInvocation.MyCommand.Definition,$PSBoundParameters
}
# here is where we execute the actual function
return "Parameter was '$Parameter'"
}
Now you can do either:
PS C:\> Test-AsJob
Parameter was '123'
PS C:\> Test-AsJob -AsJob |Receive-Job -Wait
Parameter was '123'

Invoke-Command wrapper function that passes Cmdlet name and parameters

I am building an Azure Runbook helper function, that ultimately executes on a Hybrid Runbook Worker.
To generalize my code as much as possible I came up with the idea to have a function dynamically build a scriptblock that defines the Exchange Cmdlet name and parameter.This is what I have so far - the comments are experiments that reveal some of the steps I tried before posting this thread.
function Invoke-BSExchangeCommand{
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]
$CmdletName,
[Parameter(Mandatory = $false)]
[hashtable]
$ArgumentList,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.Management.Automation.Runspaces.PSSession]
$Session
)
$scriptBlock = {
param ($invokeParams)
# Get-DistributionGroup #invokeParams
# $_::$CmdletName.Invoke($invokeParams)
$cmd = Get-Command -Name $CmdletName
& $cmd #invokeParams
}
$params = #{
Session = $Session
ArgumentList = $ArgumentList
ScriptBlock = $scriptBlock
}
Invoke-Command #params
}
Edit: I have found that the "Get-Command" cmdlet is exposed/not restricted on the Exchange endpoint.
The error I am getting is a generic one and gives many different (irrelevant) Google search results: A Begin statement block, Process statement block, or parameter statement is not allowed in a Data section
Edit: My starting point that I am trying to improve is that this scriptblock with the defined Cmdlet name works:
$scriptBlock = {
param ($invokeParams)
Get-DistributionGroup #invokeParams
}
Thank you!
-Anders

Expand variable with scriptblock inside variable in a loop with runspaces

$RunspaceCollection = #()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
Foreach ($test in $case) {
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
}.GetNewClosure()
$Powershell = [PowerShell]::Create().AddScript($finalcode)
$Powershell.RunspacePool = $RunspacePool
[Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}}
The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?
Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)
[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
<SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')
This is what I see in debugger in runspace
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>>
Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s
Stopped at: </Objs>')) }
The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:
$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = #({'somecode'},{'someothercode'})
$finalcode= {
param($Argument)
Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}
Foreach ($test in $case) {
$Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
$Powershell.RunspacePool = $RunspacePool
$RunspaceCollection.Add((New-Object -TypeName PSObject -Property #{
Runspace = $PowerShell.BeginInvoke()
PowerShell = $PowerShell
}))
}
I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.
You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.
My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.
Here's the code from my blog (also available as a GitHub gist):
function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
[Parameter(
Mandatory,
ValueFromPipeline
)]
[String]
$Code ,
[Parameter(
Mandatory,
ParameterSetName = 'AsScriptBlock'
)]
[Switch]
$AsScriptBlock
)
Process {
$cb = {
$m = $args[0]
$ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
"`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
}
$newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
if ($AsScriptBlock.IsPresent) {
[ScriptBlock]::Create($newCode)
} else {
$newCode
}
}
}
A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.
Your Use Case
$finalcode= {
Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using
For better results you might assign a variable first and then just insert that:
$value = [scriptblock]::Create($code[$test])
$finalcode= {
Invoke-Command -ScriptBlock $Using:value
} | Replace-Using

PowerShell Splatting the Argumentlist on Invoke-Command

How is it possible to use the parameters collected in a hash table for use with ArgumentList on Invoke-Command?
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
Structure = 'yyyy-MM-dd'
}
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock ${Function:Copy-FilesHC} -ArgumentList #CopyParams
Whatever I try, it's always complaining about the 'Source':
Cannot validate argument on parameter 'Source'. The "Test-Path $_" validation script for the argument with
value "System.Collections.Hashtable" did not return true. Determine why the validation script failed
This blog talks about a similar problem, but I can't get it to work.
The same is true for a simple Copy-Item within Invoke-Command, example:
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {Copy-Item} -ArgumentList #CopyParams
Invoke-Command : Missing an argument for parameter 'ArgumentList'. Specify a parameter of type 'System.Obj
ect[]' and try again.
At line:11 char:89
+ ... ck {Copy-Item} -ArgumentList #CopyParams
Thank you for your help.
One-liner, to convert a remote script to accept named parameters from a hash.
Given a scriptblock which you wish to call like this:
$Options = #{
Parameter1 = "foo"
Parameter2 = "bar"
}
Invoke-Command -ComputerName REMOTESERVER -ArgumentList $Options -ScriptBlock {
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
}
You can convert it like so
Invoke-Command -Computername REMOTESERVER -ArgumentList $Options -ScriptBlock {param($Options)&{
param(
$Parameter1,
$Parameter2
)
#Script goes here, this is just a sample
"ComputerName: $ENV:COMPUTERNAME"
"Parameter1: $Parameter1"
"Parameter2: $Parameter2"
} #Options}
What's going on? Essentially we've wrapped the original script block like so:
{param($Options)& <# Original script block (including {} braces)#> #options }
This makes the original script block an anonymous function, and creates the outer script block which has a parameter $Options, which does nothing but call the inner script block, passing #options to splat the hash.
Here's one way to approach passing named parameters:
function Copy-FilesHC
{
param ($Source,$Destination,$Structure)
"Source is $Source"
"Desintation is $Destination"
"Structure is $Structure"
}
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
Destination = "'E:\DEPARTMENTS\CBR\SHARE\Target 2'" #Nested quotes required due to embedded space in value.
Structure = 'yyyy-MM-dd'
}
$SB = [scriptblock]::Create(".{${Function:Copy-FilesHC}} $(&{$args}#CopyParams)")
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock $SB
Basically, you create a new script block from your invoked script, with the parameters splatted to that from the hash table. Everything is already in the script block with the values expanded, so there's no argument list to pass.
I found a workaround, but you have to make sure that your Advanced function which is located in your module file is loaded up front in the local session. So it can be used in the remote session. I wrote a small helper function for this.
Function Add-FunctionHC {
[CmdletBinding(SupportsShouldProcess=$True)]
Param(
[String]$Name
)
Process {
Try {
$Module = (Get-Command $Name -EA Stop).ModuleName
}
Catch {
Write-Error "Add-FunctionHC: Function '$Name' doesn't exist in any module"
$Global:Error.RemoveAt('1')
Break
}
if (-not (Get-Module -Name $Module)) {
Import-Module -Name $Module
}
}
}
# Load funtion for remoting
Add-FunctionHC -Name 'Copy-FilesHC'
$CopyParams = #{
Source = 'E:\DEPARTMENTS\CBR\SHARE\Target\De file.txt'
Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
}
$RemoteFunctions = "function Copy-FilesHC {${function:Copy-FilesHC}}" #';' seperated to add more
Invoke-Command -ArgumentList $RemoteFunctions -ComputerName 'SERVER' -Credential $Cred -ScriptBlock {
Param (
$RemoteFunctions
)
. ([ScriptBlock]::Create($RemoteFunctions))
$CopyParams = $using:CopyParams
Copy-FilesHC #CopyParams
}
The big advantage is that you don't need to copy your complete function in the script and it can stay in the module. So when you change something in the module to the function it will also be available in the remote session, without the need to update your script.
I recently experienced a similar problem and solved it by building the hash (or rebuilding the hash) inside the invoke by leveraging the $using variable scope (more on that here)
it looks something like this:
$Source = 'E:\DEPARTMENTS\CBR\SHARE\Target'
$Destination = 'E:\DEPARTMENTS\CBR\SHARE\Target 2'
$Structure = 'yyyy-MM-dd'
Invoke-Command -Credential $Cred -ComputerName 'SERVER' -ScriptBlock {
$CopyParms= #{
'Source'=$Using:Source
'Destination'=$Using:Destination
'Structure'=$Using:Structure
}
Function:Copy-FilesHC #CopyParms
}
This is what works for me:
$hash = #{
PARAM1="meaning of life"
PARAM2=42
PARAM3=$true
}
$params = foreach($x in $hash.GetEnumerator()) {"$($x.Name)=""$($x.Value)"""}
I know this is late, but I ran into the same problem and found a solution that worked for me. Assigning it to a variable within the scriptblock and then using that variable to splat didn't show any problems.
Here's an example:
$param=#{"parameter","value"}
invoke-command -asjob -session $session -ScriptBlock {$a=$args[0];cmdlet #a } -ArgumentList $param

How do I run a ps1 script file passed by parameter from inside a powershell function?

I need to write a function in PowerShell that receives a bunch of parameters, one of them being a ps1 file. I need to execute this file as part of my function code, but I don't know how to do that.
This is probably a very silly detail, but I failed horribly in trying to search for this.
This is my function at the moment. I tried using Invoke-Command there but it's not working:
Function Start-Dsc {
Param(
[Parameter(Mandatory = $true)]
[string] $configurationFile,
[Parameter(Mandatory = $true)]
[string] $configurationName,
[Parameter()]
[string] $configurationData,
[Parameter(Mandatory = $true)]
[string] $computerName
)
Begin {}
Process
{
Invoke-Command -Command "$configurationFile -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
End{}
}
UPDATE:
With Bacon Bits' help, I managed to make it work. The final script is a bit different than what I initially posted though. This is the final process block:
Process
{
Invoke-Command -FilePath $configurationFile -ComputerName 'localhost';
Invoke-Expression -Command "$configurationName -ConfigurationData $configurationData";
Start-DscConfiguration -Path ".\$configurationName" -ComputerName $computerName -Verbose -Wait
}
Arguments are a separate option in Invoke-Command. Try:
Invoke-Command -Command "$configurationFile" -ArgumentList "-ConfigurationData $configurationData";
You may also need to change -Command to -FilePath.