PowerShell splatting not working - powershell

I believe I'm missing something obvious, or misunderstanding the splatting feature of PowerShell.
I am using a hash table to pass arguments to a custom function, but it doesn't seem to take the arguments on even a simple example.
File: Test-Splat.ps1
function Test-Splat
{
param(
[Parameter(Mandatory=$true)][string]$Name,
[Parameter(Mandatory=$true)][string]$Greeting
)
$s = "$Greeting, $Name"
Write-Host $s
}
Then attempting to execute this with splatting, asks for a value for the second parameter.
. .\Test-Splat.ps1
$Params = #{
Name = "Frank"
Greeting = "Hello"
}
Test-Splat $Params
Produces the following result
cmdlet Test-Splat at command pipeline position 1
Supply values for the following parameters:
Greeting:
If I use this directly without splatting, it works
Greeting: [PS] C:\>Test-Splat -Name "Frank" -Greeting "Hello"
Hello, Frank
If it's related, I'm doing this within Exchange Management Shell under PowerShell 3.0
[PS] C:\>$PSVersionTable.PSVersion
Major Minor Build Revision
----- ----- ----- --------
3 0 -1 -1

You are indeed missing something, and that is when you want to splat a variable as the parameters of a function or cmdlet you use the # symbol instead of the $ symbol. In your example, the line where you splat the variable would look like this:
Test-Splat #Params

Related

Why does the scope of variables change depending on if it's a .ps1 or .psm1 file, and how can this be mitigated?

