vscode-powershell: `$PSScriptRoot` is not populated when running a code block (via F8)

System Details

  • Operating system name and version: Windows 10
  • VS Code version: 1.10.2
  • PowerShell extension version:
  • Output from $PSVersionTable:
PS C:\Source\neo4j-quick-demo> $pseditor.EditorServicesVersion

Major  Minor  Build  Revision
-----  -----  -----  --------
0      11     0      0


PS C:\Source\neo4j-quick-demo>
PS C:\Source\neo4j-quick-demo> code --list-extensions --show-versions
PS C:\Source\neo4j-quick-demo>
PS C:\Source\neo4j-quick-demo> $PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.14393.953
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.14393.953
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

Issue Description

$PSScriptRoot is not populated when running a code block (via F8)

I am trying load DLLs that are in the same directory as the PowerShell script and use $PSScriptRoot to get the location for them.

When running the code in PowerShell it’s fine, but using VSCode, when running the code snippet via F8, the variable is not populated.

Repro

  • Open VSCode and create a PowerShell (.ps1) file
  • Add the following command Write-Host "$PSScriptRoot\abc"
  • Select the newly created line and Press F8 to execute the PowerShell in the integrated terminal

Expected result: <full working path>\abc

e.g. If the script I was editing was in C:\Source I would expect the result to be C:\Source\abc

Actual result: \abc

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 9
  • Comments: 78 (13 by maintainers)

Most upvoted comments

Sure that’s a fine working practice but there is many a time when you’ve got a script you’ve written that doesn’t need testing and doesn’t need splitting into loads of little chunks that you just want to run a little bit of it. Not being able to use psscriptroot is just annoying.

Simon Sabin


From: Justin Grote @.> Sent: Friday, May 21, 2021 12:53:37 AM To: PowerShell/vscode-powershell @.> Cc: Simon Sabin @.>; Mention @.> Subject: Re: [PowerShell/vscode-powershell] $PSScriptRoot is not populated when running a code block (via F8) (#633)

Or do your testing by writing it into a pester test and using the run test or debug test codelens rather than f5/f8

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/PowerShell/vscode-powershell/issues/633#issuecomment-845556082, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AAJHM22UYXHISLVXTGHFSELTOWOIDANCNFSM4DFXJ4TQ.

Yes, it is still an issue for F8. I’m not aware that this is an issue when debugging. In fact, I can’t repro this using F5.

@TylerLeonhardt This should probably be retagged as an enhancement, since it’s technically working as expected.

The ISE-Compatibility tag should also be removed as ISE does the same thing with F8 (unless I’m missing something) so it’s not a user experience compatibility thing.

image

I think this must be be related:

  • $PSScriptRoot shows an empty string as tooltip when debugging in VS Code,
  • when you try to examine it while the script is paused during a debug session, by manually typing in ‘$PSScripRoot’ (or Write-Host $PSScriptRoot or Write-Output $PSScriptRoot) in the integrated console window, an empty string is also returned.
  • but when script code using it is executed, either as a whole or line by line when single stepping, the correct path is used.

I’d be happy if it wasn’t supported, but somehow I was warned that “Stuff may not do what you expect” if it saw those tokens in the text.

One could argue that since F8 simply runs the selected code in global scope, that it is expected that $PSScriptRoot would not be defined. This is how F8 works in ISE i.e.$PSScriptRoot is not defined.

Thanks for reminding me! I need to get that fixed.

I suppose what bothers me about $PSScriptRoot and $PSCommandPath is that they are not idempotent. They are populated if they are 1) part of a script and 2) run as a script. Even if it’s not run as a script, the script still has a parent folder. It seems misleading to report it as not having a parent folder. Automatic variables that depend upon context this way are going to introduce complexity. I understand that it’s complex to simplify the automatic variable. However, such complexity is done once and maintained by one team.

