6

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"
alastairtree
  • 3,960
  • 32
  • 49
  • Possible duplicate of [Pass arguments to a scriptblock in powershell](https://stackoverflow.com/questions/16347214/pass-arguments-to-a-scriptblock-in-powershell) – arco444 Mar 08 '19 at 13:14
  • 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 world` – Lee_Dailey Mar 08 '19 at 14:43
  • 1
    I dont believe this is a duplicate because i am not executing the script block - i want a string of its syntax with evaluated variables – alastairtree Mar 08 '19 at 14:57
  • @Lee_Dailey - as you can see from the answer, you can do that using `$ExecutionContext.InvokeCommand.ExpandString($b)` – alastairtree Mar 08 '19 at 16:52
  • @alastairtree - ha! i learned something new! [*grin*] i will delete this comment soon - and immediately delete my wrong comment to avoid confusing folks. – Lee_Dailey Mar 08 '19 at 17:00

1 Answers1

11

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.

AdminOfThings
  • 23,946
  • 4
  • 17
  • 27
  • Sorry, yeah meant $a not $b, although I think the question still stands. Updated the question – alastairtree Mar 08 '19 at 13:40
  • 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"` – alastairtree Mar 08 '19 at 14:51
  • 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. – AdminOfThings Mar 08 '19 at 14:56
  • 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. – alastairtree Mar 08 '19 at 15:01
  • 4
    Found a short answer. Just run this `$executioncontext.invokecommand.expandstring($b)`. – AdminOfThings Mar 08 '19 at 15:51
  • @AdamOfThings - you got it, thanks, it is `$ExecutionContext.InvokeCommand.ExpandString($b)` which works on any given script block. – alastairtree Mar 08 '19 at 16:40
  • 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](https://stackoverflow.com/questions/12574146/what-exactly-is-a-powershell-scriptblock/33581753#33581753). – KrisG Sep 04 '21 at 01:05
  • A promising approach might be to [extract variables from the SessionState Module](https://github.com/pester/Pester/issues/1227#issuecomment-456807983) and then use [ScriptBlock.InvokeWithContext](https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.invokewithcontext?view=powershellsdk-7.0.0). Would probably need to walk the [ScriptBlock AST](https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.scriptblock.ast?view=powershellsdk-7.0.0) to correctly substitute variables with strings. – KrisG Sep 05 '21 at 08:08