I am attempting to invoke a scriptblock in Powershell from a value within a PSD1 imported through Import-PowerShellDataFile
.
Let's take the simplest expression (no data file) of executing a scriptblock within a hashtable. It works as expected.
$Config = @{
test = {Write-output "Hello"}
}
$config.test.invoke()
#output: Hello
Now, if I add the following intermediary step, it fails
$tmpConfig = Import-PowerShellDataFile -Path tmpconfig.psd1
$tmpconfig.test.Invoke()
#Output: Write-Output "Hello"
I cannot find an explanation for the difference in behavior.
Any idea what is happening there and how to remediate it ?
I noticed that the imported from PowershellData file .ToString() output is wrapped in an additional pair of curly braces, which would essentially be $t ={{Write-Output 'Hello'}}
and while I'd need to essentially invoke twice, I don't understand the context of why the PowershellDataFile double-wrap (or maybe it is something else) the scriptblock like that.
For reference, my output and the simplest script to view this pecular scenario playing out.
# Imported from PowershellData
ConfigType : System.Collections.Hashtable
Value : {Write-output "Hello"}
ValueType : System.Management.Automation.ScriptBlock
Result : Write-output "Hello"
ResultType : System.Management.Automation.ScriptBlock
# Not imported.
ConfigType : System.Collections.Hashtable
Value : Write-output "Hello"
ValueType : System.Management.Automation.ScriptBlock
Result : Hello
ResultType : System.String
$tmpconfigstring = @'
@{
test = { Write-output "Hello" }
}
'@
$tmpconfigstring | Out-File tmpconfig.psd1
$tmpConfig = Import-PowerShellDataFile -Path tmpconfig.psd1
$tmpconfig2 = ([scriptblock]::Create($tmpconfigstring)).InvokeReturnAsIs()
$Result = $tmpConfig.test.InvokeReturnAsIs()
$Result2 = $tmpconfig2.test.InvokeReturnAsIs()
[PSCustomObject]@{
'ConfigType' = $tmpConfig.GetType()
'Value' = $tmpConfig.test.ToString()
'ValueType' = $tmpconfig.test.GetType()
'Result' = $Result
'ResultType' = $Result.GetType()
} | fl
[PSCustomObject]@{
'ConfigType' = $tmpConfig2.GetType()
'Value' = $tmpConfig2.test.ToString()
'ValueType' = $tmpconfig2.test.GetType()
'Result' = $Result2
'ResultType' = $Result2.GetType()
} | fl
It is possible they're taking this path for security reasons, hinted out in the documentation:
The
Import-PowerShellDataFile
cmdlet safely imports key-value pairs from hashtables defined in a.PSD1
file. The values could be imported usingInvoke-Expression
on the contents of the file. However,Invoke-Expression
runs any code contained in the file.
It is also possible this was just a mistake. In SafeValues.cs#L793-L796
they're creating a new scriptblock from the Extent:
public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
{
return ScriptBlock.Create(scriptBlockExpressionAst.Extent.Text);
}
Instead of accessing the ScriptBlockAst
and getting a new scriptblock using GetScriptBlock()
:
public object VisitScriptBlockExpression(ScriptBlockExpressionAst scriptBlockExpressionAst)
=> scriptBlockExpressionAst.ScriptBlock.GetScriptBlock();
Then the result would be similar to what we get using Invoke-Expression
:
'@{ test = { 1 + 1 } }' | Out-File tmpconfig.psd1
$tmpConfig = Get-Content .\tmpconfig.psd1 -Raw | Invoke-Expression
$tmpConfig['test'].InvokeReturnAsIs() # 2
If you want to dive deeper, they're using the Parser
to get the hashtable from the psd1
, see ImportPowerShellDataFile.cs#L60-L70
.
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
'tmpconfig.psd1',
[ref] $null,
[ref] $null)
$hashAst = $ast.Find(
{ $args[0] -is [System.Management.Automation.Language.HashtableAst] }, $false)
From here they use SafeGetValue()
to get a hashtable from the HashtableAst
:
$hashtable = $hashAst.SafeGetValue()
$hashtable['test'].InvokeReturnAsIs().InvokeReturnAsIs() # 2
This code path also uses SafeGetValue()
over the ScriptBlockExpressionAst
to get the scriptblock from the dictionary entry value, which wraps the scriptblock in another scriptblock as was shown before:
$sb = $hashAst.KeyValuePairs[0].Item2.PipelineElements[0].Expression.SafeGetValue()
$sb.InvokeReturnAsIs().InvokeReturnAsIs() # 2