8000 Change behavior of Remove-Item on symbolic links (#621) (#3637) · PowerShell/PowerShell@f1769fe · GitHub
[go: up one dir, main page]

Skip to content

Commit f1769fe

Browse files
jeffbidaxian-dbw
authored andcommitted
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.
1 parent a2268ab commit f1769fe

File tree

4 files changed

+211
-42
lines changed

4 files changed

+211
-42
lines changed

src/Microsoft.PowerShell.Commands.Management/commands/management/Navigation.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3256,7 +3256,7 @@ protected override void ProcessRecord()
32563256
try
32573257
{
32583258
System.IO.DirectoryInfo di = new System.IO.DirectoryInfo(providerPath);
3259-
if (!Platform.IsWindows && di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
3259+
if (di != null && (di.Attributes & System.IO.FileAttributes.ReparsePoint) != 0)
32603260
{
32613261
shouldRecurse = false;
32623262
treatAsFile = true;

src/System.Management.Automation/namespaces/FileSystemProvider.cs

+3-11
Original file line numberDiff line numberDiff line change
@@ -2863,15 +2863,6 @@ private void RemoveDirectoryInfoItem(DirectoryInfo directory, bool recurse, bool
28632863
continueRemoval = ShouldProcess(directory.FullName, action);
28642864
}
28652865

2866-
//if this is a reparse point and force is not specified then warn user but dont remove the directory.
2867-
if (Platform.IsWindows && ((directory.Attributes & FileAttributes.ReparsePoint) != 0) && !Force)
2868-
{
2869-
String error = StringUtil.Format(FileSystemProviderStrings.DirectoryReparsePoint, directory.FullName);
2870-
Exception e = new IOException(error);
2871-
WriteError(new ErrorRecord(e, "DirectoryNotEmpty", ErrorCategory.WriteError, directory));
2872-
return;
2873-
}
2874-
28752866
if ((directory.Attributes & FileAttributes.ReparsePoint) != 0)
28762867
{
28772868
bool success = InternalSymbolicLinkLinkCodeMethods.DeleteJunction(directory.FullName);
@@ -3327,13 +3318,14 @@ protected override bool HasChildItems(string path)
33273318
path = NormalizePath(path);
33283319

33293320
// First check to see if it is a directory
3330-
33313321
try
33323322
{
33333323
DirectoryInfo directory = new DirectoryInfo(path);
33343324

33353325
// If the above didn't throw an exception, check to
3336-
// see if it contains any directories
3326+
// see if we should proceed and if it contains any children
3327+
if ((directory.Attributes & FileAttributes.Directory) != FileAttributes.Directory)
3328+
return false;
33373329

33383330
result = DirectoryInfoHasChildItems(directory);
33393331
}

src/System.Management.Automation/resources/FileSystemProviderStrings.resx

+27-30
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
F438
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -216,9 +216,6 @@
216216
<data name="DirectoryExist" xml:space="preserve">
217217
<value>An item with the specified name {0} already exists.</value>
218218
</data>
219-
<data name="DirectoryReparsePoint" xml:space="preserve">
220-
<value>{0} is an NTFS junction point. Use the Force parameter to delete or modify this object.</value>
221-
</data>
222219
<data name="BasePathLengthError" xml:space="preserve">
223220
<value>The path length is too short. The character length of a path cannot be less than the character length of the basePath.</value>
224221
</data>

test/powershell/Modules/Microsoft.PowerShell.Management/FileSystem.Tests.ps1

+180
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,186 @@ Describe "Basic FileSystem Provider Tests" -Tags "CI" {
246246
}
247247
}
248248

249+
Describe "Hard link and symbolic link tests" -Tags "CI", "RequireAdminOnWindows" {
250+
BeforeAll {
251+
# on macOS, the /tmp directory is a symlink, so we'll resolve it here
252+
$TestPath = $TestDrive
253+
if ($IsOSX)
254+
{
255+
$item = Get-Item $TestPath
256+
$dirName = $item.BaseName
257+
$item = Get-Item $item.PSParentPath
258+
if ($item.LinkType -eq "SymbolicLink")
259+
{
260+
$TestPath = Join-Path $item.Target $dirName
261+
}
262+
}
263+
264+
$realFile = Join-Path $TestPath "file.txt"
265+
$nonFile = Join-Path $TestPath "not-a-file"
266+
$fileContent = "some text"
267+
$realDir = Join-Path $TestPath "subdir"
268+
$nonDir = Join-Path $TestPath "not-a-dir"
269+
$hardLinkToFile = Join-Path $TestPath "hard-to-file.txt"
270+
$symLinkToFile = Join-Path $TestPath "sym-link-to-file.txt"
271+
$symLinkToDir = Join-Path $TestPath "sym-link-to-dir"
272+
$symLinkToNothing = Join-Path $TestPath "sym-link-to-nowhere"
273+
$dirSymLinkToDir = Join-Path $TestPath "symd-link-to-dir"
274+
$junctionToDir = Join-Path $TestPath "junction-to-dir"
275+
276+
New-Item -ItemType File -Path $realFile -Value $fileContent >$null
277+
New-Item -ItemType Directory -Path $realDir >$null
278+
}
279+
280+
Context "New-Item and hard/symbolic links" {
281+
It "New-Item can create a hard link to a file" {
282+
New-Item -ItemType HardLink -Path $hardLinkToFile -Value $realFile
283+
Test-Path $hardLinkToFile | Should Be $true
284+
$link = Get-Item -Path $hardLinkToFile
285+
$link.LinkType | Should BeExactly "HardLink"
286+
Get-Content -Path $hardLinkToFile | Should be $fileContent
287+
}
288+
It "New-Item can create symbolic link to file" {
289+
New-Item -ItemType SymbolicLink -Path $symLinkToFile -Value $realFile
290+
Test-Path $symLinkToFile | Should Be $true
291+
$real = Get-Item -Path $realFile
292+
$link = Get-Item -Path $symLinkToFile
293+
$link.LinkType | Should BeExactly "SymbolicLink"
294+
$link.Target | Should Be $real.FullName
295+
Get-Content -Path $symLinkToFile | Should be $fileContent
296+
}
297+
It "New-Item can create a symbolic link to nothing" {
298+
New-Item -ItemType SymbolicLink -Path $symLinkToNothing -Value $nonFile
299+
Test-Path $symLinkToNothing | Should Be $true
300+
$link = Get-Item -Path $symLinkToNothing
301+
$link.LinkType | Should BeExactly "SymbolicLink"
302+
$link.Target | Should Be $nonFile
303+
}
304+
It "New-Item can create a symbolic link to a directory" -Skip:($IsWindows) {
305+
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
306+
Test-Path $symLinkToDir | Should Be $true
307+
$real = Get-Item -Path $realDir
308+
$link = Get-Item -Path $symLinkToDir
309+
$link.LinkType | Should BeExactly "SymbolicLink"
310+
$link.Target | Should Be $real.FullName
311+
}
312+
It "New-Item can create a directory symbolic link to a directory" -Skip:(-Not $IsW 10000 indows) {
313+
New-Item -ItemType SymbolicLink -Path $symLinkToDir -Value $realDir
314+
Test-Path $symLinkToDir | Should Be $true
315+
$real = Get-Item -Path $realDir
316+
$link = Get-Item -Path $symLinkToDir
317+
$link | Should BeOfType System.IO.DirectoryInfo
318+
$link.LinkType | Should BeExactly "SymbolicLink"
319+
$link.Target | Should Be $real.FullName
320+
}
321+
It "New-Item can create a directory junction to a directory" -Skip:(-Not $IsWindows) {
322+
New-Item -ItemType Junction -Path $junctionToDir -Value $realDir
323+
Test-Path $junctionToDir | Should Be $true
324+
}
325+
}
326+
327+
Context "Remove-Item and hard/symbolic links" {
328+
BeforeAll {
329+
$testCases = @(
330+
@{
331+
Name = "Remove-Item can remove a hard link to a file"
332+
Link = $hardLinkToFile
333+
Target = $realFile
334+
}
335+
@{
336+
Name = "Remove-Item can remove a symbolic link to a file"
337+
Link = $symLinkToFile
338+
Target = $realFile
339+
}
340+
)
341+
342+
# New-Item on Windows will not create a "plain" symlink to a directory
343+
$unixTestCases = @(
344+
@{
345+
Name = "Remove-Item can remove a symbolic link to a directory on Unix"
346+
Link = $symLinkToDir
347+
Target = $realDir
348+
}
349+
)
350+
351+
# Junctions and directory symbolic links are Windows and NTFS only
352+
$windowsTestCases = @(
353+
@{
354+
Name = "Remove-Item can remove a symbolic link to a directory on Windows"
355+
Link = $symLinkToDir
356+
Target = $realDir
357+
}
358+
@{
359+
Name = "Remove-Item can remove a directory symbolic link to a directory on Windows"
360+
Link = $dirSymLinkToDir
361+
Target = $realDir
362+
}
363+
@{
364+
Name = "Remove-Item can remove a junction to a directory"
365+
Link = $junctionToDir
366+
Target = $realDir
367+
}
368+
)
369+
370+
function TestRemoveItem
371+
{
372+
Param (
373+
[string]$Link,
374+
[string]$Target
375+
)
376+
377+
Remove-Item -Path $Link -ErrorAction SilentlyContinue >$null
378+
Test-Path -Path $Link | Should Be $false
379+
Test-Path -Path $Target | Should Be $true
380+
}
381+
}
382+
383+
It "<Name>" -TestCases $testCases {
384+
Param (
385+
[string]$Name,
386+
[string]$Link,
387+
[string]$Target
388+
)
389+
390+
TestRemoveItem $Link $Target
391+
}
392+
393+
It "<Name>" -TestCases $unixTestCases -Skip:($IsWindows) {
394+
Param (
395+
[string]$Name,
396+
[string]$Link,
397+
[string]$Target
398+
)
399+
400+
TestRemoveItem $Link $Target
401+
}
402+
403+
It "<Name>" -TestCases $windowsTestCases -Skip:(-not $IsWindows) {
404+
Param (
405+
[string]$Name,
406+
[string]$Link,
407+
[string]$Target
408+
)
409+
410+
TestRemoveItem $Link $Target
411+
}
412+
413+
It "Remove-Item ignores -Recurse switch when deleting symlink to directory" {
414+
$folder = Join-Path $TestDrive "folder"
415+
$file = Join-Path $TestDrive "folder" "file"
416+
$link = Join-Path $TestDrive "sym-to-folder"
417+
New-Item -ItemType Directory -Path $folder >$null
418+
New-Item -ItemType File -Path $file -Value "some content" >$null
419+
New-Item -ItemType SymbolicLink -Path $link -value $folder >$null
420+
$childA = Get-Childitem $folder
421+
Remove-Item -Path $link -Recurse
422+
$childB = Get-ChildItem $folder
423+
$childB.Count | Should Be 1
424+
$childB.Count | Should BeExactly $childA.Count
425+
$childB.Name | Should BeExactly $childA.Name
426+
}
427+
}
428+
}
249429

250430
Describe "Copy-Item can avoid copying an item onto itself" -Tags "CI", "RequireAdminOnWindows" {
251431
BeforeAll {

0 commit comments

Comments
 (0)
0