4

i have a number of strings like this this that i would like powershell to reevaluate\convert to an array (like what would happen if you just wrote the same code in ISE without the single quotes).

$String = '@("a value","b value","c value")'

Is there a easier way to do this than stripping the '@' & '()' out of the string and using -split?

Thanks for the help in advance.

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
Pete
  • 91
  • 5
  • 1
    I'm not sure what you mean with *"easier"* but you could use the according methods instead of the operators lie this: `$String.Trim('@()').Split(',')` – Olaf Dec 26 '21 at 12:45
  • 3
    Extremely naughty [Invoke-Expression](https://devblogs.microsoft.com/powershell/invoke-expression-considered-harmful/), use at your own risk version without trimming - `($String | iex).Split(',')` _[Eyes]_ – Ash Dec 26 '21 at 13:10
  • Can the values contain embedded `"` and `,`? – zett42 Dec 26 '21 at 13:28
  • 4
    @Ash `$String | iex` is enough. Split is not needed here as the result is already the array. – Sage Pourpre Dec 26 '21 at 13:30
  • 1
    @SagePourpre Doh, of course. Haha. Not fully with it today. – Ash Dec 26 '21 at 13:48

1 Answers1

7

As long as the string contains a valid expression, you can use the [scriptblock]::Create(..) method:

$String = '@("a value","b value","c value")'
& ([scriptblock]::Create($String))

Invoke-Expression would also work and in this case would be mostly the same thing.


However, the nice feature about Script Blocks, as zett42 pointed out in a comment, is that with them we can validate that arbitrary code execution is forbidden with it's CheckRestrictedLanguage method.

In example below, Write-Host is an allowed command and a string containing only 'Write-Host "Hello world!"' would not throw an exception however, assignment statements or any other command not listed in $allowedCommands will throw and the script block will not be executed.

$String = @'
Write-Host "Hello world!"
$stream = [System.Net.Sockets.TcpClient]::new('google.com', 80).GetStream()
'@

[string[]] $allowedCommands  =  'Write-Host'
[string[]] $allowedVaribales =  ''

try {
    $scriptblock = [scriptblock]::Create($String)
    $scriptblock.CheckRestrictedLanguage(
        $allowedCommands,
        $allowedVaribales,
        $false # Argument to allow Environmental Variables
    )
    & $scriptblock
}
catch {
    Write-Warning $_.Exception.Message
}

Another alternative is to run the expression in a Runspace with ConstrainedLanguage Mode. This function can make it really easy.

using namespace System.Management.Automation.Language
using namespace System.Collections.Generic
using namespace System.Management.Automation.Runspaces

function Invoke-ConstrainedExpression {
    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(ParameterSetName = 'Command', Mandatory, ValueFromPipeline, Position = 0)]
        [string] $Command,

        [Parameter(ParameterSetName = 'ScriptBlock', Mandatory, Position = 0)]
        [scriptblock] $ScriptBlock,

        [Parameter()]
        [Management.Automation.PSLanguageMode] $LanguageMode = 'ConstrainedLanguage',

        # When using this switch, the function inspects the AST to find any variable
        # not being an assigned one in the expression, queries the local state to find
        # it's value and injects that variable to the Initial Session State of the Runspace.
        [Parameter()]
        [switch] $InjectLocalVariables
    )

    process {
        try {
            $Expression = $ScriptBlock
            if($PSBoundParameters.ContainsKey('Command')) {
                $Expression = [scriptblock]::Create($Command)
            }

            # bare minimum for the session state
            $iss = [initialsessionstate]::CreateDefault2()
            # set `ContrainedLanguage` for this session
            $iss.LanguageMode = $LanguageMode

            if($InjectLocalVariables.IsPresent) {
                $ast = $Expression.Ast
                $map = [HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
                $ast.FindAll({ $args[0] -is [AssignmentStatementAst] }, $true).Left.Extent.Text |
                    ForEach-Object { $null = $map.Add($_) }

                $variablesToInject = $ast.FindAll({
                    $args[0] -is [VariableExpressionAst] -and
                    -not $map.Contains($args[0].Extent.Text)
                }, $true).VariablePath.UserPath

                foreach($var in $variablesToInject) {
                    $value = $ExecutionContext.SessionState.PSVariable.GetValue($var)
                    $entry = [SessionStateVariableEntry]::new($var, $value, '')
                    $iss.Variables.Add($entry)
                }
            }

            # create the PS Instance and add the expression to invoke
            $ps = [powershell]::Create($iss).AddScript($Expression)
            # invoke the expression
            [Collections.Generic.List[object]] $stdout = $ps.Invoke()
            $streams = $ps.Streams
            $streams.PSObject.Properties.Add([psnoteproperty]::new('Success', $stdout))
            $streams
        }
        finally {
            if($ps) { $ps.Dispose() }
        }
    }
}

Now we can test the expression using Constrained Language:

Invoke-ConstrainedExpression {
    Write-Host 'Starting script'
    [System.Net.WebClient]::new().DownloadString($uri) | iex
    'Hello world!'
}

The output would look like this:

Success     : {Hello world!}
Error       : {Cannot create type. Only core types are supported in this language mode.}
Progress    : {}
Verbose     : {}
Debug       : {}
Warning     : {}
Information : {Starting script}

DevBlogs Article: PowerShell Constrained Language Mode has some nice information and is definitely worth a read.

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37