Skip to content

Commit

Permalink
Add location history for Set-Location to enable 'cd -' scenario (issue
Browse files Browse the repository at this point in the history
…#2188) (#5051)

* Implement location history feature for Set-Location.
This makes it possible to go back to the last directory using 'cd -',
which is exactly the same syntax as on Linux shells.

* Use an existing stack implementation instead of relying on an environment 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.

* Use a bounded stack with a limit of 1000 and adapt tests.
Remove setting environment variable on Linux as discussed in PR.

* Fix test: use get-location explicitly to avoid subtle difference in the path string (e.g. an additional slash at the end)
Make syntax to .net call of environment consistent with surrounding code

* Move initialization of _workingLocationHistoryStack into constructor of SessionStateInternal and set the limit to 20 as discussed in PR 5051.

* Make new BoundedStack class in engine utils internal
  • Loading branch information
bergmeister authored and iSazonov committed Jun 26, 2018
1 parent 7d72e38 commit bbd48f6
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 15 deletions.
5 changes: 5 additions & 0 deletions src/System.Management.Automation/engine/SessionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Management.Automation.Internal;
using System.Management.Automation.Runspaces;
using Dbg = System.Management.Automation;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -66,6 +67,10 @@ internal SessionStateInternal(SessionStateInternal parent, bool linkToGlobal, Ex

_workingLocationStack = new Dictionary<String, Stack<PathInfo>>(StringComparer.OrdinalIgnoreCase);

// Conservative choice to limit the Set-Location history in order to limit memory impact in case of a regression.
const int locationHistoryLimit = 20;
_SetLocationHistory = new BoundedStack<PathInfo>(locationHistoryLimit);

GlobalScope = new SessionStateScope(null);
ModuleScope = GlobalScope;
_currentScope = GlobalScope;
Expand Down
57 changes: 42 additions & 15 deletions src/System.Management.Automation/engine/SessionStateLocationAPIs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Management.Automation.Internal;
using System.Management.Automation.Provider;
using Dbg = System.Management.Automation;

Expand Down Expand Up @@ -230,10 +231,28 @@ internal PathInfo SetLocation(string path, CmdletProviderContext context)
ProviderInfo provider = null;
string providerId = null;

// Replace path with last working directory when '-' was passed.
bool pushNextLocation = true;
if (originalPath.Equals("-", StringComparison.OrdinalIgnoreCase))
{
if (_SetLocationHistory.Count <= 0)
{
throw new InvalidOperationException(SessionStateStrings.SetContentToLastLocationWhenHistoryIsEmpty);
}
var previousLocation = _SetLocationHistory.Pop();
path = previousLocation.Path;
pushNextLocation = false;
}

if (pushNextLocation)
{
var newPushPathInfo = GetNewPushPathInfo();
_SetLocationHistory.Push(newPushPathInfo);
}

PSDriveInfo previousWorkingDrive = CurrentDrive;

// First check to see if the path is a home path

if (LocationGlobber.IsHomePath(path))
{
path = Globber.GetHomeRelativePath(path);
Expand Down Expand Up @@ -783,6 +802,11 @@ CmdletProviderContext normalizePathContext

#region push-Pop current working directory

/// <summary>
/// A bounded stack for the location history of Set-Location
/// </summary>
private BoundedStack<PathInfo> _SetLocationHistory;

/// <summary>
/// A stack of the most recently pushed locations
/// </summary>
Expand Down Expand Up @@ -811,8 +835,23 @@ internal void PushCurrentLocation(string stackName)
stackName = _defaultStackName;
}

// Create a new instance of the directory/drive pair
// Get the location stack from the hashtable
Stack<PathInfo> locationStack = null;

if (!_workingLocationStack.TryGetValue(stackName, out locationStack))
{
locationStack = new Stack<PathInfo>();
_workingLocationStack[stackName] = locationStack;
}

// Push the directory/drive pair onto the stack
var newPushPathInfo = GetNewPushPathInfo();
locationStack.Push(newPushPathInfo);
}

private PathInfo GetNewPushPathInfo()
{
// Create a new instance of the directory/drive pair
ProviderInfo provider = CurrentDrive.Provider;
string mshQualifiedPath =
LocationGlobber.GetMshQualifiedPath(CurrentDrive.CurrentLocation, CurrentDrive);
Expand All @@ -829,19 +868,7 @@ internal void PushCurrentLocation(string stackName)
CurrentDrive.Name,
mshQualifiedPath);

// Get the location stack from the hashtable

Stack<PathInfo> locationStack = null;

if (!_workingLocationStack.TryGetValue(stackName, out locationStack))
{
locationStack = new Stack<PathInfo>();
_workingLocationStack[stackName] = locationStack;
}

// Push the directory/drive pair onto the stack

locationStack.Push(newPushLocation);
return newPushLocation;
}

