Powershell splatting a nested hash table - powershell

I have a function that returns a complex nested hash table data structure, where part of it forms the arguments for a further function call, and part of it is strings for reporting errors that result in the arguments not being populated.
Ideally I would like to splat just the arguments hash table, but I am starting to think that can't be done.
So, an example of the basic problem looks like this...
function Test {
param (
[String]$A,
[String]$B,
[String]$C
)
Write-Host "A: $A"
Write-Host "B: $B"
Write-Host "C: $C"
}
$data = #{
arguments = #{
A = 'A string'
B = 'Another string'
C = 'The last string'
}
kruft = 'A string that doesn not need to get passed'
}
Ideally I want to splat $data.arguments, so something like this...
Test #data.arguments
But that doesn't work, resulting in the error
The splatting operator '#' cannot be used to reference variables in an expression. '#data' can be used only as an argument to a command. To
reference variables in an expression use '$data'.
So I tried...
Test #(data.arguments)
Which results in the error
data.arguments : The term 'data.arguments' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is correct and try again.
I also tried...
Test #($data.arguments)
Which results in the whole hash table being passed as a single argument and the output is
A: System.Collections.Hashtable
B:
C:
What DOES work is...
$arguments = $data.arguments
Test #arguments
Which has me thinking you really cannot splat anything but a simple variable that is an appropriate hash table. But, I am hoping someone can verify that is indeed true, or point out the solution I haven't come up with yet.
The actual code requires 5 arguments, with somewhat verbose names because I prefer descriptive names, so splatting is very much an appropriate solution. Needing to make a new variable with just the hash table to be passed isn't an issue, just wondering if it really is the only option.

That's not possible.
Any variable (and only variables) provided with "#" instead of "$" for a function parameter is declared as TokenKind "SplattedVariable".
See:
https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.tokenkind?view=powershellsdk-7.0.0
PS:
Some quick tests from my side which could have succeed (apart from PS design):
Write-Host 'Test 1' -ForegroundColor Yellow
Test #$data.arguments
Write-Host 'Test 2' -ForegroundColor Yellow
Test #$($bla = $data.arguments)
Write-Host 'Test 3' -ForegroundColor Yellow
Test #$bla = $data.arguments
Write-Host 'Test 4' -ForegroundColor Yellow
Test #$bla = $data.arguments.GetEnumerator()
Write-Host 'Test 5' -ForegroundColor Yellow
Test #$($data.arguments.GetEnumerator())
... but they didn't.

I cannot claim to fully understand what you're attempting, but here are a couple of solutions that might help.
Your Test function is expecting three strings, but I don't see anything in your example that satisfies that. I would either change it to accept a Hashtable (which is the data type in question) or have it accept a string array and pass $data.arguments.values
Using a Hashtable:
function Test {
param (
[Hashtable]$hash
)
write-host $hash.A
write-host $hash.B
write-host $hash.C
}
Test $data.arguments
Using a String array:
function Test {
param (
[String[]]$a
)
$a | % { write-host $_ }
}
Test $data.arguments.values

This is a totally artificial way (more of a hack) of doing this but you could pass it as a hash table then self-call with the arguments splatted
function Test {
param (
[Parameter()]
[hashtable]$ht,
[String]$A,
[String]$B,
[String]$C
)
if($null -eq $ht){
Write-Host "A: $a"
Write-Host "B: $B"
Write-Host "C: $C"
}
else
{
Write-Host "ht.A: $($ht.a)"
Test -a $($ht.A+ " new blob") -b $ht.B -c $ht.C
Test #ht
}
}
$tdata = #{
arguments = #{
A = 'A string'
B = 'Another string'
C = 'The last string'
}
kruft = 'A string that doesn not need to get passed'
}
Write-Host 'Test ht' -ForegroundColor Yellow
Test -ht $tdata.arguments

Related

What is the "correct" way of passing parameters to powershell functions

