Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
glennsarti opened this issue Mar 30, 2017 · 78 comments
Open

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

glennsarti opened this issue Mar 30, 2017 · 78 comments
Labels
Area-Extension Terminal Issue-Enhancement A feature request (enhancement). Up for Grabs Will shepherd PRs.

Comments

@glennsarti
Copy link
Contributor

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

@daviwil
Copy link
Contributor

daviwil commented Mar 30, 2017

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

@glennsarti
Copy link
Contributor Author

Glad I wasn't going insane (more than usual)

@rkeithhill
Copy link
Contributor

rkeithhill commented Mar 30, 2017

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.

@glennsarti
Copy link
Contributor Author

True, although if the code is being run from a file that exists, and it's a ps1, in my mind there's an expectation that $PSScriptRoot should be discoverable. If I use the PS Debugging (F5) it exists.

I feel I do understand the technicalities here, and yes I can see your argument.

The bigger question in my mind is should the user know the difference? If so, how would VS Code convey that to the user, instead of errors and null values?

@rkeithhill
Copy link
Contributor

rkeithhill commented Mar 30, 2017

If I use the PS Debugging (F5) it exists.

Dot sourcing will evaluate $PSScriptRoot and when the file is executed without dot sourcing, it obviously works as well.

The bigger question in my mind is should the user know the difference?

IMO yes, because running script with F8 i.e. in the global session is subtly different than executing the script file or even dot sourcing it. $PSScriptRoot is just on example. Other differences are $PSCommandPath, $MyInvocation , Get-PSCallStack and likely others I haven't thought of. Also, the integrated console global session is "global" to all scripts you are editing. So these values would have to change for the lines of script that are being executed by F8. Which is likely doable except that have you tried to set $PSScriptRoot? It doesn't want to let you set it. :-) Anyway, if you don't appreciate these subtleties, you can get yourself into trouble thinking you have F8'd yourself to a working script when it won't work when invoked normally. All that said, the common stumbling block is likely to be $PSScriptRoot. If this variable can be set in the global scope and setting it doesn't interfere with proper script operation later, it is probably worth setting.

Finally, maybe the F8 feature could be made smart and if you have selected all the text in a script, it would dot source the script rather than execute script directly in the global session. Then, at least $PSScriptRoot and $PSCommandPath would be defined correctly.

@glennsarti
Copy link
Contributor Author

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.

@rkeithhill
Copy link
Contributor

warned that "Stuff may not do what you expect" if it saw those tokens in the text.

We should probably do that if we detect $PSCommandPath, $MyInvocation or Get-PSCallStack in the text. I think those are corner-case enough to not try to "fix up" in the global session.

Now that I think about it, perhaps the F8 mechanism could do the evaluation of $PSScriptRoot and replace it with the full path of the script's parent dir?

@daviwil
Copy link
Contributor

daviwil commented Mar 30, 2017

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?

@dragonwolf83
Copy link

dragonwolf83 commented Mar 30, 2017

ISE has the following property to get the Current File in the Editor:

$psise.CurrentFile

I don't know if you have ported it yet, but that makes it easy to inject into $PSScriptRoot when F8 is run. It always stays up-to-date with new editor tabs and even has path to where Untitled.ps1 would be saved.

This is one of the most annoying things in ISE. It requires special handling and thinking when developing because you need $PSScriptRoot in production and $psise.CurrentFile in testing specific code blocks.

A couple reasons for using F8 is because previous code in script is either dangerous to run multiple times (deleting files) or has performance impacts (large RESTful call or Get-ChildItem -Recurse). So I run a snippet and then test various things on that snippet and would like one "truth" of where the script is located.

@simonsabin
Copy link

Hit this today, Really don't want to have to comment lots of code out to run that file. One might argue I need to factor my code correctly, but rightly or wrongly I expect people to be in this situation and want to F8 a script that tries to run files in relative folders.

@SeeminglyScience
Copy link
Collaborator

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)))
        }
    }
}

@daviwil
Copy link
Contributor

daviwil commented Jan 17, 2018

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.

@lucvdv
Copy link

lucvdv commented May 15, 2019

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.

@JustinGrote
Copy link
Collaborator

JustinGrote commented Apr 17, 2020

@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

@ghost ghost added the Needs: Maintainer Attention Maintainer attention needed! label Apr 17, 2020
@simonsabin
Copy link

I can understand the challenges here, unless anyone has a suitable workaround I would love to see a solution.
In the absence of a workaround can anyone suggest what development practice to follow to avoid the need to use $psscriptroot to access relative paths. 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".
Script writing I feel is different, and if you are dot sourcing modules in a relative path, then your only other choice is commenting out code.
Any ideas?

@JustinGrote
Copy link
Collaborator

@simonsabin if I'm using psscriptroot in my code, 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.

@TylerLeonhardt TylerLeonhardt added Issue-Enhancement A feature request (enhancement). and removed Area-ISE Compatibility Issue-Bug A bug to squash. labels Apr 19, 2020
@glennsarti
Copy link
Contributor Author

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>

@simonsabin
Copy link

@glennsarti by application code I guess I means compiled code in a project like system.

@JustinGrote Got to agree with @glennsarti that its not a feasible workaround for every time.

Has anyone tried implementing @SeeminglyScience in VS Code

@PrzemyslawKlys
Copy link
Contributor

Is changing the behavior of $PSScriptRoot a good idea? In the terminal, if you copy/paste something $PSScriptRoot is null. We are going to confuse people.

