diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs
index 23aa621266..c00c374611 100644
--- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs
+++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs
@@ -2699,7 +2699,7 @@ namespace Microsoft.PowerShell.Commands
try
{
System.IO.DirectoryInfo di = new(providerPath);
- if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
+ if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(di))
{
shouldRecurse = false;
treatAsFile = true;
diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs
index bc443ac10a..8c37713234 100644
--- a/src/System.Management.Automation/engine/Utils.cs
+++ b/src/System.Management.Automation/engine/Utils.cs
@@ -1864,7 +1864,7 @@ namespace System.Management.Automation
if (ExperimentalFeature.IsEnabled("PSAnsiRendering"))
{
- PSStyle psstyle = PSStyle.Instance;
+ PSStyle psstyle = PSStyle.Instance;
switch (formatStyle)
{
case FormatStyle.Reset:
@@ -2104,6 +2104,14 @@ namespace System.Management.Automation.Internal
internal static bool ThrowExdevErrorOnMoveDirectory;
+ // To emulate OneDrive behavior we use the hard-coded symlink.
+ // If OneDriveTestRecurseOn is false then the symlink works as regular symlink.
+ // If OneDriveTestRecurseOn is true then we recurse into the symlink as OneDrive should work.
+ // OneDriveTestSymlinkName defines the symlink name used in tests.
+ internal static bool OneDriveTestOn;
+ internal static bool OneDriveTestRecurseOn;
+ internal static string OneDriveTestSymlinkName = "link-Beta";
+
/// This member is used for internal test purposes.
public static void SetTestHook(string property, object value)
{
diff --git a/src/System.Management.Automation/namespaces/FileSystemProvider.cs b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
index a32dc42174..2eb0de56d5 100644
--- a/src/System.Management.Automation/namespaces/FileSystemProvider.cs
+++ b/src/System.Management.Automation/namespaces/FileSystemProvider.cs
@@ -1891,9 +1891,14 @@ namespace Microsoft.PowerShell.Commands
}
bool hidden = false;
+ bool checkReparsePoint = true;
if (!Force)
{
hidden = (recursiveDirectory.Attributes & FileAttributes.Hidden) != 0;
+
+ // We've already taken the expense of initializing the Attributes property here,
+ // so we can use that to avoid needing to call IsReparsePointLikeSymlink() later.
+ checkReparsePoint = recursiveDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint);
}
// if "Hidden" is explicitly specified anywhere in the attribute filter, then override
@@ -1907,7 +1912,7 @@ namespace Microsoft.PowerShell.Commands
// c) it is not a reparse point with a target (not OneDrive or an AppX link).
if (tracker == null)
{
- if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
+ if (checkReparsePoint && InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(recursiveDirectory))
{
continue;
}
@@ -2062,7 +2067,7 @@ namespace Microsoft.PowerShell.Commands
{
if (instance?.BaseObject is FileSystemInfo fileInfo)
{
- if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo))
+ if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo))
{
return $"{PSStyle.Instance.FileInfo.SymbolicLink}{fileInfo.Name}{PSStyle.Instance.Reset} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}";
}
@@ -2090,7 +2095,7 @@ namespace Microsoft.PowerShell.Commands
else
{
return instance?.BaseObject is FileSystemInfo fileInfo
- ? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
+ ? InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(fileInfo)
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
: fileInfo.Name
: string.Empty;
@@ -3131,22 +3136,31 @@ namespace Microsoft.PowerShell.Commands
continueRemoval = ShouldProcess(directory.FullName, action);
}
- if (directory.Attributes.HasFlag(FileAttributes.ReparsePoint))
+ if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointLikeSymlink(directory))
{
+ void WriteErrorHelper(Exception exception)
+ {
+ WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
+ }
+
try
{
- // TODO:
- // Different symlinks seem to vary by behavior.
- // In particular, OneDrive symlinks won't remove without recurse,
- // but the .NET API here does not allow us to distinguish them.
- // We may need to revisit using p/Invokes here to get the right behavior
- directory.Delete();
+ if (InternalTestHooks.OneDriveTestOn)
+ {
+ WriteErrorHelper(new IOException());
+ return;
+ }
+ else
+ {
+ // Name surrogates should just be detached.
+ directory.Delete();
+ }
}
catch (Exception e)
{
string error = StringUtil.Format(FileSystemProviderStrings.CannotRemoveItem, directory.FullName, e.Message);
var exception = new IOException(error, e);
- WriteError(new ErrorRecord(exception, errorId: "DeleteSymbolicLinkFailed", ErrorCategory.WriteError, directory));
+ WriteErrorHelper(exception);
}
return;
@@ -8056,8 +8070,7 @@ namespace Microsoft.PowerShell.Commands
private static extern bool FindClose(IntPtr handle);
}
- // SetLastError is false as the use of this API doesn't not require GetLastError() to be called
- [DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = false, CharSet = CharSet.Unicode)]
+ [DllImport(PinvokeDllNames.FindFirstFileDllName, EntryPoint = "FindFirstFileExW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFindHandle FindFirstFileEx(string lpFileName, FINDEX_INFO_LEVELS fInfoLevelId, ref WIN32_FIND_DATA lpFindFileData, FINDEX_SEARCH_OPS fSearchOp, IntPtr lpSearchFilter, int dwAdditionalFlags);
internal enum FINDEX_INFO_LEVELS : uint
@@ -8248,28 +8261,55 @@ namespace Microsoft.PowerShell.Commands
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
}
- internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
+ internal static bool IsReparsePointLikeSymlink(FileSystemInfo fileInfo)
{
- if (!IsReparsePoint(fileInfo))
+#if UNIX
+ // Reparse point on Unix is a symlink.
+ return IsReparsePoint(fileInfo);
+#else
+ if (InternalTestHooks.OneDriveTestOn && fileInfo.Name == InternalTestHooks.OneDriveTestSymlinkName)
{
- return false;
+ return !InternalTestHooks.OneDriveTestRecurseOn;
}
-#if !UNIX
- // It is a reparse point and we should check some reparse point tags.
- var data = new WIN32_FIND_DATA();
- using (var handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
+
+ WIN32_FIND_DATA data = default;
+ string fullPath = Path.TrimEndingDirectorySeparator(fileInfo.FullName);
+ if (fullPath.Length > MAX_PATH)
{
+ fullPath = PathUtils.EnsureExtendedPrefix(fullPath);
+ }
+
+ using (SafeFindHandle handle = FindFirstFileEx(fullPath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
+ {
+ if (handle.IsInvalid)
+ {
+ // Our handle could be invalidated by something else touching the filesystem,
+ // so ensure we deal with that possibility here
+ int lastError = Marshal.GetLastWin32Error();
+ throw new Win32Exception(lastError);
+ }
+
+ // We already have the file attribute information from our Win32 call,
+ // so no need to take the expense of the FileInfo.FileAttributes call
+ const int FILE_ATTRIBUTE_REPARSE_POINT = 0x0400;
+ if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
+ {
+ // Not a reparse point.
+ return false;
+ }
+
// The name surrogate bit 0x20000000 is defined in https://docs.microsoft.com/windows/win32/fileio/reparse-point-tags
// Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem
// (like symlinks and mount points).
// In the case of OneDrive, they are not name surrogates and would be safe to recurse into.
- if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
+ if ((data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK))
{
return false;
}
}
-#endif
+
return true;
+#endif
}
internal static bool WinIsHardLink(FileSystemInfo fileInfo)
diff --git a/src/System.Management.Automation/utils/PathUtils.cs b/src/System.Management.Automation/utils/PathUtils.cs
index 991e3fa63b..45ba787aee 100644
--- a/src/System.Management.Automation/utils/PathUtils.cs
+++ b/src/System.Management.Automation/utils/PathUtils.cs
@@ -4,9 +4,10 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
+using System.Management.Automation.Internal;
+using System.Runtime.CompilerServices;
using System.Text;
-using System.Management.Automation.Internal;
using Dbg = System.Management.Automation.Diagnostics;
namespace System.Management.Automation
@@ -447,5 +448,110 @@ namespace System.Management.Automation
return false;
}
+
+ #region Helpers for long paths from .Net Runtime
+
+ // Code here is copied from .NET's internal path helper implementation:
+ // https://github.com/dotnet/runtime/blob/dcce0f56e10f5ac9539354b049341a2d7c0cdebf/src/libraries/System.Private.CoreLib/src/System/IO/PathInternal.Windows.cs
+ // It has been left as a verbatim copy.
+
+ internal static string EnsureExtendedPrefix(string path)
+ {
+ if (IsPartiallyQualified(path) || IsDevice(path))
+ return path;
+
+ // Given \\server\share in longpath becomes \\?\UNC\server\share
+ if (path.StartsWith(UncPathPrefix, StringComparison.OrdinalIgnoreCase))
+ return path.Insert(2, UncDevicePrefixToInsert);
+
+ return ExtendedDevicePathPrefix + path;
+ }
+
+ private const string ExtendedDevicePathPrefix = @"\\?\";
+ private const string UncPathPrefix = @"\\";
+ private const string UncDevicePrefixToInsert = @"?\UNC\";
+ private const string UncExtendedPathPrefix = @"\\?\UNC\";
+ private const string DevicePathPrefix = @"\\.\";
+
+ // \\?\, \\.\, \??\
+ private const int DevicePrefixLength = 4;
+
+ ///
+ /// Returns true if the given character is a valid drive letter
+ ///
+ private static bool IsValidDriveChar(char value)
+ {
+ return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'));
+ }
+
+ private static bool IsDevice(string path)
+ {
+ return IsExtended(path)
+ ||
+ (
+ path.Length >= DevicePrefixLength
+ && IsDirectorySeparator(path[0])
+ && IsDirectorySeparator(path[1])
+ && (path[2] == '.' || path[2] == '?')
+ && IsDirectorySeparator(path[3])
+ );
+ }
+
+ private static bool IsExtended(string path)
+ {
+ return path.Length >= DevicePrefixLength
+ && path[0] == '\\'
+ && (path[1] == '\\' || path[1] == '?')
+ && path[2] == '?'
+ && path[3] == '\\';
+ }
+
+ ///
+ /// Returns true if the path specified is relative to the current drive or working directory.
+ /// Returns false if the path is fixed to a specific drive or UNC path. This method does no
+ /// validation of the path (URIs will be returned as relative as a result).
+ ///
+ ///
+ /// Handles paths that use the alternate directory separator. It is a frequent mistake to
+ /// assume that rooted paths (Path.IsPathRooted) are not relative. This isn't the case.
+ /// "C:a" is drive relative- meaning that it will be resolved against the current directory
+ /// for C: (rooted, but relative). "C:\a" is rooted and not relative (the current directory
+ /// will not be used to modify the path).
+ ///
+ private static bool IsPartiallyQualified(string path)
+ {
+ if (path.Length < 2)
+ {
+ // It isn't fixed, it must be relative. There is no way to specify a fixed
+ // path with one character (or less).
+ return true;
+ }
+
+ if (IsDirectorySeparator(path[0]))
+ {
+ // There is no valid way to specify a relative path with two initial slashes or
+ // \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
+ return !(path[1] == '?' || IsDirectorySeparator(path[1]));
+ }
+
+ // The only way to specify a fixed path that doesn't begin with two slashes
+ // is the drive, colon, slash format- i.e. C:\
+ return !((path.Length >= 3)
+ && (path[1] == Path.VolumeSeparatorChar)
+ && IsDirectorySeparator(path[2])
+ // To match old behavior we'll check the drive character for validity as the path is technically
+ // not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
+ && IsValidDriveChar(path[0]));
+ }
+ ///
+ /// True if the given character is a directory separator.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsDirectorySeparator(char c)
+ {
+ return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
+ }
+
+ #endregion
}
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1
index f1b352a16b..70a18ff604 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1
@@ -92,6 +92,22 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" {
$existsAfter | Should -BeFalse
}
+ It "Verify Remove-Item for file" {
+ $longDir = 'a' * 250
+ $longSubDir = 'b' * 250
+ $fileName = "file1.txt"
+ $topPath = Join-Path $TestDrive $longDir
+ $longDirPath = Join-Path $topPath $longSubDir
+ $longFilePath = Join-Path $longDirPath $fileName
+ $null = New-Item -itemtype file -path $longFilePath -force
+
+ $longFilePath | Should -Exist
+
+ Remove-Item -Path $longFilePath -Force
+
+ $longFilePath | Should -Not -Exist
+ }
+
It "Verify Rename-Item for file" {
Rename-Item -Path $testFile -NewName $newTestFile -ErrorAction Stop
$testFile | Should -Not -Exist
@@ -570,7 +586,7 @@ Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows"
$omegaFile1 = Join-Path $omegaDir "OmegaFile1"
$omegaFile2 = Join-Path $omegaDir "OmegaFile2"
$betaDir = Join-Path $alphaDir "sub-Beta"
- $betaLink = Join-Path $alphaDir "link-Beta"
+ $betaLink = Join-Path $alphaDir "link-Beta" # Don't change! The name is hard-coded in PowerShell for OneDrive tests.
$betaFile1 = Join-Path $betaDir "BetaFile1.txt"
$betaFile2 = Join-Path $betaDir "BetaFile2.txt"
$betaFile3 = Join-Path $betaDir "BetaFile3.txt"
@@ -1529,3 +1545,119 @@ Describe "Windows admin tests" -Tag 'RequireAdminOnWindows' {
}
}
}
+
+Describe "OneDrive filesystem manipulation" -Tags @('CI', 'RequireAdminOnWindows') {
+ BeforeAll {
+ # on macOS, the /tmp directory is a symlink, so we'll resolve it here
+ $TestPath = $TestDrive
+ if ($IsMacOS)
+ {
+ $item = Get-Item $TestPath
+ $dirName = $item.BaseName
+ $item = Get-Item $item.PSParentPath -Force
+ if ($item.LinkType -eq "SymbolicLink")
+ {
+ $TestPath = Join-Path $item.Target $dirName
+ }
+ }
+
+ $realFile = Join-Path $TestPath "file.txt"
+ $nonFile = Join-Path $TestPath "not-a-file"
+ $fileContent = "some text"
+ $realDir = Join-Path $TestPath "subdir"
+ $nonDir = Join-Path $TestPath "not-a-dir"
+ $hardLinkToFile = Join-Path $TestPath "hard-to-file.txt"
+ $symLinkToFile = Join-Path $TestPath "sym-link-to-file.txt"
+ $symLinkToDir = Join-Path $TestPath "sym-link-to-dir"
+ $symLinkToNothing = Join-Path $TestPath "sym-link-to-nowhere"
+ $dirSymLinkToDir = Join-Path $TestPath "symd-link-to-dir"
+ $junctionToDir = Join-Path $TestPath "junction-to-dir"
+
+ New-Item -ItemType File -Path $realFile -Value $fileContent > $null
+ New-Item -ItemType Directory -Path $realDir > $null
+
+ $alphaDir = Join-Path $TestDrive "sub-alpha"
+ $alphaLink = Join-Path $TestDrive "link-alpha"
+ $alphaFile1 = Join-Path $alphaDir "AlphaFile1.txt"
+ $alphaFile2 = Join-Path $alphaDir "AlphaFile2.txt"
+ $omegaDir = Join-Path $TestDrive "sub-omega"
+ $omegaFile1 = Join-Path $omegaDir "OmegaFile1"
+ $omegaFile2 = Join-Path $omegaDir "OmegaFile2"
+ $betaDir = Join-Path $alphaDir "sub-Beta"
+ $betaLink = Join-Path $alphaDir "link-Beta" # Don't change! The name is hard-coded in PowerShell for OneDrive tests.
+ $betaFile1 = Join-Path $betaDir "BetaFile1.txt"
+ $betaFile2 = Join-Path $betaDir "BetaFile2.txt"
+ $betaFile3 = Join-Path $betaDir "BetaFile3.txt"
+ $gammaDir = Join-Path $betaDir "sub-gamma"
+ $uponeLink = Join-Path $gammaDir "upone-link"
+ $uptwoLink = Join-Path $gammaDir "uptwo-link"
+ $omegaLink = Join-Path $gammaDir "omegaLink"
+
+ New-Item -ItemType Directory -Path $alphaDir
+ New-Item -ItemType File -Path $alphaFile1
+ New-Item -ItemType File -Path $alphaFile2
+ New-Item -ItemType Directory -Path $betaDir
+ New-Item -ItemType File -Path $betaFile1
+ New-Item -ItemType File -Path $betaFile2
+ New-Item -ItemType File -Path $betaFile3
+ New-Item -ItemType Directory $omegaDir
+ New-Item -ItemType File -Path $omegaFile1
+ New-Item -ItemType File -Path $omegaFile2
+ }
+
+ AfterAll {
+ Remove-Item -Path $alphaLink -Force -ErrorAction SilentlyContinue
+ Remove-Item -Path $betaLink -Force -ErrorAction SilentlyContinue
+ }
+
+ BeforeEach {
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $true)
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
+ }
+
+ AfterEach {
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $false)
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestOn', $false)
+ }
+
+ It "Get-ChildItem will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
+ New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir -Force
+ New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir -Force
+
+ # '$betaDir' is a symlink - we don't follow symlinks
+ # This emulates PowerShell 6.2 and below behavior.
+ $ci = Get-ChildItem -Path $alphaDir -Recurse
+ $ci.Count | Should -BeExactly 7
+
+ # Now we follow the symlink like on OneDrive.
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
+ $ci = Get-ChildItem -Path $alphaDir -Recurse
+ $ci.Count | Should -BeExactly 10
+ }
+
+ It "Remove-Item will recurse into emulated OneDrive directory" -Skip:(-not $IsWindows) {
+ $alphaDir = Join-Path $TestDrive "sub-alpha2"
+ $alphaLink = Join-Path $TestDrive "link-alpha2"
+ $alphaFile1 = Join-Path $alphaDir "AlphaFile1.txt"
+ $betaDir = Join-Path $alphaDir "sub-Beta"
+ $betaLink = Join-Path $alphaDir "link-Beta"
+ $betaFile1 = Join-Path $betaDir "BetaFile1.txt"
+
+ New-Item -ItemType Directory -Path $alphaDir > $null
+ New-Item -ItemType File -Path $alphaFile1 > $null
+ New-Item -ItemType Directory -Path $betaDir > $null
+ New-Item -ItemType File -Path $betaFile1 > $null
+
+ New-Item -ItemType SymbolicLink -Path $alphaLink -Value $alphaDir > $null
+ New-Item -ItemType SymbolicLink -Path $betaLink -Value $betaDir > $null
+
+ # With the test hook turned on we don't remove '$betaDir' symlink.
+ # This emulates PowerShell 7.1 and below behavior.
+ { Remove-Item -Path $betaLink -Recurse -ErrorAction Stop } | Should -Throw -ErrorId "DeleteSymbolicLinkFailed,Microsoft.PowerShell.Commands.RemoveItemCommand"
+
+ # Now we emulate OneDrive and follow the symlink like on OneDrive.
+ [System.Management.Automation.Internal.InternalTestHooks]::SetTestHook('OneDriveTestRecurseOn', $true)
+ Remove-Item -Path $betaLink -Recurse
+ Test-Path -Path $betaLink | Should -BeFalse
+ }
+}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1
index 3e8ccb8fa9..28c5ed6052 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Remove-Item.Tests.ps1
@@ -1,170 +1,195 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
+
Describe "Remove-Item" -Tags "CI" {
- $testpath = $TestDrive
- $testfile = "testfile.txt"
- $testfilepath = Join-Path -Path $testpath -ChildPath $testfile
+ BeforeAll {
+ $testpath = $TestDrive
+ $testfile = "testfile.txt"
+ $testfilepath = Join-Path -Path $testpath -ChildPath $testfile
+ }
+
Context "File removal Tests" {
- BeforeEach {
- New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
- Test-Path $testfilepath | Should -BeTrue
- }
+ BeforeEach {
+ New-Item -Name $testfile -Path $testpath -ItemType "file" -Value "lorem ipsum" -Force
+ Test-Path $testfilepath | Should -BeTrue
+ }
- It "Should be able to be called on a regular file without error using the Path parameter" {
- { Remove-Item -Path $testfilepath } | Should -Not -Throw
+ It "Should be able to be called on a regular file without error using the Path parameter" {
+ { Remove-Item -Path $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to be called on a file without the Path parameter" {
- { Remove-Item $testfilepath } | Should -Not -Throw
+ It "Should be able to be called on a file without the Path parameter" {
+ { Remove-Item $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to call the rm alias" {
- { rm $testfilepath } | Should -Not -Throw
+ It "Should be able to call the rm alias" {
+ { rm $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to call the del alias" {
- { del $testfilepath } | Should -Not -Throw
+ It "Should be able to call the del alias" {
+ { del $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to call the erase alias" {
- { erase $testfilepath } | Should -Not -Throw
+ It "Should be able to call the erase alias" {
+ { erase $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to call the ri alias" {
- { ri $testfilepath } | Should -Not -Throw
+ It "Should be able to call the ri alias" {
+ { ri $testfilepath } | Should -Not -Throw
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should not be able to remove a read-only document without using the force switch" {
- # Set to read only
- Set-ItemProperty -Path $testfilepath -Name IsReadOnly -Value $true
+ It "Should not be able to remove a read-only document without using the force switch" {
+ # Set to read only
+ Set-ItemProperty -Path $testfilepath -Name IsReadOnly -Value $true
- # attempt to remove the file
- { Remove-Item $testfilepath -ErrorAction SilentlyContinue } | Should -Not -Throw
+ # attempt to remove the file
+ { Remove-Item $testfilepath -ErrorAction SilentlyContinue } | Should -Not -Throw
- # validate
- Test-Path $testfilepath | Should -BeTrue
+ # validate
+ Test-Path $testfilepath | Should -BeTrue
- # remove using the -force switch on the readonly object
- Remove-Item $testfilepath -Force
+ # remove using the -force switch on the readonly object
+ Remove-Item $testfilepath -Force
- # Validate
- Test-Path $testfilepath | Should -BeFalse
- }
+ # Validate
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to remove all files matching a regular expression with the include parameter" {
- # Create multiple files with specific string
- New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
- New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
- New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
- # Create a single file that does not match that string - already done in BeforeEach
+ It "Should be able to remove all files matching a regular expression with the include parameter" {
+ # Create multiple files with specific string
+ New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ New-Item -Name file2.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ New-Item -Name file3.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ # Create a single file that does not match that string - already done in BeforeEach
- # Delete the specific string
- Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt
- # validate that the string under test was deleted, and the nonmatching strings still exist
- Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
- Test-Path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse
- Test-Path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse
- Test-Path $testfilepath | Should -BeTrue
+ # Delete the specific string
+ Remove-Item (Join-Path -Path $testpath -ChildPath "*") -Include file*.txt
+ # validate that the string under test was deleted, and the nonmatching strings still exist
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
+ Test-Path (Join-Path -Path $testpath -ChildPath file2.txt) | Should -BeFalse
+ Test-Path (Join-Path -Path $testpath -ChildPath file3.txt) | Should -BeFalse
+ Test-Path $testfilepath | Should -BeTrue
- # Delete the non-matching strings
- Remove-Item $testfilepath
+ # Delete the non-matching strings
+ Remove-Item $testfilepath
- Test-Path $testfilepath | Should -BeFalse
- }
+ Test-Path $testfilepath | Should -BeFalse
+ }
- It "Should be able to not remove any files matching a regular expression with the exclude parameter" {
- # Create multiple files with specific string
- New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum"
- New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ It "Should be able to not remove any files matching a regular expression with the exclude parameter" {
+ # Create multiple files with specific string
+ New-Item -Name file1.wav -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ New-Item -Name file2.wav -Path $testpath -ItemType "file" -Value "lorem ipsum"
- # Create a single file that does not match that string
- New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
+ # Create a single file that does not match that string
+ New-Item -Name file1.txt -Path $testpath -ItemType "file" -Value "lorem ipsum"
- # Delete the specific string
- Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt
+ # Delete the specific string
+ Remove-Item (Join-Path -Path $testpath -ChildPath "file*") -Exclude *.wav -Include *.txt
- # validate that the string under test was deleted, and the nonmatching strings still exist
- Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue
- Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue
- Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
+ # validate that the string under test was deleted, and the nonmatching strings still exist
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeTrue
+ Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeTrue
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.txt) | Should -BeFalse
- # Delete the non-matching strings
- Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav)
- Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav)
+ # Delete the non-matching strings
+ Remove-Item (Join-Path -Path $testpath -ChildPath file1.wav)
+ Remove-Item (Join-Path -Path $testpath -ChildPath file2.wav)
- Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse
- Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse
- }
+ Test-Path (Join-Path -Path $testpath -ChildPath file1.wav) | Should -BeFalse
+ Test-Path (Join-Path -Path $testpath -ChildPath file2.wav) | Should -BeFalse
+ }
}
Context "Directory Removal Tests" {
- $testdirectory = Join-Path -Path $testpath -ChildPath testdir
- $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd
- BeforeEach {
- New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force
+ BeforeAll {
+ $testdirectory = Join-Path -Path $testpath -ChildPath testdir
+ $testsubdirectory = Join-Path -Path $testdirectory -ChildPath subd
+ }
- Test-Path $testdirectory | Should -BeTrue
- }
+ BeforeEach {
+ New-Item -Name "testdir" -Path $testpath -ItemType "directory" -Force
- It "Should be able to remove a directory" {
- { Remove-Item $testdirectory } | Should -Not -Throw
+ Test-Path $testdirectory | Should -BeTrue
+ }
- Test-Path $testdirectory | Should -BeFalse
- }
+ It "Should be able to remove a directory" {
+ { Remove-Item $testdirectory -ErrorAction Stop } | Should -Not -Throw
- It "Should be able to recursively delete subfolders" {
- New-Item -Name "subd" -Path $testdirectory -ItemType "directory"
- New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum"
+ Test-Path $testdirectory | Should -BeFalse
+ }
- $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile
- Test-Path $complexDirectory | Should -BeTrue
+ It "Should be able to recursively delete subfolders" {
+ New-Item -Name "subd" -Path $testdirectory -ItemType "directory"
+ New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum"
- { Remove-Item $testdirectory -Recurse} | Should -Not -Throw
+ $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile
+ Test-Path $complexDirectory | Should -BeTrue
- Test-Path $testdirectory | Should -BeFalse
- }
+ { Remove-Item $testdirectory -Recurse -ErrorAction Stop } | Should -Not -Throw
+
+ Test-Path $testdirectory | Should -BeFalse
+ }
+
+ It "Should be able to recursively delete a directory with a trailing backslash" {
+ New-Item -Name "subd" -Path $testdirectory -ItemType "directory"
+ New-Item -Name $testfile -Path $testsubdirectory -ItemType "file" -Value "lorem ipsum"
+
+ $complexDirectory = Join-Path -Path $testsubdirectory -ChildPath $testfile
+ Test-Path $complexDirectory | Should -BeTrue
+
+ $testdirectoryWithBackSlash = Join-Path -Path $testdirectory -ChildPath ([IO.Path]::DirectorySeparatorChar)
+ Test-Path $testdirectoryWithBackSlash | Should -BeTrue
+
+ { Remove-Item $testdirectoryWithBackSlash -Recurse -ErrorAction Stop } | Should -Not -Throw
+
+ Test-Path $testdirectoryWithBackSlash | Should -BeFalse
+ Test-Path $testdirectory | Should -BeFalse
+ }
}
Context "Alternate Data Streams should be supported on Windows" {
- BeforeAll {
- if (!$IsWindows) {
- return
- }
- $fileName = "ADStest.txt"
- $streamName = "teststream"
- $dirName = "ADStestdir"
- $fileContent =" This is file content."
- $streamContent = "datastream content here"
- $streamfile = Join-Path -Path $testpath -ChildPath $fileName
- $streamdir = Join-Path -Path $testpath -ChildPath $dirName
+ BeforeAll {
+ if (!$IsWindows) {
+ return
+ }
+ $fileName = "ADStest.txt"
+ $streamName = "teststream"
+ $dirName = "ADStestdir"
+ $fileContent =" This is file content."
+ $streamContent = "datastream content here"
+ $streamfile = Join-Path -Path $testpath -ChildPath $fileName
+ $streamdir = Join-Path -Path $testpath -ChildPath $dirName
- $null = New-Item -Path $streamfile -ItemType "File" -force
- Add-Content -Path $streamfile -Value $fileContent
- Add-Content -Path $streamfile -Stream $streamName -Value $streamContent
- $null = New-Item -Path $streamdir -ItemType "Directory" -Force
- Add-Content -Path $streamdir -Stream $streamName -Value $streamContent
- }
- It "Should completely remove a datastream from a file" -Skip:(!$IsWindows) {
- Get-Item -Path $streamfile -Stream $streamName | Should -Not -BeNullOrEmpty
- Remove-Item -Path $streamfile -Stream $streamName
- Get-Item -Path $streamfile -Stream $streamName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
- }
- It "Should completely remove a datastream from a directory" -Skip:(!$IsWindows) {
- Get-Item -Path $streamdir -Stream $streamName | Should -Not -BeNullOrEmpty
- Remove-Item -Path $streamdir -Stream $streamName
- Get-Item -Path $streamdir -Stream $streamname -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
- }
+ $null = New-Item -Path $streamfile -ItemType "File" -force
+ Add-Content -Path $streamfile -Value $fileContent
+ Add-Content -Path $streamfile -Stream $streamName -Value $streamContent
+ $null = New-Item -Path $streamdir -ItemType "Directory" -Force
+ Add-Content -Path $streamdir -Stream $streamName -Value $streamContent
+ }
+
+ It "Should completely remove a datastream from a file" -Skip:(!$IsWindows) {
+ Get-Item -Path $streamfile -Stream $streamName | Should -Not -BeNullOrEmpty
+ Remove-Item -Path $streamfile -Stream $streamName
+ Get-Item -Path $streamfile -Stream $streamName -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
+ }
+
+ It "Should completely remove a datastream from a directory" -Skip:(!$IsWindows) {
+ Get-Item -Path $streamdir -Stream $streamName | Should -Not -BeNullOrEmpty
+ Remove-Item -Path $streamdir -Stream $streamName
+ Get-Item -Path $streamdir -Stream $streamname -ErrorAction SilentlyContinue | Should -BeNullOrEmpty
+ }
}
}