/// <summary>
Expand Down
49 changes: 49 additions & 0 deletions src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1459,4 +1459,53 @@ public static void SetTestHook(string property, object value)
}
}
}

/// <summary>
/// An bounded stack based on a linked list.
/// </summary>
internal class BoundedStack<T> : LinkedList<T>
{
private readonly int _capacity;

/// <summary>
/// Lazy initialisation, i.e. it sets only its limit but does not allocate the memory for the given capacity.
/// </summary>
/// <param name="capacity"></param>
internal BoundedStack(int capacity)
{
_capacity = capacity;
}

/// <summary>
/// Push item.
/// </summary>
/// <param name="item"></param>
internal void Push(T item)
{
this.AddFirst(item);

if(this.Count > _capacity)
{
this.RemoveLast();
}
}

/// <summary>
/// Pop item.
/// </summary>
/// <returns></returns>
internal T Pop()
{
var item = this.First.Value;
try
{
this.RemoveFirst();
}
catch (InvalidOperationException)
{
throw new InvalidOperationException(SessionStateStrings.BoundedStackIsEmpty);
}
return item;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@
<data name="GetContentWriterDynamicParametersProviderException" xml:space="preserve">
<value>The dynamic parameters for the GetContentWriter operation cannot be retrieved for the '{0}' provider for path '{1}'. {2}</value>
</data>
<data name="SetContentToLastLocationWhenHistoryIsEmpty" xml:space="preserve">
<value>There is no location history left to navigate backwards.</value>
</data>
<data name="BoundedStackIsEmpty" xml:space="preserve">
<value>The BoundedStack is empty.</value>
</data>
<data name="ClearContentProviderException" xml:space="preserve">
<value>Attempting to perform the ClearContent operation on the '{0}' provider failed for path '{1}'. {2}</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,38 @@ Describe "Set-Location" -Tags "CI" {
Remove-PSDrive -Name 'Z'
}
}

Context 'Set-Location with last location history' {

It 'Should go to last location when specifying minus as a path' {
$initialLocation = Get-Location
Set-Location ([System.IO.Path]::GetTempPath())
Set-Location -
(Get-Location).Path | Should -Be ($initialLocation).Path
}

It 'Should go back to previous locations when specifying minus twice' {
$initialLocation = (Get-Location).Path
Set-Location ([System.IO.Path]::GetTempPath())
$firstLocationChange = (Get-Location).Path
Set-Location ([System.Environment]::GetFolderPath("user"))
Set-Location -
(Get-Location).Path | Should -Be $firstLocationChange
Set-Location -
(Get-Location).Path | Should -Be $initialLocation
}

It 'Location History is limited' {
$initialLocation = (Get-Location).Path
$maximumLocationHistory = 20
foreach ($i in 1..$maximumLocationHistory) {
Set-Location ([System.IO.Path]::GetTempPath())
}
foreach ($i in 1..$maximumLocationHistory) {
Set-Location -
}
(Get-Location).Path | Should Be $initialLocation
{ Set-Location - } | Should -Throw -ErrorId 'System.InvalidOperationException,Microsoft.PowerShell.Commands.SetLocationCommand'
}
}
}

0 comments on commit bbd48f6

Please sign in to comment.