-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Add location history for Set-Location to enable 'cd -' scenario (issue #2188) #5051
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
Add location history for Set-Location to enable 'cd -' scenario (issue #2188) #5051
Conversation
This makes it possible to go back to the last directory using 'cd -', which is exactly the same syntax as on Linux shells.
|
Should it be some per- |
|
@PetSerAl Interesting thought and I did not know that the ISE tabs share the same process. +1 for sharing your knowledge. :) |
|
As far as I understand, Also PowerShell ISE is just an example of PowerShell host, which use more than one Simply: there are no rule "only one |
|
OK. I see what you mean. My assumption was that |
Even when |
|
I agree with @PetSerAl. See PoshRSJobs for an example of how users can run multiple runspaces in the same process. Rather than make this an environment variable just make it a session (runspace) variable e.g. |
|
I agree with @PetSerAl too - we should take into account many runspaces. Main question - Do we really need this in scripts? Set-Location -
cd -looks very poorly read. Using Push-Location Pop-Location pair looks more readable and understandable. Next question - Should we keep a history and implement some step back by "cd-; cd-; cd -"? |
|
Ok. If you say runspace variable, do you mean we should declare it as an automatic variable or is there a subtle difference? How would one ban its usage in scripts? About the history: the idea is to keep it simple at the moment and another PR could improve it to use a history stack (that is the point about agile development: make something minimal and viable with value that works and then iteratively enhance it) |
As an example of this, see the PSCX package. It implements a cd stack that you can backwards navigate with It also handles two cases that I particularly like: and
You don't. That isn't something I'd worry about. However there are other CD type packages out there. Some allow you to fuzzy match when CD'ing to directories. |
|
I think we should easily implement the history based on "Stack" from the class. |
|
I'd prefer a stack as well, though it needs to be bounded. You could start with this code from PSReadline. I don't think it's necessary to prevent the use of |
|
|
||
| private string ReplacePathWithLastLocationIfApplicable(string path) | ||
| { | ||
| Environment.SetEnvironmentVariable("DEBUG_PATH", Path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we want this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. Well spotted. This was a development debugging leftover, sorry.
| Path = ReplacePathWithLastLocationIfApplicable(Path); | ||
| var initialPath = SessionState.Path.CurrentLocation.Path; | ||
| result = SessionState.Path.SetLocation(Path, CmdletProviderContext); | ||
| Environment.SetEnvironmentVariable(environmentVariableNameForPreviousLocation, initialPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't use environment variables, instead we should expose a new variable (PSOldPWD?) that is an instance of a bounded stack.
We could consider a user configurable stack size, but we can leave that for later (not this PR).
I also wonder if this logic should be in SessionState.Path.SetLocation - that could be useful for alternate implementations of Set-Location that call the api.
| StackName = ReplacePathWithLastLocationIfApplicable(StackName); | ||
| var initialPath = SessionState.Path.CurrentLocation.Path; | ||
| result = SessionState.Path.SetDefaultLocationStack(StackName); | ||
| Environment.SetEnvironmentVariable(environmentVariableNameForPreviousLocation, initialPath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous comments apply here, including moving the logic to SessionState.Path.SetDefaultLocationStack.
| { | ||
| { | ||
| relationshipPath = ReplacePathWithLastLocationIfApplicable(relationshipPath); | ||
| var initialPath = SessionState.Path.CurrentLocation.Path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can revert the changes under RELATIONSHIP_SUPPORTED - the code is dead, and I've submitted a PR to delete it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, will do.
| result = SessionState.Path.SetLocation (relationshipPath, CmdletProviderContext); | ||
| relationshipPath = ReplacePathWithLastLocationIfApplicable(relationshipPath); | ||
| var initialPath = SessionState.Path.CurrentLocation; | ||
| result = SessionState.Path.SetLocation(relationshipPath, CmdletProviderContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous comments apply here, including moving the logic to SessionState.Path.SetLocation. In fact, I think if you did that, the change here wouldn't be necessary.
| @@ -0,0 +1,123 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's reasonable to put the new string in SessionStateStrings instead of this new file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
…ment variable and move logic for 'cd -' into the internal class of SessionState.Path.SetLocation Keep the (additional) setting of an environment for Unix system in case of mixed usage of cd and Set-Location to provide consistency.
|
@lzybkr I have implemented your suggestions and used the existing stack implementation that pusdh/popd use under the hood to avoid code duplication. |
iSazonov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like side effects - performance decreases, memory consumption increases.
| It 'Should go to last location when specifying minus as a path using alias' { | ||
| $initialLocation = Get-Location | ||
| Set-Location ([System.IO.Path]::GetTempPath()) | ||
| cd - |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove the test - we shouldn't tests aliases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
| result = SessionState.Path.SetLocation(Path, CmdletProviderContext); | ||
| #if UNIX | ||
| // Be consistent with most Unix shells that set the environment variable 'OLDPWD' when changing location | ||
| Environment.SetEnvironmentVariable("OLDPWD", Path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see the point - we don't use it anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - I also don't see the point. Maybe if ps1 files ran in a new process, then it might make sense, but they don't.
And if setting the environment variable was useful, we should explain why it is UNIX only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I've removed it. I found out later that on Linux cd is also an alias for Set-Location (my assumption was that native Linux commands are not overridden by pwsh but this seems to be one of the exceptions)
| { | ||
| return PopLocation(locationStackHistoryId); | ||
| } | ||
| PushCurrentLocation(locationStackHistoryId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice - so simple. I do have a concern about an unbounded stack though. This stack will not be popped often, so I think there should be some limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I've implemented now a bounded stack based on a linked list.
| $tempLocation = Get-Location | ||
| Set-Location - | ||
| Set-Location - | ||
| (Get-Location).Path | Should Be ($tempLocation).Path |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test doesn't make sense to me - there is one push, but two pops. How can this pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have re-written all tests but the reason why it passed was because Set-Location also pushed to the stack (this was a bug that is fixed now)
| (Get-Location).Path | Should Be ($tempLocation).Path | ||
| } | ||
|
|
||
| It 'Should fail if there is no last location' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm also surprised about this test - it seems like you need a way to clear the stack before running cd - to make sure it reliably raises the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rewrote this test since it does not make sense any more (it was based on the first environment variable based implementation)
Remove setting environment variable on Linux as discussed in PR.
…o SimpleLocationHistory
…he path string (e.g. an additional slash at the end) Make syntax to .net call of environment consistent with surrounding code
iSazonov
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why not enhance the existing API?
|
@iSazonov If I understand correctly, changing the existing API to use a bounded stack would be a breaking change for pushd/popd. Enhancing the existing API should just be a matter of changing the type from |
|
@bergmeister We should enhance the API not change. Actually you will use new constructor to set limits, existing code will keep current behavior without limits. This will allow us enhance our *-Location cmdlets and allow customers to use the new API possibilities. |
|
@iSazonov There were test failures due it being the full test suite: |
…o SimpleLocationHistory
|
Those tests seemed to be sporadic and the new build contains 'only' 1 other sporadic failure |
|
@bergmeister Fix is ready #6943. |
|
@iSazonov Any updates on this so that it goes into the next preview for getting some testing exposure before 6.1 goes RTM after that? |
|
@bergmeister I don't understand. Please reword your question. |
|
@iSazonov. Nevermind, I just saw that the next preview is already in the making. |
|
@bergmeister We are waiting MSFT team sign. |
rjmholt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me. Thanks @bergmeister!
| bool pushNextLocation = true; | ||
| if (originalPath.Equals("-", StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| if (_workingLocationHistoryStack.Count > 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would structure this if as:
if (_workingLocationHistoryStack.Count <= 0)
{
throw new InvalidOperationException(...);
}
// Golden path behaviourThere are some pretty deep if/else trees in the codebase and they get hard to reason about quickly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this is neater, done.
| { | ||
| this.AddFirst(item); | ||
|
|
||
| if(this.Count > _capacity) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe include braces for the if and drop the this from property references. That seems to be the style convention of the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I agree. Fixed.
|
@daxian-dbw @adityapatwardhan Have you any thoughts before we merge? |
…to SimpleLocationHistory
BrucePay
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general it looks good. Just a few things I think need to change.
| _workingLocationStack = new Dictionary<String, Stack<PathInfo>>(StringComparer.OrdinalIgnoreCase); | ||
| // conservative choice to limit the memory impact in case of a regression | ||
| const int locationHistoryLimit = 20; | ||
| _workingLocationHistoryStack = new BoundedStack<PathInfo>(locationHistoryLimit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable name is easily confused with _workingLocationStack and _workingLocationHistoryStack. I would suggest some more meaningful like _SetLocationHistory. (I don't think it needs it's type in it's name, and if we do decide to implement cd + it won't be a stack anymore.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bergmeister Please replace _SetLocationHistory -> _setLocationHistory - it is our name convention for static variables.
Also please review CodeFactor issues - maybe you would fix some ones (in your code or near).
| // is used for the pushd and popd commands | ||
|
|
||
| _workingLocationStack = new Dictionary<String, Stack<PathInfo>>(StringComparer.OrdinalIgnoreCase); | ||
| // conservative choice to limit the memory impact in case of a regression |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would put a space before the comment. I'd also add a comment saying that this is for the Set-Location history to distinguish it from the earlier "pushd and popd" comment.
| internal T Pop() | ||
| { | ||
| var item = this.First.Value; | ||
| this.RemoveFirst(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should have a specific error message here instead of letting the base class throw it's own message if the linked list is empty. If we change the implementation to be a circular buffer, then the exception change too.
… when attempting to pop empty bounded stack. Error message was chosen to be similar to the one by LinkedList
|
It seems we set a absolute record in PR review duration 😕 |
|
@bergmeister Thanks for your contribution! |
|
@daxian-dbw Please clarify what is the "Revert..." commit? |
|
I saw this as well and cannot track it back, the PR is still in master (which is good), so I assume this commit either happened on a branch or the revert got reverted afterwards |
|
I think when you use GitHub's UI to revert a PR, it generates a new commit that reverts all the commits since that PR (which is not what we want -- reverting the single commit in the commandline is the better way). That commit is generated on another branch and not merged in, but still references all the other commits and shows up everywhere, which manages to confuse everyone. II'm guessing @daxian-dbw initially tried to use the revert button, and has since overridden it in the commandline. So this merge hasn't been reverted, but a revert commit for it was generated and never used. I know because I clicked the "revert" button once and was answering questions about it for a week. |
|
The confusing revert commit comes from https://github.com/PowerShell/PowerShell/pull/7182/commits. Changes from this PR is intact, so don't worry :) |


PR Summary
Closes #2188
This makes it possible to go back to the last directory using 'cd -', which is exactly the same syntax as on Linux shells.
In contrast to the original proposal, it does not set the environment variable
OLDPWDbut instead uses a bounded stack to store the history in a similar way how Push/Pop-Location do.A localised error message is given if there is no location history.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright headerWIP:to the beginning of the title and remove the prefix when the PR is ready.[feature]if the change is significant or affects feature tests