Here’s one of my current work arounds. Not sure how many more I’ll have. Other developers will have their own variations. I think it would be nice to have just one version that just worked without thinking about it. I wonder how many people think about this.

$cmd = $PSCommandPath # blank if F8
if ( -not $cmd -and $script:psEditor ) { $cmd = $script:psEditor.GetEditorContext().CurrentFile.Path }
if ( -not $cmd -and $script:psISE ) { $cmd = $script:psISE.CurrentFile.FullPath }
$root = Split-Path $cmd -Parent

I was wondering if this could be solved by the debug capability rather than built in F8. i.e. being able to specify something in launch.json I however couldn’t find a way to have a script run, the script property of launch.json only seems to take a file.

By Design, but not always as expected. There are enough people that fall fowl of this and thus it would be good to have a solution. I understand the rationale, but when you do F8 and you are in a script the expectation is that $psscriptroot is that script. The fact the implementation of F8 doesn’t run that script, is a detail of implementation/powershell that the whole script isn’t being run.

@RandyInMarin there isn’t a great option for relative paths that works everywhere. $PSScriptRoot is recommended if the code will always be run as a script and you are explicitly referencing files relative to the script, because the paths will be relative to the script. Using ./relative/path is not recommended if you explicitly want something relative to where the script is located because it will be relative to the working directory, so if someone starts the script in a different path, it will break.

You may want to consider consolidating your functions into a Powershell module, that way you just import the module and don’t have to reference a lot of paths relative to the script.

I’m going to close this as “working as expected”

@RandyInMarin for Powershell at least, there are several variables like $PSScriptRoot that are only present in the context of a script, not within an interactive session.

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.2#myinvocation image

When you do F8, you are basically “cut pasting” into the terminal, so the current behavior is exactly what happens outside of vscode at a normal terminal.

Like I said, an opt-in setting to populate these to ease development would make sense, if someone wants to attempt a PR I’m sure we could massage it to get accepted, but it definitely wouldn’t be the default.

Or do your testing by writing it into a pester test and using the run test or debug test codelens rather than f5/f8

For debugging, what you noted was before the major omnisharp refactor. I’d say debugging F8 on a per-line basis would be very hard, because it’s just running commands directly into the terminal, not saving the file and executing it, so there’s no way for the debugger to determine if the breakpoints been hit or not.

It’s possible actually, @TylerLeonhardt and I talked about this awhile ago iirc.

Line breakpoints are based on the file path associated with an AST (e.g. $ast.Extent.File) which can be fudged a bit if you create the scriptblock to be invoked via Parser.ParseInput(string input, string fileName, ...).GetScriptBlock(). The dirty part is having to arbitrarily pad the script extent so that the offsets/line/column all match. This is the same way my example of F8 PSScriptRoot works.

It should even be technically possible for untitled files in the latest PowerShell. That’s assuming the change went through that allows a Breakpoint object to be created with an arbitrary file string (e.g. an non-existing or generally invalid path). @TylerLeonhardt you know if that change went through?

I’d say that 2 would be confusing. the text is not part of a script and thus psscriptroot should be empty. If you want psscriptroot to be populated save the file.

I really can’t see where you would be using psscriptroot in an file that isn’t saved to disk.

I guess with “application code” you don’t have a concept of F8 and you generally don’t use relative references, they are defined elsewhere in the “project”.

@simonsabin Maybe? My original issue was with “application code” trying to load vendored DLLs. If the code has no concept of where it’s running from then loading dependencies becomes very difficult.

I’ll set a Breakpoint and use the debugging tool to run either the script itself or a pester test that calls the script, works just fine.

@JustinGrote While this may be a workaround, it’s not really feasible to do that for every time you run F8

This should probably be retagged as an enhancement, since it’s technically working as expected.

I’m happy with that. It’s somewhat trivial to create (I think) a runspace with no script file e.g.

test.ps1

Write-Host "Outside Block ScriptRoot = $PSScriptRoot"

