How to resolve variables in a Powershell script block - powershell

Given I have:
$a = "world"
$b = { write-host "hello $a" }
How do I get the resolved text of the script block, which should be the entre string including write-host:
write-host "hello world"
UPDATE: additional clarifications
If you just print $b you get the variable and not the resolved value
write-host "hello $a"
If you execute the script block with & $b you get the printed value, not the contents of the script block:
hello world
This question is seeking a string containing the contents of the script block with the evaluated variables, which is:
write-host "hello world"

As in the original question, if your entire scriptblock contents is not a string (but you want it to be) and you need variable substitution within the scriptblock, you can use the following:
$ExecutionContext.InvokeCommand.ExpandString($b)
Calling .InvokeCommand.ExpandString($b) on the current execution context will use the variables in the current scope for substitution.
The following is one way to create a scripblock and retrieve its contents:
$a = "world"
$b = [ScriptBlock]::create("write-host hello $a")
$b
write-host hello world
You can use your scriptblock notation {} as well to accomplish the same thing, but you need to use the & call operator:
$a = "world"
$b = {"write-host hello $a"}
& $b
write-host hello world
A feature to using the method above is that if you change the value of $a at any time and then call the scriptblock again, the output will be updated like so:
$a = "world"
$b = {"write-host hello $a"}
& $b
write-host hello world
$a = "hi"
& $b
write-host hello hi
The GetNewClosure() method can be used to create a clone of the scriptblock above to take a theoretical snapshot of the scriptblock's current evaluation. It will be immune to the changing of the $a value later the code:
$b = {"write-host hello $a"}.GetNewClosure()
& $b
write-host hello world
$a = "new world"
& $b
write-host hello world
The {} notation denotes a scriptblock object as you probably already know. That can be passed into Invoke-Command, which opens up other options. You can also create parameters inside of the scriptblock that can be passed in later. See about_Script_Blocks for more information.

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

Start-Job - Positional order of arguments

I'm trying to understand the correct order of arguments when using Start-Job. What is the correct way to supply parameters to a PowerShell job?
I would expect this to print hello world, but it prints world hello.
Is Param() or -ArgumentList the issue here?
$foo = "hello"
$bar = "world"
$job = Start-Job -ScriptBlock {
Param(
$foo,
$bar
)
Write-Host $foo
Write-Host $bar
} -ArgumentList $bar, $foo
Receive-Job $job
Output:
world
hello
The argument of the parameter -ArgumentList is an array, whose values are passed to parameters defined inside the scriptblock in positional order. You're confused about the result you're getting, because you apparently expected your global variables to be mapped to the parameter names you defined in your scriptblock. That is not how this works.
To maybe illustrate a bit better what is happening in your example let's use distinct variable names in the scriptblock and global scope:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Param(
$c,
$d
)
Write-Host $c
Write-Host $d
} -ArgumentList $b, $a
Essentially, the names of the parameters have nothing to do with the names of the variables in the global scope.
You're switching the values when you're passing $b, $a to the scriptblock instead of $a, $b, hence the value of $b is passed to $c and the value of $a is passed to $d.
Normally one would use splatting for mapping values to specific named parameters. However, that won't work here, since -ArgumentList expects an array of values, not a hashtable. If the difference between positional and named parameters is not clear to you please have a look at the documentation.
What you can do if you want to use the same variable names inside and outside the scriptblock is use the using: scope qualifier instead of passing the variables as arguments:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Write-Host $using:a
Write-Host $using:b
}

Passing in $args variable to function

I have a script tester.ps1; the first thing it does is call a function (defined within the script itself) called main.
I need to pass in the automatic variable $args that was passed into it from the commandline.
How do I do this?
The following doesn't seem to work:
#Requires -Version 5.0
#scriptname: tester.ps1
function main($args) {
Write-Host $args
}
# Entry point
main $args
When I save this tester.ps1 and call it, the function doesn't see the passed-in parameter?
PS> . .\tester.ps1 hello world
From entry point: hello world
From Function:
In your example, simply removing $args from the main function declaration, would be enough to have the output you want.
Though, be aware that if you want to pass parameters by name, you need to call main with the splatting operator #, for example:
#Requires -Version 5.0
#scriptname: tester.ps1
function main($myString, $otherVar) {
Write-Host $myString
}
# Entry point
Write-Host "Not using splatting: " -NoNewline
main $args
Write-Host "Using splatting: " -NoNewline
main #args
Output:
PS> . .\test.ps1 -myString "Hi World" -otherVar foobar
Not using splatting: -myString Hi World -otherVar foobar
Using splatting: Hi World
Find more about the splatting operator # here
Based on Jeroen Mostert's comment*; the solution is below.
Basically I was incorrectly trying to 'overload' or 'shadow' the built-in $arg variable.
I just need to have a param with a different name like this:
#Requires -Version 5.0
function main($my_args) {
write-host "From Function:" $my_args
}
# Entry point
write-host "From entry point:" $args
main $args
> . .\tester.ps1 hello world
From entry point: hello world
From Function: hello world

How do I share variables among scripts in Powershell?

I have difficulty trying to find an answer that solves this issue online.
I have a script which runs all my other scripts in a particular order.
$x=0;
$z=0;
cd *file path* ;
.\filename1.ps1 ; write-host "$x text $z text";
.\filename2.ps1 ; write-host "$x text $z text";
In each of these scripts I have options that will add 1 to either variable $x or variable $z
$a=Read-Host
if ($a -eq "Option One") {$x = $x+1}
elseif ($a -eq "Option Two") {$z = $z+1}
else {Write-Host "Not a valid option" ; .\filenameX.ps1}
The issue is that the script that runs all these scripts won't recognise the change in variable. How do I fix this?
The naïve answer is to "dot-source" these scripts, i.e. to invoke them with operator . 
Using .  executes the scripts in the caller's variable scope, so that top-level modifications of $x an $z will be visible even after .\filename1.ps1 and .\filename2.ps1 have completed.
# Note the `. ` preceding the file path - the space after "." is mandatory
. .\filename1.ps1 ; "$x text $z text"
. .\filename2.ps1 ; "$x text $z text"
Note, however, that all top-level variables created or modified in . -invoked scripts will be visible to the caller.
For more on variable scopes in PowerShell, see this answer.
Better encapsulated options are to either (a) output modified values or, less commonly, (b) use of [ref] parameters to pass by-reference variables to scripts - whose parameters must be declared and assigned to accordingly.
If you define you x (and z) variable with a global scope outside your scripts like this:
$global:x=0.
You can increment it inside your scripts like this:
$global:x = $global:x + 1

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"
}
}