@PrzemyslawKlys
Copy link
Contributor

PrzemyslawKlys commented Jan 24, 2022

If you run $PSScriptRoot in PowerShell console it's empty. F8 basically takes the code you highlighted and pastes it into console, so in this case $PSScriptRoot is empty. That's why the behavior is different. To fix it VSCode extension would need to overwrite the default behavior and overwrite $PSScriptRoot variable with folder path of the script the highlighted code was taken from. Not great, not terrible. $PSScriptRoot is simply special variable that is only usable in scripts, rather than in pasted content. If you take the content yourself and paste it into PowerShell console and it acts like it acts now - and it acts differently on F8 which basically is copy/paste into console - it may be look confusing. PS. I don't get the smell analogy - sorry

@JustinGrote
Copy link
Collaborator

@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.

@JustinGrote JustinGrote removed the Needs: Maintainer Attention Maintainer attention needed! label Jan 24, 2022
@ghost
Copy link

ghost commented Jan 24, 2022

Okay, I see that I should not test a block of script via F8 and expect the same behavior as F5. For a new, unsaved file or the console, I might have a hard time co-locating. By smell, I mean that perhaps there is a better alternative than $PSScriptRoot in code for locating a file in the same location, even if the location gets weird. Or perhaps importing co-located files is a bad practice in the first place.

I've been trying to write code that works the same no matter what. Instead, perhaps I emulate environment variables in Visual Studio by testing the environment at the start of the main script. If it's VSCode or PSISE, set my environment. (Not the same as the environment. Perhaps an environment setting would simplify setting things like default preferences.)

@ghost ghost added the Needs: Maintainer Attention Maintainer attention needed! label Jan 24, 2022
@JustinGrote
Copy link
Collaborator

@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"

@andyleejordan
Copy link
Member

I also like David's early idea of replacing any custom F8 functionality with just Code's own APIs. I think deleting code is a good reason to do so.

@simonsabin
Copy link

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.

@JustinGrote
Copy link
Collaborator

@simonsabin for some reason in my brain I tracked this as a different issue, sorry. I think it's still open to, if we don't remove the "clever" F8 functionality, to allow an opt-in setting to reconstitute these variables as part of a F8 run, but it would have a big disclaimer that it would be best-effort.

@JustinGrote JustinGrote reopened this Jan 24, 2022
@JustinGrote JustinGrote removed the Needs: Maintainer Attention Maintainer attention needed! label Jan 24, 2022
@ghost
Copy link

ghost commented Jan 25, 2022

@JustinGrote Do we need to create a feature request?

@ghost ghost added the Needs: Maintainer Attention Maintainer attention needed! label Jan 25, 2022
@JustinGrote
Copy link
Collaborator

This issue can be used for tracking the enhancement. I've flagged it as Up for Grabs which means we will entertain PR attempts, right now with the pipeline rework the focus is on getting that to the point it can be promoted to stable so that we can have a regular stable release cadence again, so I wouldn't expect this anytime soon unless someone contributes it.

@JustinGrote JustinGrote removed the Needs: Maintainer Attention Maintainer attention needed! label Jan 25, 2022
@simonsabin
Copy link

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.

@ghost ghost added the Needs: Maintainer Attention Maintainer attention needed! label Jan 25, 2022
@JustinGrote
Copy link
Collaborator

@simonsabin

        {
            "type": "PowerShell",
            "request": "launch",
            "name": "PS: 📄 with args",
            "script": "${file}",
            "args": [
                "${command:SpecifyScriptArgs}"
            ],
            "cwd": "${file}"
        },

@JustinGrote JustinGrote removed the Needs: Maintainer Attention Maintainer attention needed! label Jan 25, 2022
@simonsabin
Copy link

Thanks @JustinGrote.my point was that "script" only takes a filepath. one can't pass a chunk of text.
I was looking for a cheap workaround

   {
            "name": "PowerShell F8 Session",
            "type": "PowerShell",
            "request": "launch",
            "script": "${workspaceFolder}/.vscode/run.ps1",
            "args": [
                "-file","${file}",
                "{${selectedText}}"
            ]
        },

where run.ps1 is a script that can run the selected text. However you can't override psscriptroot.

It looks like @SeeminglyScience solution would be a much better one.

@walterg32
Copy link

@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.

I had my "AHA" moment when I read this comment. Totally makes sense now why my $PSScriptRoot wasn't working. I was selectively running code segments. Thanks for the solution!

@ghost
Copy link

ghost commented Mar 17, 2023

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

@JustinGrote
Copy link
Collaborator

JustinGrote commented Mar 22, 2023

Even if it's not run as a script, the script still has a parent folder.

What would the parent folder of Remove-Item $PSScriptRoot -recurse -force be if run as a selection? Your current working directory? That sounds dangerous, that's why it only populates if it is in the context of the script.

When you use Run Selection, it's the same as copying and pasting the code to the command line. Testing of scripts proper including $PSScriptRoot you really should be using Pester, run as file (default F5) or dot sourcing the script to test it. Running fragments out of a script and expecting PSScriptRoot to work in my opinion is the same as running a block of code without the variable above it and expecting the variable to be magically autopopulated.

@andyleejordan andyleejordan pinned this issue Apr 27, 2023
@andyleejordan andyleejordan unpinned this issue Apr 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Extension Terminal Issue-Enhancement A feature request (enhancement). Up for Grabs Will shepherd PRs.
Projects
None yet
Development

No branches or pull requests