Invoke-Command {
  Write-Host "Inside Block ScriptRoot = $PSScriptRoot"
}

Invoke-Expression 'Write-Host "Inside IEX ScriptRoot = $PSScriptRoot"'
C:\Source\tmp> .\test.ps1
Outside Block ScriptRoot = C:\Source\tmp
Inside Block ScriptRoot = C:\Source\tmp
Inside IEX ScriptRoot =
C:\Source\tmp>

Very clever solution! If we tried that, one thing we’d want to do is send the EditorContext along with the run selection request so that it doesn’t have to be fetched from within PowerShell, saving another round trip. If the editor sends the EditorContext with the request we can take this approach, otherwise go with the original approach.

Fixing this is a little bit more difficult than it may appear. The big problem with this one is that it’s basically impossible to set the PSScriptRoot variable manually because it’s replaced by the engine in every single scope.

That said, the engine creates the variable based on the ScriptExtent for the command. With the Parser API, you can parse specific input and specify a file source. Here’s a proof of concept editor command preserves MyInvocation, PSScriptExtent, and position info for breakpoints.

Register-EditorCommand -Name TestingF8 -DisplayName 'Run selected text and preserve extent' -ScriptBlock {
    [System.Diagnostics.DebuggerHidden()]
    [System.Diagnostics.DebuggerStepThrough()]
    [CmdletBinding()]
    param()
    end {
        function __PSES__GetScriptBlockToInvoke {
            $context = $psEditor.GetEditorContext()
            $extent = $context.SelectedRange | ConvertTo-ScriptExtent

            $newScript = [System.Text.StringBuilder]::new().
                Append([char]' ', $extent.StartOffset - $extent.StartLineNumber - $extent.StartColumnNumber).
                Append([char]"`n", $extent.StartLineNumber - 1).
                Append([char]' ', $extent.StartColumnNumber - 1).
                Append($extent.Text).
                ToString()

            try {
                $errors = $null
                return [System.Management.Automation.Language.Parser]::ParseInput(
                    <# input:    #> $newScript,
                    <# fileName: #> $Context.CurrentFile.Path,
                    <# tokens:   #> [ref] $null,
                    <# errors:   #> [ref] $errors).
                    GetScriptBlock()
            } catch [System.Management.Automation.PSInvalidOperationException] {
                $exception = New-Object System.Management.Automation.ParseException($errors)
                $PSCmdlet.ThrowTerminatingError(
                    (New-Object System.Management.Automation.ErrorRecord(
                        <# exception:     #> $exception,
                        <# errorId:       #> 'RunSelectionParseError',
                        <# errorCategory: #> 'ParserError',
                        <# targetObject:  #> $newScript)))
            }
        }

        try {
            return . (__PSES__GetScriptBlockToInvoke)
        } catch {
            if ($PSItem -is [System.Management.Automation.ErrorRecord]) {
                $PSCmdlet.ThrowTerminatingError($PSItem)
                return
            }

            $PSCmdlet.ThrowTerminatingError(
                (New-Object System.Management.Automation.ErrorRecord(
                    <# exception:     #> $PSItem,
                    <# errorId:       #> 'RunSelectionRuntimeException',
                    <# errorCategory: #> 'NotSpecified',
                    <# targetObject:  #> $null)))
        }
    }
}

Yeah, F8 could just insert the script’s parent dir into the session as $PSScriptRoot right before running the snippet. I believe people used to complain that the ISE did not do this. Might be nice to make it work.

However, I’ve been considering using VS Code’s built in Run Selection in Terminal command instead of my own custom F8 implementation. This would mean that I wouldn’t be able to do the $PSScriptRoot injection. However, I don’t have a good reason to do that other than just removing “unnecessary” code. If the $PSScriptRoot injection is important enough (which it might be for interactive dev workflow) then I can still keep the current F8.

Thoughts?