Change behavior of Remove-Item on symbolic links (#621) (#3637)

When 'Remove-Item' is used to remove a symbolic link in Windows, only the link itself is removed. The '-Force' switch is no longer required.
If the directory pointed to by the link has child items, the cmdlet no longer prompts the user to remove the child items---those child items are not removed. The '-Recurse' switch, if given, is ignored.
This brings 'Remove-Item' more in line with the behavior of the 'rm' command on Unix.
This commit is contained in:
jeffbi 2017-04-27 17:47:24 -07:00 committed by Dongbo Wang
parent a2268ab3ec
commit f1769fe7a8
4 changed files with 211 additions and 42 deletions

View File

@ -3256,7 +3256,7 @@ namespace Microsoft.PowerShell.Commands
try
{
System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(providerPath);
if (!Platform.IsWindows && di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
{
shouldRecurse = false;
treatAsFile = true;

View File

@ -2863,15 +2863,6 @@ namespace Microsoft.PowerShell.Commands
continueRemoval = ShouldProcess(directory.FullName, action);
}
//if this is a reparse point and force is not specified then warn user but dont remove the directory.
if (Platform.IsWindows && ((directory.Attributes & FileAttributes.ReparsePoint) != 0) && !Force)
{
String error = StringUtil.Format(FileSystemProviderStrings.DirectoryReparsePoint, directory.FullName);
Exception e = new IOException(error);
WriteError(new ErrorRecord(e, "DirectoryNotEmpty", ErrorCategory.WriteError, directory));
return;
}
if ((directory.Attributes & FileAttributes.ReparsePoint) != 0)
{
bool success = InternalSymbolicLinkLinkCodeMethods.DeleteJunction(directory.FullName);
@ -3327,13 +3318,14 @@ namespace Microsoft.PowerShell.Commands
path = NormalizePath(path);
// First check to see if it is a directory
try
{
DirectoryInfo directory = new DirectoryInfo(path);
// If the above didn't throw an exception, check to
// see if it contains any directories
// see if we should proceed and if it contains any children
if ((directory.Attributes & FileAttributes.Directory) != FileAttributes.Directory)
return false;
result = DirectoryInfoHasChildItems(directory);
}

View File

@ -216,9 +216,6 @@
<data name="DirectoryExist" xml:space="preserve">
<value>An item with the specified name {0} already exists.</value>
</data>
<data name="DirectoryReparsePoint" xml:space="preserve">
<value>{0} is an NTFS junction point. Use the Force parameter to delete or modify this object.</value>
</data>
<data name="BasePathLengthError" xml:space="preserve">
<value>The path length is too short. The character length of a path cannot be less than the character length of the basePath.</value>
</data>

View File

@ -246,6 +246,186 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" {
}
}
Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows" {
BeforeAll {
# on macOS, the /tmp directory is a symlink, so we'll resolve it here
$TestPath = $TestDrive
if ($IsOSX)
{
$item = Get-Item $TestPath
$dirName = $item.BaseName
$item = Get-Item $item.PSParentPath
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
}
Context "New-Item and hard/symbolic links" {
It "New-Item can create a hard link to a file" {
New-Item -ItemType HardLink -Path $hardLinkToFile -Value $realFile
Test-Path $hardLinkToFile | Should Be $true
$link = Get-Item -Path $hardLinkToFile
$link.LinkType | Should BeExactly "HardLink"
Get-Content -Path $hardLinkToFile | Should be $fileContent
}
It "New-Item can create symbolic link to file" {
New-Item -ItemType SymbolicLink -Path $symLinkToFile -Value $realFile
Test-Path $symLinkToFile | Should Be $true
$real = Get-Item -Path $realFile
$link = Get-Item -Path $symLinkToFile
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
Get-Content -Path $symLinkToFile | Should be $fileContent
}
It "New-Item can create a symbolic link to nothing" {
New-Item -ItemType SymbolicLink -Path $symLinkToNothing -Value $nonFile
Test-Path $symLinkToNothing | Should Be $true
$link = Get-Item -Path $symLinkToNothing
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $nonFile
}
It "New-Item can create a symbolic link to a directory" -Skip:($IsWindows) {
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
Test-Path $symLinkToDir | Should Be $true
$real = Get-Item -Path $realDir
$link = Get-Item -Path $symLinkToDir
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
}
It "New-Item can create a directory symbolic link to a directory" -Skip:(-Not $IsWindows) {
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
Test-Path $symLinkToDir | Should Be $true
$real = Get-Item -Path $realDir
$link = Get-Item -Path $symLinkToDir
$link | Should BeOfType System.IO.DirectoryInfo
$link.LinkType | Should BeExactly "SymbolicLink"
$link.Target | Should Be $real.FullName
}
It "New-Item can create a directory junction to a directory" -Skip:(-Not $IsWindows) {
New-Item -ItemType Junction -Path $junctionToDir -Value $realDir
Test-Path $junctionToDir | Should Be $true
}
}
Context "Remove-Item and hard/symbolic links" {
BeforeAll {
$testCases = @(
@{
Name = "Remove-Item can remove a hard link to a file"
Link = $hardLinkToFile
Target = $realFile
}
@{
Name = "Remove-Item can remove a symbolic link to a file"
Link = $symLinkToFile
Target = $realFile
}
)
# New-Item on Windows will not create a "plain" symlink to a directory
$unixTestCases = @(
@{
Name = "Remove-Item can remove a symbolic link to a directory on Unix"
Link = $symLinkToDir
Target = $realDir
}
)
# Junctions and directory symbolic links are Windows and NTFS only
$windowsTestCases = @(
@{
Name = "Remove-Item can remove a symbolic link to a directory on Windows"
Link = $symLinkToDir
Target = $realDir
}
@{
Name = "Remove-Item can remove a directory symbolic link to a directory on Windows"
Link = $dirSymLinkToDir
Target = $realDir
}
@{
Name = "Remove-Item can remove a junction to a directory"
Link = $junctionToDir
Target = $realDir
}
)
function TestRemoveItem
{
Param (
[string]$Link,
[string]$Target
)
Remove-Item -Path $Link -ErrorAction SilentlyContinue >$null
Test-Path -Path $Link | Should Be $false
Test-Path -Path $Target | Should Be $true
}
}
It "<Name>" -TestCases $testCases {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)
TestRemoveItem $Link $Target
}
It "<Name>" -TestCases $unixTestCases -Skip:($IsWindows) {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)
TestRemoveItem $Link $Target
}
It "<Name>" -TestCases $windowsTestCases -Skip:(-not $IsWindows) {
Param (
[string]$Name,
[string]$Link,
[string]$Target
)
TestRemoveItem $Link $Target
}
It "Remove-Item ignores -Recurse switch when deleting symlink to directory" {
$folder = Join-Path $TestDrive "folder"
$file = Join-Path $TestDrive "folder" "file"
$link = Join-Path $TestDrive "sym-to-folder"
New-Item -ItemType Directory -Path $folder >$null
New-Item -ItemType File -Path $file -Value "some content" >$null
New-Item -ItemType SymbolicLink -Path $link -value $folder >$null
$childA = Get-Childitem $folder
Remove-Item -Path $link -Recurse
$childB = Get-ChildItem $folder
$childB.Count | Should Be 1
$childB.Count | Should BeExactly $childA.Count
$childB.Name | Should BeExactly $childA.Name
}
}
}
Describe "Copy-Item can avoid copying an item onto itself" -Tags "CI", "RequireAdminOnWindows" {
BeforeAll {