diff --git a/docs/KNOWNISSUES.md b/docs/KNOWNISSUES.md
index 34f6f312e66..5b6587183cf 100644
--- a/docs/KNOWNISSUES.md
+++ b/docs/KNOWNISSUES.md
@@ -87,3 +87,38 @@ following cmdlets that exist in the FullCLR version:
- Send-MailMessage
- Show-Command
- Update-List
+
+## File paths with literal backward slashes
+
+On some filesystems (Linux, OS X), file paths are allowed to contain literal
+backward slashes, '\', as valid filename characters. These slashes, when
+escaped, are not directory separators. In Bash, the backward slash is the escape
+character, so a `path/with/a\\slash` is two directories, `path` and `with`, and
+one file, `a\slash`. In PowerShell, we *will* support this using the normal
+backtick escape character, so a `path\with\a``\slash` or a
+`path/with/a``\slash`, but this edge case is *currently unsupported*.
+
+That being said, native commands will work as expected. Thus this is the current
+scenario:
+
+```powershell
+PS > Get-Content a`\slash
+Get-Content : Cannot find path '/home/andrew/src/PowerShell/a/slash' because it does not exist.
+At line:1 char:1
++ Get-Content a`\slash
++ ~~~~~~~~~~~~~~~~~~~~
+ + CategoryInfo : ObjectNotFound: (/home/andrew/src/PowerShell/a/slash:String) [Get-Co
+ ntent], ItemNotFoundException
+ + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand
+
+PS > /bin/cat a\slash
+hi
+
+```
+
+The PowerShell cmdlet `Get-Content` cannot yet understand the escaped backward
+slash, but the path is passed literally to the native command `/bin/cat`. Most
+file operations are thus implicitly supported by the native commands. The
+notable exception is `cd` since it is not a command, but a shell built-in,
+`Set-Location`. So until this issue is resolved, PowerShell cannot change to a
+directory whose name contains a literal backward slash.
diff --git a/src/System.Management.Automation/engine/SessionStateStrings.cs b/src/System.Management.Automation/engine/SessionStateStrings.cs
index 4ea503ed5dc..93e40863c45 100644
--- a/src/System.Management.Automation/engine/SessionStateStrings.cs
+++ b/src/System.Management.Automation/engine/SessionStateStrings.cs
@@ -26,6 +26,9 @@ internal static class StringLiterals
///
/// The default path separator used by the base implementation of the providers.
+ ///
+ /// Porting note: IO.Path.DirectorySeparatorChar is correct for all platforms. On Windows,
+ /// it is '\', and on Linux, it is '/', as expected.
///
///
internal static readonly char DefaultPathSeparator = System.IO.Path.DirectorySeparatorChar;
@@ -33,9 +36,14 @@ internal static class StringLiterals
///
/// The alternate path separator used by the base implementation of the providers.
+ ///
+ /// Porting note: we do not use .NET's AlternatePathSeparatorChar here because it correctly
+ /// states that both the default and alternate are '/' on Linux. However, for PowerShell to
+ /// be "slash agnostic", we need to use the assumption that a '\' is the alternate path
+ /// separator on Linux.
///
///
- internal static readonly char AlternatePathSeparator = System.IO.Path.AltDirectorySeparatorChar;
+ internal static readonly char AlternatePathSeparator = Platform.IsWindows ? '/' : '\\';
internal static readonly string AlternatePathSeparatorString = AlternatePathSeparator.ToString();
///
diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
index 65541d578a6..8f691de069f 100644
--- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs
+++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
@@ -951,7 +951,7 @@ protected override Collection InitializeDefaultDrives()
// add the filesystem with the root "/" to the initial drive list,
// otherwise path handling will not work correctly because there
// is no : available to separate the filesystems from each other
- if (Platform.HasSingleRootFilesystem() && root != "/")
+ if (Platform.HasSingleRootFilesystem() && root != StringLiterals.DefaultPathSeparatorString)
continue;
// Porting notes: On non-windows platforms .net can report two
@@ -4765,12 +4765,13 @@ protected override string GetParentPath (string path, string root)
return parentPath;
} // GetParentPath
+ // Note: we don't use IO.Path.IsPathRooted as this deals with "invalid" i.e. unnormalized paths
private static bool IsAbsolutePath(string path)
{
bool result = false;
- // this needs to be done differently on single root filesystems
- if (Platform.HasSingleRootFilesystem() && path.StartsWith("/"))
+ // check if we're on a single root filesystem and it's an absolute path
+ if (LocationGlobber.IsSingleFileSystemAbsolutePath(path))
{
return true;
}
@@ -5239,18 +5240,15 @@ private string NormalizeRelativePathHelper (string path, string basePath)
private string RemoveRelativeTokens(string path)
{
- string sep = System.IO.Path.DirectorySeparatorChar.ToString();
- string altSep = System.IO.Path.AltDirectorySeparatorChar.ToString();
-
- string testPath = path.Replace(altSep,sep);
+ string testPath = path.Replace('/', '\\');
if (
- (testPath.IndexOf(sep, StringComparison.OrdinalIgnoreCase) < 0) ||
- testPath.StartsWith("." + sep, StringComparison.OrdinalIgnoreCase) ||
- testPath.StartsWith(".." + sep, StringComparison.OrdinalIgnoreCase) ||
- testPath.EndsWith(sep + ".", StringComparison.OrdinalIgnoreCase) ||
- testPath.EndsWith(sep + "..", StringComparison.OrdinalIgnoreCase) ||
- (testPath.IndexOf(sep + "." + sep, StringComparison.OrdinalIgnoreCase) > 0) ||
- (testPath.IndexOf(sep + ".." + sep, StringComparison.OrdinalIgnoreCase) > 0))
+ (testPath.IndexOf("\\", StringComparison.OrdinalIgnoreCase) < 0) ||
+ testPath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) ||
+ testPath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) ||
+ testPath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) ||
+ testPath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) ||
+ (testPath.IndexOf("\\.\\", StringComparison.OrdinalIgnoreCase) > 0) ||
+ (testPath.IndexOf("\\..\\", StringComparison.OrdinalIgnoreCase) > 0))
{
try
{
diff --git a/src/System.Management.Automation/namespaces/LocationGlobber.cs b/src/System.Management.Automation/namespaces/LocationGlobber.cs
index 4b5441c2925..313e9e0e6c5 100644
--- a/src/System.Management.Automation/namespaces/LocationGlobber.cs
+++ b/src/System.Management.Automation/namespaces/LocationGlobber.cs
@@ -1570,6 +1570,30 @@ internal static bool IsProviderQualifiedPath(string path, out string providerId)
return result;
} // IsProviderQualifiedPath
+ ///
+ /// Determines if the given path is absolute while on a single root filesystem.
+ ///
+ ///
+ ///
+ /// Porting notes: absolute paths on non-Windows filesystems start with a '/' (no "C:" drive
+ /// prefix, the slash is the prefix). We compare against both '/' and '\' (default and
+ /// alternate path separator) in order for PowerShell to be slash agnostic.
+ ///
+ ///
+ ///
+ /// The path used in the determination
+ ///
+ ///
+ ///
+ /// Returns true if we're on a single root filesystem and the path is absolute.
+ ///
+ internal static bool IsSingleFileSystemAbsolutePath(string path)
+ {
+ return Platform.HasSingleRootFilesystem() &&
+ (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal) ||
+ path.StartsWith(StringLiterals.AlternatePathSeparatorString, StringComparison.Ordinal));
+ } // IsSingleFileSystemAbsolutePath
+
///
/// Determines if the given path is relative or absolute
///
@@ -1619,11 +1643,8 @@ internal static bool IsAbsolutePath(string path)
break;
}
- // non-windows porting notes:
- // this needs to be handled differently on non-windows, paths starting with / are always absolute
- // -> still continue with other isAbsolute processing, colons can still happen in drives
- // of other providers
- if (Platform.HasSingleRootFilesystem() && path.StartsWith("/",StringComparison.Ordinal))
+ // check if we're on a single root filesystem and it's an absolute path
+ if (IsSingleFileSystemAbsolutePath(path))
{
result = true;
break;
@@ -1716,13 +1737,10 @@ internal bool IsAbsolutePath(string path, out string driveName)
break;
}
- // non-windows porting notes:
- // this needs to be handled differently on non-windows, paths starting with / are always absolute
- // -> still continue with other isAbsolute processing, colons can still happen in drives
- // of other providers
- if (Platform.HasSingleRootFilesystem() && path.StartsWith("/",StringComparison.Ordinal))
+ // check if we're on a single root filesystem and it's an absolute path
+ if (IsSingleFileSystemAbsolutePath(path))
{
- driveName = "/";
+ driveName = StringLiterals.DefaultPathSeparatorString;
result = true;
break;
}
@@ -2266,7 +2284,8 @@ internal string GenerateRelativePath(
driveRootRelativeWorkingPath = String.Empty;
- // Remove the \ or / from the drive relative path
+ // Remove the \ or / from the drive relative
+ // path
path = path.Substring(1);
@@ -2442,41 +2461,18 @@ internal string GenerateRelativePath(
private bool HasRelativePathTokens(string path)
{
- if (System.IO.Path.DirectorySeparatorChar == '/')
- {
- string comparePath = path;
-
- // the next line will only replace something, if the directory separators
- // are different on the platform
- if (System.IO.Path.DirectorySeparatorChar != System.IO.Path.AltDirectorySeparatorChar)
- comparePath = path.Replace(System.IO.Path.AltDirectorySeparatorChar,System.IO.Path.DirectorySeparatorChar);
-
- return (
- comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) ||
- comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) ||
- comparePath.Contains("/./") ||
- comparePath.Contains("/../") ||
- comparePath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) ||
- comparePath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith("../", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith("./", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase));
- }
- else
- {
- string comparePath = path.Replace('/', '\\');
-
- return (
- comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) ||
- comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) ||
- comparePath.Contains("\\.\\") ||
- comparePath.Contains("\\..\\") ||
- comparePath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) ||
- comparePath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) ||
- comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase));
- }
+ string comparePath = path.Replace('/', '\\');
+
+ return (
+ comparePath.Equals(".", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.Equals("..", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.Contains("\\.\\") ||
+ comparePath.Contains("\\..\\") ||
+ comparePath.EndsWith("\\..", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.EndsWith("\\.", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.StartsWith("..\\", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.StartsWith(".\\", StringComparison.OrdinalIgnoreCase) ||
+ comparePath.StartsWith("~", StringComparison.OrdinalIgnoreCase));
}
///
@@ -3130,50 +3126,43 @@ private Collection ExpandMshGlobPath(
}
else
{
- // Platform note:
- // this needs to be done differently on non-windows platforms
-
string unescapedPath = context.SuppressWildcardExpansion ? path : RemoveGlobEscaping(path);
- // this is the output of the platform specific code right below
- string resolvedPath;
+ string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}";
- if (drive.VolumeSeparatedByColon)
+ // Check to see if its a hidden provider drive.
+ if (drive.Hidden)
{
- string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}";
-
- // Check to see if its a hidden provider drive.
- if (drive.Hidden)
+ if (IsProviderDirectPath(unescapedPath))
{
- if (IsProviderDirectPath(unescapedPath))
- {
- formatString = "{1}";
- }
- else
- {
- formatString = "{0}::{1}";
- }
+ formatString = "{1}";
}
else
{
- if (path.StartsWith(StringLiterals.DefaultPathSeparator.ToString(), StringComparison.Ordinal))
- {
- formatString = "{0}:{1}";
- }
+ formatString = "{0}::{1}";
}
-
- resolvedPath =
- String.Format(
- System.Globalization.CultureInfo.InvariantCulture,
- formatString,
- drive.Name,
- unescapedPath);
}
else
{
- resolvedPath = drive.Name + unescapedPath;
+ if (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal))
+ {
+ formatString = "{0}:{1}";
+ }
+ }
+
+ // Porting note: if the volume is not separated by a colon (non-Windows filesystems), don't add it.
+ if (!drive.VolumeSeparatedByColon)
+ {
+ formatString = "{0}{1}";
}
+ string resolvedPath =
+ String.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ formatString,
+ drive.Name,
+ unescapedPath);
+
// Since we didn't do globbing, be sure the path exists
if (allowNonexistingPaths ||
provider.ItemExists(GetProviderPath(resolvedPath, context), context))
@@ -3332,13 +3321,10 @@ internal static string GetDriveQualifiedPath(string path, PSDriveInfo drive)
}
string result = path;
+ bool treatAsRelative = true;
- // platform notes:
- // this needs to be implemented differently depending if the drive uses colon to separate paths
if (drive.VolumeSeparatedByColon)
{
- bool treatAsRelative = true;
-
// Ensure the drive name is the same as the portion of the path before
// :. If not add the drive name and colon as if it was a relative path
@@ -3359,32 +3345,39 @@ internal static string GetDriveQualifiedPath(string path, PSDriveInfo drive)
}
}
}
-
- if (treatAsRelative)
+ }
+ else
+ {
+ if (IsAbsolutePath(path))
{
- string formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}";
+ treatAsRelative = false;
+ }
+ }
- if (path.StartsWith(StringLiterals.DefaultPathSeparator.ToString(), StringComparison.Ordinal))
+ if (treatAsRelative)
+ {
+ string formatString;
+ if (drive.VolumeSeparatedByColon)
+ {
+ formatString = "{0}:" + StringLiterals.DefaultPathSeparator + "{1}";
+ if (path.StartsWith(StringLiterals.DefaultPathSeparatorString, StringComparison.Ordinal))
{
formatString = "{0}:{1}";
}
- result =
- String.Format(
- System.Globalization.CultureInfo.InvariantCulture,
- formatString,
- drive.Name,
- path);
}
- }
- else
- {
- if (path.StartsWith(drive.Name))
- result = path;
else
- result = drive.Name + path;
+ {
+ formatString = "{0}{1}";
+ }
+
+ result =
+ String.Format(
+ System.Globalization.CultureInfo.InvariantCulture,
+ formatString,
+ drive.Name,
+ path);
}
- tracer.WriteLine("result = {0}", result);
return result;
} // GetDriveQualifiedPath
diff --git a/src/System.Management.Automation/namespaces/NavigationProviderBase.cs b/src/System.Management.Automation/namespaces/NavigationProviderBase.cs
index b234acd0917..beeb0d80d22 100644
--- a/src/System.Management.Automation/namespaces/NavigationProviderBase.cs
+++ b/src/System.Management.Automation/namespaces/NavigationProviderBase.cs
@@ -416,8 +416,8 @@ protected string MakePath(string parent, string child, bool childIsLeaf)
}
else
{
- // Normalize the path so that only the backslash is used as a separator even if the
- // user types a forward slash.
+ // Normalize the path so that only the default path separator is used as a
+ // separator even if the user types the alternate slash.
parent = parent.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator);
child = child.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator);
diff --git a/src/System.Management.Automation/namespaces/RegistryProvider.cs b/src/System.Management.Automation/namespaces/RegistryProvider.cs
index e33492c8f6e..1951f59eead 100644
--- a/src/System.Management.Automation/namespaces/RegistryProvider.cs
+++ b/src/System.Management.Automation/namespaces/RegistryProvider.cs
@@ -3888,7 +3888,7 @@ private string NormalizePath(string path)
if (!String.IsNullOrEmpty(path))
{
- result = path.Replace('/', '\\');
+ result = path.Replace(StringLiterals.AlternatePathSeparator, StringLiterals.DefaultPathSeparator);
// Remove relative path tokens
if (HasRelativePathTokens(path))
diff --git a/test/powershell/Hierarchical-Path.Tests.ps1 b/test/powershell/Hierarchical-Path.Tests.ps1
new file mode 100644
index 00000000000..f45c1e8b8de
--- /dev/null
+++ b/test/powershell/Hierarchical-Path.Tests.ps1
@@ -0,0 +1,46 @@
+Describe "Hierarchical paths" {
+ BeforeAll {
+ $data = "Hello World"
+ Setup -File testFile.txt -Content $data
+ }
+
+ It "should work with Join-Path " {
+ $testPath = Join-Path $TestDrive testFile.txt
+ Get-Content $testPath | Should Be $data
+ }
+
+ It "should work with platform's slashes" {
+ $testPath = "$TestDrive$([IO.Path]::DirectorySeparatorChar)testFile.txt"
+ Get-Content $testPath | Should Be $data
+ }
+
+ It "should work with forward slashes" {
+ $testPath = "$TestDrive/testFile.txt"
+ Get-Content $testPath | Should Be $data
+ }
+
+ It "should work with backward slashes" {
+ $testPath = "$TestDrive\testFile.txt"
+ Get-Content $testPath | Should Be $data
+ }
+
+ It "should work with backward slashes for each separator" {
+ $testPath = "$TestDrive\testFile.txt".Replace("/","\")
+ Get-Content $testPath | should be $data
+ }
+
+ It "should work with forward slashes for each separator" {
+ $testPath = "$TestDrive/testFile.txt".Replace("\","/")
+ Get-Content $testPath | should be $data
+ }
+
+ It "should work even if there are too many forward slashes" {
+ $testPath = "$TestDrive//////testFile.txt"
+ Get-Content $testPath | should be $data
+ }
+
+ It "should work even if there are too many backward slashes" {
+ $testPath = "$TestDrive\\\\\\\testFile.txt"
+ Get-Content $testPath | should be $data
+ }
+}