Has anyone written the "PowerShell Gotchas for VBA Coders" guide? I am attempting to teach myself Powershell, I have some experience in VBA. What is the "correct" method of defining and passing parameters to functions.
Here is my test code:
Function Pass-Parameters1
{param( $s1, $s2)
write-host Pass-Parameters1 s1: $s1
Write-Host Pass-Parameters1 s2: $s2
return $s1 + $s2
}
Function Pass-Parameters2($ss1, $ss2){
Write-Host Pass-Parameters2 ss1: $ss1
Write-Host Pass-Parameters2 ss2: $ss2
return $ss1 + $ss2
}
$x = "Hello "
$y = "There!!"
$z = Pass-Parameters1 -s1 $x -s2 $y
$zz = Pass-Parameters2 $x, $y
$zzz = Pass-Parameters2 $x $y
Write-Host 1..Z = $z
write-host 1.ZZ = $zz
Write-Host 1ZZZ = $zzz
Here are the results:
Pass-Parameters1 s1: Hello
Pass-Parameters1 s2: There!!
Pass-Parameters2 ss1: Hello There!!
Pass-Parameters2 ss2:
Pass-Parameters2 ss1: Hello
Pass-Parameters2 ss2: There!!
1..Z = Hello There!!
1.ZZ = Hello There!!
1ZZZ = Hello There!!
Which is the recommended method, example 1 or example 2? I have a lot to learn about Powershell as $zz = Pass-Parameters2 $x, $y did not do what I expected, which is the way I would call the function in VBA. I am assuming $z = Pass-Parameters1 -s1 $x -s2 $y is the recommended method of calling the function as there is no ambiguity.
Any comments or suggestions welcome!
For specifying the parameters to a function I would choose the 2 form because to me 1 is overly verbose/I'm well used to .net languages where it's functionname(argument1,argument2)/the majority of C-like programming languages don't have a separate line inside the function that describes the parameters, but this is personal preference
You can provide a name of an argument, prefixed by hyphen, and provide the arguments in any order:
$z = Pass-Parameters1 -s1 $x -s2 $y
$z = Pass-Parameters1 -s2 $y -s1 $x
You can separate the arguments with spaces and provide the arguments in order:
$zzz = Pass-Parameters2 $x $y
Either of these is correct, and most languages have positional and named arguments. The advantage to using a named argument approach is if you don't want to specify all parameters. There is also the need to consider that power shell developers can force some arguments to be positional and others as named so sometimes you'll need to specify names
For example Write-Host can take an array of things to output as it's first argument and has parameters for what to separate them with as a named argument (third, as written). If you wanted to pass the array but not specify the second arg (as written, which controls new line behavior) at all you need to Write-Host $someArray -separator ":" - it mixes positional and named And needs to be presented thus because of the way positional/named has been specified by (Microsoft), otherwise write will just end up being given more things to output
If you're specifying all arguments, or only need to specify eg the first 3 of a 5 argument function, use whichever is more terse but readable. If you have a (terrible) habit of naming your string variables s1, s2, s3 then calling Add-Person -name $s1 -address $s2 -ssn $s3 keeps things readable. Or use good variable names, and Add-Person $name $address $ssn because it's more terse/redundant to specify parameter names when the variable does a pretty good job of describing the data. Consider using names if you're passing string literals: Add-Person "Markus Crescent" "Lee Street" "12345" - which is the name and which is the address (ok, its a stretch, but consider if these strings are just paths like "file to keep" and "file to delete")
This one turned your X and Y into an array, passed it into the first parameter and passed nothing for the second.. which Write-Host then duly printed the array contents on one line:
$zz = Pass-Parameters2 $x, $y
Your PowerShell code looks very unusual to PowerShell users imho. Here is an example, how I would format your functions:
function Pass-Parameters1 {
Param(
$s1,
$s2
)
Write-Host "Pass-Parameters1 s1: $s1"
Write-Host "Pass-Parameters1 s2: $s2"
return $s1 + $s2
}
function Pass-Parameters2($ss1, $ss2) {
Write-Host "Pass-Parameters2 ss1: $ss1"
Write-Host "Pass-Parameters2 ss2: $ss2"
return $ss1 + $ss2
}
Both versions are valid. In my experience, the first method is more common, because it is more clear, if you have many parameters with type definitions, switches, mandatory tags, default values, constraints, positional info or other additional info. Check out this documentation, to see how complex the definition of even one parameter can be.
You may be used to the syntax like function Pass-Parameters2($ss1, $ss2) from other languages, but as you will call functions in PowerShell like Pass-Parameters2 -ss1 "String1" -ss2 "String2" you won't stick to the also used call syntax from other languages like Pass-Parameters2("String1", "String2") anyway.
Thank you both Caius Jard and Thomas for your input. The vital piece of information I have discovered is Pass-Parameters2 $x, $y passes $x and $y as an array to the first variable in the function, whereas Pass-Parameters2 $x $y passes the two variables.
I need to read a few more basic tutorials on Powershell to learn the finer points of syntax and not think in VBA mode.
The arguments that you pass into the function are added to an array called 'args'. This args[] array can be used as below:
Function TestFunction(){
# access your arguments in the args array
Write-Host args[0]
}
TestFunction $param1 $param2 $param3
Or you can explicitly name the arguments:
Function TestFunction($test1, $test2, $test3){
# access your arguments in the args array
Write-Host $test1
}
TestFunction $param1 $param2 $param3

