How to resolve variables in a Powershell script block
Asked Answered
C

2

8

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"
Cytolysin answered 8/3, 2019 at 12:48 Comment(5)
Possible duplicate of Pass arguments to a scriptblock in powershellAustral
you have a scriptblock and you need to invoke/run the scriptblock. [grin] simply calling the $Var that holds the scriptblock will give the literal content, not run it. you can run it thus ... Invoke-Command -ScriptBlock $b output = hello worldGlorify
I dont believe this is a duplicate because i am not executing the script block - i want a string of its syntax with evaluated variablesCytolysin
@Glorify - as you can see from the answer, you can do that using $ExecutionContext.InvokeCommand.ExpandString($b)Cytolysin
@Cytolysin - ha! i learned something new! [grin] i will delete this comment soon - and immediately delete my wrong comment to avoid confusing folks.Glorify
I
14

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.

Insula answered 8/3, 2019 at 13:19 Comment(8)
Sorry, yeah meant $a not $b, although I think the question still stands. Updated the questionCytolysin
This is not correct and misses the crus of the question - printing $b just prints write-host "hello $a" and the question asks for write-host "hello world"Cytolysin
Did you read through the post and try the different scenarios? You have to have quotes around everything inside of the scriptblock to print the entire text, which is there in my examples.Insula
I understand but you have changed the question to replace the contents of the script block with a string. Instead the question assumes you already have a script block, and want to print its contents without executing it.Cytolysin
Found a short answer. Just run this $executioncontext.invokecommand.expandstring($b).Insula
@AdamOfThings - you got it, thanks, it is $ExecutionContext.InvokeCommand.ExpandString($b) which works on any given script block.Cytolysin
This approach doesn't work for script blocks that contain assignment or local variables, e.g.: $ExecutionContext.InvokeCommand.ExpandString({$x=1; $x}) yields =1;. I am looking for a way to truly serialize a closure to include all referenced variables so that I can execute it outside of the current execution context. Not sure if this is possible as PowerShell uses dynamic scoping for closures according to what-exactly-is-a-powershell-scriptblock.Ingrowing
A promising approach might be to extract variables from the SessionState Module and then use ScriptBlock.InvokeWithContext. Would probably need to walk the ScriptBlock AST to correctly substitute variables with strings.Ingrowing
I
0

Another way to do it is:

$a = "world"
$b = "write-host ""hello $a"""
$b

The output is:

write-host "hello world"
Insensate answered 29/7 at 11:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.