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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -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 {