Function with undeclared parameters having priority over declared

I want to create log function that works similar to Write-Host in a manner that I can give it ad hoc arguments along with some parameters:
function log ( [int]$ident=0, [switch]$notime) {
$now = (Get-Date).ToString('s')
Write-Host $(if (!$NoTime) {now}) $($args | % { ' '*$ident*2 + $_ })
}
log 'test 1' 'test 2' # Cannot convert value "test 1" to type "System.Int32"
log 'test 1' 'test 2' -Ident 1 #Works
I know that I can get undeclared args with $args or use ValueFromRemainingArguments attribute but that requires me to change the way the function is called as declared function parameters will collect them.
Turn off positional binding:
function log {
[CmdletBinding(PositionalBinding=$False)]
param (
[int]$indent=0,
[switch]$notime,
[Parameter(ValueFromRemainingArguments)]
[string[]] $rest
)
$now = (Get-Date).ToString('s')
Write-Host $(if (!$NoTime) {$now}) $($rest | % { ' '*$indent*2 + $_ })
}
Note that, obviously, you are now required to include the names of the parameters, but that should be a good thing (it would be confusing to have to know whether 1 is the value of indent or the first value to log).

Choose from multiple return values

When I have a function, which uses several Write-Output commands and returns single number, how can I get number value in function caller code?
As far as I got, line
[int] $var = Get-MyNumber(...)
gets me the error
Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Int32"".
Probably because PowerShell returns an array of objects (containing Write-Output messages) back to caller code, where the assignment to the [int]-typed variable fails. Got that.
Now, how can I tell PowerShell that I'm interested in only a single value from those returned from the function, which is typed as [int].
BTW, I DON'T want to choose output by indexing the return array as I could mess up the indexes in the return array simply by adding another Write-Output line. (Which will happen sooner or later due to code maintenance).
Code
function f1() {
Write-Output "Lala"
return 5
}
[int] $x = f1
Write-Output $x
Results in the same error.
I see from your edit that you are using Write-Output to display a status message.
You should use Write-Host for that, or if you were using an advanced function, I would recommend using Write-Verbose and calling the function with -Verbose when you want to see the messages (see about_CommonParameters).
Updated code:
function f1() {
Write-Host "Lala"
return 5
}
[int] $x = f1
Write-Host $x
Advanced Function Example
function f1 {
[CmdletBinding()]
param()
Write-Verbose "Lala"
return 5
}
$x = f1
# The "Lala" message will not be seen.
$x = f1 -Verbose
# The "Lala" message will be seen.
Why Write-Output seems to work outside of a function:
Write-Output passes the input object to the caller. In the case of code that is executed directly in the host, and not in a function or cmdlet, the caller is the host, and the host decides what to do with it. In the case of powershell.exe (or ISE), it displays it.
Write-Host on the other hand, always writes to the host; it doesn't pass anything back to the caller.
Also note that Write-Output is, basically, optional. The following lines are equivalent:
Write-Output $x
$x

