2

Keeping in mind the XY problem, here are my X and Y problems.

X Problem

There are PowerShell scripts, the contents of which I have obfuscated. I am still able to run the script, however.

How this is working is that the scripts are stored inside an executable. PowerShell can execute the executable, asking for a particular script by passing in that script's name as a CLI parameter. The executable's process will then forward the PowerShell script's content through stdout. PowerShell stores the stdout in a string variable, then calls Invoke-Expression, passing in the string, running the obfuscated script.

The problem is that $PSScriptRoot, when it occurs in the obfuscated scripts, is evaluated as $null.

Y Problem

When a PowerShell script, script.ps1 executes a PowerShell script as a string $scriptString, how to get $PSScriptRoot to evaluate as if it were evaluated in script.ps1.

Test 1

# script.ps1

$PSScriptRoot

# Output: Path to directory containing `script.ps1`.

This demonstrates that $PSScriptRoot is non-$null in at least one case.

Test 2

# script.ps1

Invoke-Expression '$PSScriptRoot'

# Output: Nothing

Demonstrates that the way that I am currently executing the string does not work.

Test 3

# script.ps1

. { $PSScriptRoot }

# Output: Path to directory containing `script.ps1`.

Creating a script block using the { ... } syntax does work. This requires the actual commands, however. What I have is a string containing the commands. Putting that string between the curly braces would cause commands to be output rather than run.

Test 4

# script.ps1

. [ScriptBlock]::Create('$PSScriptRoot')

# Output: Nothing

The other way to create a script block is with the C# [ScriptBlock]::Create(...) method. This actually allows creating a script block from a string. Unfortunately, it does not work.

Test 5

# script.ps1

Invoke-Command -ScriptBlock { $PSScriptRoot }

# Output: Path to directory containing `script.ps1`.

Same as test 3, except calling Invoke-Command on the script block rather than dot-sourcing the script block.

Test 6

# script.ps1

$scriptBlock = [ScriptBlock]::Create('$PSScriptRoot')
Invoke-Command -ScriptBlock $scriptBlock

# Output: Nothing.

Same as test 3, except calling Invoke-Command on the script block rather than dot-sourcing the script block.

Current belief about the problem.

  • The only way that I know of to execute a string-script as a string directly is the Invoke-Expression cmdlet, which does not work. Maybe there is another way to execute a string directly which might work, but I do not know about.
  • The other way to execute the string-script is to convert the string into a script block, then execute the script block. There seem to be two ways of creating a script block, one which works and one which does not work.
    • Creating a script block using the { ... } syntax seems to work, but it does not appear to work with strings.
    • Creating a script block using the [ScriptBlock]::Create(...) method does not work, but works with strings.

Questions:

  • Is there a way to execute a string directly which will work without calling Invoke-Expression?
  • Is there a way to create a script block from a string using the { ... } syntax?
  • What is the reason/mechanism behind the test scenarios working/not working?
  • Most importantly, how can we execute a string script and get $PSScriptRoot to evaluate to the proper, non-$null value?
14
  • 1
    Try using double quotes Commented May 13, 2021 at 1:24
  • To construct the string or for some other reason? The string is constructed properly, so that is not the issue. Commented May 13, 2021 at 1:40
  • @AbrahamZinala, this appears to be a problem with scope. $PSScriptRoot is available in the script scope. A script block created with { ... } has access to the $PSScriptRoot script-scope variable, whereas a script block created with [ScriptBlock]::Create(...). Does not have access to the $PSScriptRoot script-scope variable. { ... } solves the scope problem except that only commands can be entered in the { ... } syntax. There does not appear to be a way to get the commands in a string to be the commands of a script block without using the [ScriptBlock]::Create() method. Commented May 13, 2021 at 2:29
  • 1
    iex 'Split-Path $MyInvocation.InvocationName' Commented May 13, 2021 at 2:52
  • and then there is that - @SantiagoSquarzon to the rescue :) Commented May 13, 2021 at 2:53

1 Answer 1

6

You can make PowerShell treat a script stored in a string as if it had been read off disk - that is, $PSScriptRoot and $PSCommandPath will resolve correctly - by manually parsing it:

# Prepare script + path
$script = 'Write-Host "Script root:  $PSScriptRoot`nCommand path: $PSCommandPath"'
$fileName = "C:\my\path\to\script.ps1"

# Make the parser generate an AST from the input script + path
$AST = [System.Management.Automation.Language.Parser]::ParseInput($script, $fileName, [ref]$null, [ref]$null)

# Get an executable scriptblock from AST
$block = $AST.GetScriptBlock()

# Now invoke it
& $block

At which point you'll find the $PS* variables have been resolved correctly:

Script root:  C:\my\path\to
Command path: C:\my\path\to\script.ps1
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.