I have a function that executes a script block. For convenience, the script block does not need to have explicitly defined parameters, but instead can use $_ and $A to refer to the inputs.
In the code, this is done as such:
$_ = $Value
$A = $Value2
& $ScriptBlock
This whole thing is wrapped in a function. Minimal example:
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
[Object]$Value2
)
$_ = $Value
$A = $Value2
& $ScriptBlock
}
If this function is written in a PowerShell script file (.ps1), but imported using Import-Module, the behaviour of F is as expected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
15
PS>
However, when the function is written in a PowerShell module file (.psm1) and imported using Import-Module, the behaviour is unexpected:
PS> F -Value 7 -Value2 1 -ScriptBlock {$_ * 2 + $A}
PS>
Using {$_ + 1} instead gives 1. It seems that $_ has a value of $null instead. Presumably, some security measure restricts the scope of the $_ variable or otherwise protects it. Or, possibly, the $_ variable is assigned by some automatic process. Regardless, if only the $_ variable was affected, the first unsuccessful example would return 1.
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
In conclusion, the questions are:
Why can't script blocks in module files access variables defined in functions from which they were called?
Is there a method for explicitly specifying the variables accessible by a script block when invoking it?
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Out of order:
Is there some other way of solving this that does not involve including an explicit parameter declaration in the script block?
Yes, if you just want to populate $_, use ForEach-Object!
ForEach-Object executes in the caller's local scope, which helps you work around the issue - except you won't have to, because it also automatically binds input to $_/$PSItem:
# this will work both in module-exported commands and standalone functions
function F {
param(
[ScriptBlock]$ScriptBlock,
[Object]$Value
)
ForEach-Object -InputObject $Value -Process $ScriptBlock
}
Now F will work as expected:
PS C:\> F -Value 7 -ScriptBlock {$_ * 2}
Ideally, the solution would involve the ability to explicitly specify the environment in which a script block is run. Something like:
Invoke-ScriptBlock -Variables #{"_" = $Value; "A" = $Value2} -InputObject $ScriptBlock
Execute the scripblock using ScriptBlock.InvokeWithContext():
$functionsToDefine = #{
'Do-Stuff' = {
param($a,$b)
Write-Host "$a - $b"
}
}
$variablesToDefine = #(
[PSVariable]::new("var1", "one")
[PSVariable]::new("var2", "two")
)
$argumentList = #()
{Do-Stuff -a $var1 -b two}.InvokeWithContext($functionsToDefine, $variablesToDefine, $argumentList)
Or, wrapped in a function like your original example:
function F
{
param(
[scriptblock]$ScriptBlock
[object]$Value
)
$ScriptBlock.InvokeWithContext(#{},#([PSVariable]::new('_',$Value)),#())
}
Now you know how to solve your problem, let's get back to the question(s) about module scoping.
At first, it's worth noting that you could actually achieve the above using modules, but sort of in reverse.
(In the following, I use in-memory modules defined with New-Module, but the module scope resolution behavior describe is the same as when you import a script module from disk)
While module scoping "bypasses" normal scope resolution rules (see below for explanation), PowerShell actually supports the inverse - explicit execution in a specific module's scope.
Simply pass a module reference as the first argument to the & call operator, and PowerShell will treat the subsequent arguments as a command to be invoked in said module:
# Our non-module test function
$twoPlusTwo = { return $two + $two }
$two = 2
& $twoPlusTwo # yields 4
# let's try it with explicit module-scoped execution
$myEnv = New-Module {
$two = 2.5
}
& $myEnv $twoPlusTwo # Hell froze over, 2+2=5 (returns 5)
Why can't script blocks in module files access variables defined in functions from which they were called?
If they can, why can't the $_ automatic variable?
Because loaded modules maintain state, and the implementers of PowerShell wanted to isolate module state from the caller's environment.
Why might that be useful, and why might one preclude the other, you ask?
Consider the following example, a non-module function to test for odd numbers:
$two = 2
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
If we run the above statements in a script or an interactive prompt, subsequently invocating Test-IsOdd should yield the expected result:
PS C:\> Test-IsOdd 123
True
So far, so great, but relying on the non-local $two variable bears a flaw in this scenario - if, somewhere in our script or in the shell we accidentally reassign the local variable $two, we might break Test-IsOdd completely:
PS C:\> $two = 1 # oops!
PS C:\> Test-IsOdd 123
False
This is expected since, by default, variable scope resolution just wanders up the call stack until it reaches the global scope.
But sometimes you might require state to be kept across executions of one or more functions, like in our example above.
Modules solve this by following slightly different scope resolution rules - module-exported functions defer to something we call module scope (before reaching the global scope).
To illustrate how this solves our problem from before, considering this module-exported version of the same function:
$oddModule = New-Module {
function Test-IsOdd
{
param([int]$n)
return $n % $two -ne 0
}
$two = 2
}
Now, if we invoke our new module-exported Test-IsOdd, we predictably get the expected result, regardless of "contamination" in the callers scope:
PS C:\> Test-IsOdd 123
True
PS C:\> $two = 1
PS C:\> Test-IsOdd 123 # still works
True
This behavior, while maybe surprising, basicly serves to solidify the implicit contract between the module author and the user - the module author doesn't need to worry too much about what's "out there" (the callers session state), and the user can expect whatever going on "in there" (the loaded module's state) to work correctly without worrying about what they assign to variables in the local scope.
Module scoping behavior poorly documented in the help files, but is explained in some depth in chapter 8 of Bruce Payette's "PowerShell In Action" (ISBN:9781633430297)

PowerShell update a list variable with values returned from within the invoke-command [duplicate]

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.

Modifying remote variables inside ScriptBlock using Invoke-Command

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.

Powershell Call-Operator(&) with Parameters from Variable

G'day everyone,
I'm trying to execute a function in PowerShell with the Parameters coming from a Variable I'm not sure if it's possible in the way I want it to but maybe someone has any idea how I would go about doing that.
$scriptPath = "C:\temp\Create-File.ps1"
$parameters = "-Path C:\temp\testfile.txt -DoSomethingSpecial"
& $scriptPath $parameters
Something along those lines, I don't know in which order the Parameters get entered so I can't use $args[n..m] or binding by position for that. Maybe there is some other Cmdlet I don't know about that is capable of doing that?
Passing an Object as #James C. suggested in his answer allows only to pass parameters in Powershell syntax (e.g. -param1 value1 -param2 value2)
When you need more control over the parameters you pass such as:
double dash syntax for unix style --param1 value1
Slash syntax for Windows style /param1 value1
Equals sign required (or colon) -param1=value1 or -param1:value1
No value for parameter -boolean_param1
additional verbs (values without a param name) value1 value2
you can use an array instead of an object
take ipconfig command for example to renew all connections with "con" in their name:
$cmd = "ipconfig"
$params = #('/renew', '*Con*');
& $cmd $params
or the specific question given example:
$params = #('-Path', 'C:\temp\testfile.txt', '-DoSomethingSpecial')
.\Create-File.ps1 #params
You can use a hastable and Splatting to do this.
Simply set each param name and value in the variable as you would a normal hastable, then pass this in using #params syntax.
The switch param however, needs a $true value for it to function correctly.
$params = #{
Path = 'C:\temp\testfile.txt'
DoSomethingSpecial = $true
}
.\Create-File.ps1 #params
You can run it by Start-Process
Start-Process powershell -ArgumentList "$scriptPath $parameters"

PowerShell script string interpolation with named parameters is not working

I have the following function (I just paste it into the command line):
function Test ($url, $interface, $method)
{
Write-Host "http://$url/$interface/$method"
}
I then call it:
Test("localhost:90", "IService", "TestMethod")
I get:
http://localhost:90 IService TestMethod//
I expect to get:
http://localhost:90/IService/TestMethod
The same thing happens if I first set the result to a variable:
$res = "http://$url/$interface/$method"
Write-Host $res
I also don't think it's due to Write-Host, since I get the same error if I pass this string into .NET objects.
It completely confuses me that this works if I just define each variable. So, it's something to do with the fact that these are function parameters. I can do this from the command line:
PS C:\> $url = "localhost:90"
PS C:\> $interface = "IService"
PS C:\> $method = "TestMethod"
PS C:\> Write-Host "http://$url/$interface/$method"
http://localhost:90/IService/TestMethod
PS C:\>
Am I doing something silly, or is there another way to do string interpolation in PowerShell?
You aren't doing anything silly, but you are conflating PowerShell with something like Python.
When I do:
Function Count-Args
{
$args.count
}
Count-args($var1, $var2, $var3)
I get a count of 1, and all three variables you put into () are cast as a single array to $args.
Just change the way you call the function to the test mysite myinterface mymethod. Note the ss64 site advice.
Don't add brackets around the function parameters:
$result = Add-Numbers (5, 10) --Wrong!
$result = Add-Numbers 5 10 --Right