Invoke method on ScriptBlock truncating array

I have the following ScriptBlock defined:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4')
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
As the parameter states in the ScriptBlock, I want to be able to pass in an array of strings. When I call $strSb.Invoke(#('String11','String12')) I receive the following:
Passed in params:
String11
Final set:
String11
String3
String4
What I expect is:
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4
Why is the invoke method truncating my array to the first item entered? And how would I go about fixing it so I can pass in an array of strings?
FWIW: I'm working in v2 and v3.
The problem is that the Invoke method takes an array of arguments (kind of like commands that have an -ArgumentList parameter), so each element in your array is parsed as a separate argument. The first argument, 'String11', is assigned to the first postitional parameter, $Modules, and any subsequent arguments are discarded, since there are no more positional parameters. It doesn't matter that $Modules is declared as a string array; since each element of the argument list is a separate argument, you're setting $Modules to an array of one element.
If you use the , operator to indicate that you're passing in a single array argument, it works as intended:
$strSb.Invoke((,#('String11','String12')))
BTW, you don't really need the #, because a comma-separated list of strings is interpreted as an array by default. Not just in this particular context, but in general. So just use this:
$strSb.Invoke((,('String11','String12')))
To prove out the explanation above, try this scriptblock, which is the same except that a second parameter (creatively named $SecondParameter) is declared, and then displayed after the loop that displays the value of the first parameter:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4'),
[String]$SecondParameter
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
Write-Host "`nSecondParameter: $SecondParameter`n"
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
If you then pass in the arguments as you were, $strSb.Invoke(#('String11','String12')), you get these results:
11-26-13 19:02:12.55 D:\Scratch\soscratch» $strSb.Invoke(#('String11','String12'))
Passed in params:
String11
SecondParameter: String12
Final set:
String11
String3
String4
11-26-13 19:02:29.34 D:\Scratch\soscratch»
One last tip, not directly related to the question, is that you can compact the foreach loops by using a pipelines, which is are not only more succinct but generally more efficient. Here's a compacted version of your code:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = ('String3','String4')
)
Write-Host "Passed in params:"
$Modules | Write-Host
$defaultModules = 'String3','String4'
# Add Default Modules back if not present #
$defaultModules | ?{$Modules -notcontains $_} | %{$Modules += $_}
Write-Host "Final set:"
# Load Dependencies #
$Modules | Write-Host
}
If I understand what you're doing, you want to take 2 arrays, concatenate them, and ensure uniqueness...
First, Since you have a [Parameter...] on your parameter, you magically get [CmdletBinding()] on the method. This means that you are automatically going to get $Modules split into multiple calls.
Second, ScriptBlock.Invoke() takes a params style array and puts them into the method as separate arguments.
The first thing I would try is to add the attribute to gather all values:
[Parameter(ValueFromRemainingArguments=$true, Position=0, Mandatory=$true)]
[String[]]$Modules
However, for the Join, you can much more easily do something like:
($modules + $defaultModules) | Select -Unique
Not sure exactly why, but it doesn't seem to like that named parameter. Seems to like $args, tho :
[ScriptBlock]$strSb = {
$Modules = $args
Write-Host "Passed in params:"
foreach($m in $modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
$strSb.Invoke('String11','String12')
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4

Can I resolve PowerShell scriptblock parameters without invoking?

I'm looking at writing some PowerShell code that can either execute immediately, or produce the commands it would execute as generated scripts.
I'd like to avoid this scenario:
if($Generating){
write-Output "somecommand.exe"
}
else{
somecommand.exe
}
I got looking at ScriptBlocks, which at first looked promising because I can write the contents of the ScriptBlock to the console without executing it. Such as:
$sc = { somecommand.exe }
$sc
somecommand.exe
My specific question is, if my scriptblock contains parameters, can I get them to resolve when I'm writing the scriptblock contents to the console, but WITHOUT invoking the scriptblock?
For example given the following scriptblock:
$b2 = { Param([string]$P) Write-Host "$P" }
When I just type "$b2" at the console and hit enter I see this:
Param([string]$P) Write-Host "$P"
What I'd like to see is this (if the parameter value is "Foo"):
Param([string]$P) Write-Host "Foo"
I realize this can be done when it's invoked, either via "&" or using Invoke(), but would there be any way to get the parameters to resolve without invoking to make my script generation a little more elegant without needing a bunch of conditional statements throughout the code?
In PowerShell v3, you can get the param info via the AST property e.g.:
PS> $sb = {param($a,$b) "a is $a b is $b"}
PS> $sb.Ast.ParamBlock
Attributes Parameters Extent Parent
---------- ---------- ------ ------
{} {$a, $b} param($a,$b) {param($a,$b) "a...
Solution suitable for PowerShell v2:
# given the script block
$b2 = { Param([string]$P) Write-Host "$P" }
# make a function of it and "install" in the current scope
Invoke-Expression "function tmp {$b2}"
# get the function and its parameters
(Get-Command tmp).Parameters
When displaying a here-string with double quotes #" , it expands the variables. For the variables that should'nt expand, escape the variable with a backtick ( ` ).
So try this:
$P = "Foo"
$b2 = #"
{ Param([string]`$P) Write-Host "$P" }
"#
Test:
PS-ADMIN > $b2
{ Param([string]$P) Write-Host "Foo" }
If you want to convert it to scriptblock-type again:
#Convert it into scriptblock
$b3 = [Scriptblock]::Create($b2)
PS-ADMIN > $b3
{ Param([string]$P) Write-Host "Foo" }
PS-ADMIN > $b3.GetType().name
ScriptBlock
Using some of the suggestions I think I've found the best solution for my needs. Consider the following code
function TestFunc
{
Param(
[Parameter(Mandatory=$true)]
[string]$Folder,
[Parameter(Mandatory=$true)]
[string]$Foo
)
$code = #"
Write-Host "This is a folder $Folder"
Write-Host "This is the value of Foo $Foo"
"#
$block = [Scriptblock]::Create($code)
Write-Host "Running the block" -BackgroundColor Green -ForegroundColor Black
&$block
Write-Host "Displaying block code" -BackgroundColor Green -ForegroundColor Black
$block
}
And it's output:
Running the block
This is a folder c:\some\folder
This is the value of Foo FOOFOO
Displaying block code
Write-Host "This is a folder c:\some\folder"
Write-Host "This is the value of Foo FOOFOO"
By doing it this way, I still get all the benefit of keeping my existing functions and their parameters, parameter validation, CBH etc. I can also easily generate the code that the function would execute or just let it execute. Thanks for all the input, it's definitely been a good learning experience.
If you want to express your block as a block, not a string, the following works:
$printable = invoke-expression ('"' + ($block -replace '"', '`"') + '"')
Essentially, you're wrapping everything in quotes and then invoking it as an expression. The -replace call ensures any quotes in the block itself are escaped.
I'm using this in this handy function, which also halts execution if the invoked command failed.
# usage: exec { dir $myDir }
function exec($block)
{
# expand variables in block so it's easier to see what we're doing
$printable = invoke-expression ('"' + ($block -replace '"', '`"').Trim() + '"')
write-host "# $printable" -foregroundcolor gray
& $block
if ($lastExitCode -ne 0)
{
throw "Command failed: $printable in $(pwd) returned $lastExitCode"
}
}