Is there a way to get a list of all remote variables in a script block?
Consider the following:
$x = 1
$block = { Write-Host $using:x }
Invoke-Command -Session (New-PSSession) -ScriptBlock $block
Inside of $block, is there any way of getting a list of available $using: scoped variables?
$x = 1
$block = { Get-Variable }
Invoke-Command -Session (New-PSSession) -ScriptBlock $block
Does not yield $x as an available variable
The short answer is: you can't.
The remote side doesn't know anything about the variables. They get serialized and then the deserialization code and the literal serialized XML is embedded.
If you're the one writing the script bock, then I recommend you just assign each $Using: variable to a local variable inside the scriptblock:
$block = {
$x = $Using:x
$y = $Using:y
}
I wrote a more detailed explanation of how $Using: is implemented on my blog regarding using it in DSC Script resources.
Related
I have a function that takes a single string array as a parameter in my PowerShell .pm1 that I want to be able to call on a remote server using a second function in my .pm1 (I do not want to rely on the server having a copy of the function). I found this Using Invoke-Command -ScriptBlock on a function with arguments but it only seems to work for 'non-arrays' or for multiple parameters (where array variable is not last)
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people
#output => "hello bob" only
Invoke-Command -ComputerName "s1" -ScriptBlock ${function:Hello_Worlds} -ArgumentList $people, ""
#output => "hello bob hello joe"
I can modify my argument list like -ArgumentList $people, "" (above) to make it work by forcing the function to see the $persons variable as a single parameter and not an array of parameters, but that seems like bad practice and I sure that I am just missing something simple.
EDIT:
I was directed here ArgumentList parameter in Invoke-Command don't send all array and while it works for this exact example, it requires that I KNOW which parameters require an array. Is there a generic way to pass an any arguments that would prevent this issue? I.E. I build my argument list as an array of parameters and there could be 0 or more of them and any number of them could be arrays - or am I stuck with putting this in front of calls?
foreach($parg in $myCustomGeneratedArguments) {
if($parg -is [array]) {$paramArgs += ,$parg}
else {$paramArgs += $parg}
}
Looking at your edit I'm afraid the linked answer doesn't lead you to the easier path, which is to not use -ArgumentList at all, instead, refer to your Hello_Worlds function and to your $people array with the $using: scope modifier:
function Hello_Worlds { param([string[]]$persons)
foreach($person in $persons){
write-host ("hello "+$person)
}
}
# store the function definition locally
$func = ${function:Hello_Worlds}.ToString()
$people = "bob","joe"
Invoke-Command -ComputerName "s1" -ScriptBlock {
# define the function in the remote scope
${function:Hello_Worlds} = $using:func
# now you can use it normally
Hello_Worlds -persons $using:people
}
Is it possible to modify remote variables? I am trying to do something like the following:
$var1 = ""
$var2 = ""
Invoke-Command -ComputerName Server1 -ScriptBlock{
$using:var1 = "Hello World"
$using:var2 = "Goodbye World"
}
When I try this I get the error:
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a variable or a property.
So obviously, it doesn't work using this method, but are there any other approaches I could take? I need to use and modify those variables in both a remote and local scope
So what you are trying to do wont work. But here is a work around.
Place your data you want returned into a hashtable and then capture the results and enumerate over them and place the value into the variables.
$var1 = ""
$var2 = ""
$Reponse = Invoke-Command -ComputerName Server1 -ScriptBlock{
$Stuff1 = "Hey"
$Stuff2 = "There"
Return #{
var1 = $Stuff1
var2 = $Stuff2
}
}
$Reponse.GetEnumerator() | %{
Set-Variable $_.Key -Value $_.Value
}
$var1
$var2
This will return
Hey
There
What you're trying to do fundamentally cannot work:
A $using: reference to a variable in the caller's scope in script blocks executed in a different runspace (such as remotely, via Invoke-Command -ComputerName, as in your case):
is not a reference to the variable object (to the variable as a whole),
but expands to the variable's value, and you fundamentally cannot assign something to a value.
In the case at hand, $using:var1 effectively becomes "" in your script block (the value of $var1 when Invoke-Command is called), and something like "" = "Hello world" cannot work.
The conceptual help topic about_Remote_Variables (now) mentions that (emphasis added):
A variable reference such as $using:var expands to the value of variable $var from the caller's context. You do not get access to the caller's variable object.
See this answer for background information.
As for a potential solution:
Make your script block output the values of interest, then assign to local variables, as shown in ArcSet's helpful answer.
I have following function:
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject,
[Object] $ScriptBlock
)
process {
$value = Invoke-Command -ScriptBlock $ScriptBlock
Write-Host "Script: $value"
}
}
When I define this function directly in script and pipe input into it I get following result which is expected:
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: Test"
But when I define this function inside module and export it with Export-ModuleMember -Function PipeScript then pipeline variable $_ inside script block is always null:
Import-Module PipeModule
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name }
# Outputs: "Script: "
Full repro is available at: https://github.com/lpatalas/DelayBindScriptBlock
Can someone explain this behaviour?
Tip of the hat to PetSerAl for all his help.
Here's a simple solution, but note that it runs the script block directly in the caller's scope, i.e. it effectively "dot-sources", which allows modification of the caller's variables.
By contrast, your use of Invoke-Command runs the script block in a child scope of the caller's scope - if that is truly the intent, see the variant solution below.
"Dot-sourcing" the script block is also what standard cmdlets such as Where-Object and ForEach-Object do.
# Define the function in an (in-memory) module.
# An in-memory module is automatically imported.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ForEach-Object to create the automatic $_ variable
# in the script block's origin scope.
$value = ForEach-Object -Process $ScriptBlock -InputObject $InputObject
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42; #{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 43 - the script block ran in the caller's scope.
The above outputs string Script: Test and 43 afterwards, proving that the input object was seen as $_ and that dot-sourcing worked ($var was successfully incremented in the caller's scope).
Here's a variant, via the PowerShell SDK, that runs the script block in a child scope of the caller's scope.
This can be helpful if you don't want the execution of the script block to accidentally modify the caller's variables.
It is the same behavior you get with the engine-level delay-bind script-block and calculated-property features - though it's unclear whether that behavior was chosen intentionally.
$null = New-Module {
function PipeScript {
param(
[Parameter(ValueFromPipeline)]
[Object] $InputObject
,
[scriptblock] $ScriptBlock
)
process {
# Use ScriptBlock.InvokeContext() to inject a $_ variable
# into the child scope that the script block runs in:
# Creating a custom version of what is normally an *automatic* variable
# seems hacky, but the docs do state:
# "The list of variables may include the special variables
# $input, $_ and $this." - see https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext
$value = $ScriptBlock.InvokeWithContext(
$null, # extra functions to define (none here)
[psvariable]::new('_', $InputObject) # actual parameter type is List<PSVariable>
)
# Output the value
"Script: $value"
}
}
}
# Test the function:
$var = 42
#{ Name = 'Test' } | PipeScript -ScriptBlock { $_.Name; ++$var }
$var # -> 42 - unaltered, because the script block ran in a child scope.
The above outputs string Script: Test, followed by 42, proving that the script block saw the input object as $_ and that variable $var - although seen in the script block, was not modified, due to running in a child scope.
The ScriptBlock.InvokeWithContext() method is documented here.
As for why your attempt didn't work:
Generally, script blocks are bound to the scope and scope domain in which they are created (except if they're created expressly as unbound script blocks, with [scriptblock]::Create('...')).
A scope outside of a module is part of the default scope domain. Every module has its own scope domain, and except for the global scope, which all scopes across all scope domains see, scopes in different scope domains do not see one another.
Your script block is created in the default scope domain, and when the module-defined function invokes it, the $_ is looked for in the scope of origin, i.e., in the (non-module) caller scope, where it isn't defined, because the automatic $_ variable is created by PowerShell on demand in the local scope, which is in the enclosing module's scope domain.
By using .InvokeWithContext(), the script block runs in a child scope of the caller's scope (as would be the case with .Invoke() and Invoke-Command by default), into which the above code injects a custom $_ variable so that the script block can reference it.
Providing better SDK support for these scenarios is being discussed in GitHub issue #3581.
I normally do the following to invoke a script block containing $_:
$scriptBlock = { $_ <# do something with $_ here #> }
$theArg | ForEach-Object $scriptBlock
In effect, I am creating a pipeline which will give $_ its value (within the Foreach-Object function invocation).
However, when looking at the source code of the LINQ module, it defines and uses the following function to invoke the delegate:
# It is actually surprisingly difficult to write a function (in a module)
# that uses $_ in scriptblocks that it takes as parameters. This is a strange
# issue with scoping that seems to only matter when the function is a part
# of a module which has an isolated scope.
#
# In the case of this code:
# 1..10 | Add-Ten { $_ + 10 }
#
# ... the function Add-Ten must jump through hoops in order to invoke the
# supplied scriptblock in such a way that $_ represents the current item
# in the pipeline.
#
# Which brings me to Invoke-ScriptBlock.
# This function takes a ScriptBlock as a parameter, and an object that will
# be supplied to the $_ variable. Since the $_ may already be defined in
# this scope, we need to store the old value, and restore it when we are done.
# Unfortunately this can only be done (to my knowledge) by hitting the
# internal api's with reflection. Not only is this an issue for performance,
# it is also fragile. Fortunately this appears to still work in PowerShell
# version 2 through 3 beta.
function Invoke-ScriptBlock {
[CmdletBinding()]
param (
[Parameter(Position=1,Mandatory=$true)]
[ScriptBlock]$ScriptBlock,
[Parameter(ValueFromPipeline=$true)]
[Object]$InputObject
)
begin {
# equivalent to calling $ScriptBlock.SessionState property:
$SessionStateProperty = [ScriptBlock].GetProperty('SessionState',([System.Reflection.BindingFlags]'NonPublic,Instance'))
$SessionState = $SessionStateProperty.GetValue($ScriptBlock, $null)
}
}
process {
$NewUnderBar = $InputObject
$OldUnderBar = $SessionState.PSVariable.GetValue('_')
try {
$SessionState.PSVariable.Set('_', $NewUnderBar)
$SessionState.InvokeCommand.InvokeScript($SessionState, $ScriptBlock, #())
}
finally {
$SessionState.PSVariable.Set('_', $OldUnderBar)
}
}
}
This strikes me as a bit low-level. Is there a recommended, safe way of doing this?
You can invoke scriptblocks with the ampersand. No need to use Foreach-Object.
$scriptblock = {## whatever}
& $scriptblock
#(1,2,3) | % { & {write-host $_}}
To pass parameters:
$scriptblock = {write-host $args[0]}
& $scriptblock 'test'
$scriptBlock = {param($NamedParam) write-host $NamedParam}
& $scriptBlock -NamedParam 'test'
If you're going to be using this inside of Invoke-Command, you could also usin the $using construct.
$test = 'test'
$scriptblock = {write-host $using:test}
I'm creating a dynamic ScriptBlock the way below so I can use local functions and variables and easily pass them to remote computers via Invoke-Command. The issue is that since all the text inside Create is enclosed with double quotes, I loose all my syntax highlighting since all editors see the code as one big string.
While this is only a cosmetic issue, I'd like to find a work around that allow my code to be passed without having double quotes. I've tried passing a variable inside Create instead of the actually text, but it does not get interpreted.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = [ScriptBlock]::Create(#"
#Define Function
function local_admin {$Function:local_admin}
local_admin domain username
"#)
Invoke-Command -ComputerName server2 -ScriptBlock $SB
You can pass the function into the remote session using the following example. This allows you to define the ScriptBlock using curly braces instead of as a string.
# Define the function
function foo {
"bar";
}
$sb = {
# Import the function definition into the remote session
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
foo;
};
#(gi function:foo) | select *
Invoke-Command -ComputerName . -ScriptBlock $sb -ArgumentList (Get-Item -Path function:foo);
Here is a modified version of your function. Please take note that the domain and username can be dynamically passed into the remote ScriptBlock using the -ArgumentList parameter. I am using the $args automatic variable to pass objects into the ScriptBlock.
function local_admin($a, $b) {
([adsi]"WinNT://localhost/Administrators,group").Add("WinNT://$a/$b,user")
}
$SB = {
#Define Function
[void](New-Item -Path $args[0].PSPath -Value $args[0].Definition);
# Call the function
local_admin $args[1] $args[2];
}
Invoke-Command -ComputerName server2 -ScriptBlock $SB -ArgumentList (Get-Item -Path function:local_admin), 'domain', 'username';