无法删除项目,目录不为空
Posted
技术标签:
【中文标题】无法删除项目,目录不为空【英文标题】:Cannot remove item, The Directory is not empty 【发布时间】:2019-05-02 09:13:46 【问题描述】:使用Remove-Item
命令时,即使使用-r
和-Force
参数,有时也会返回如下错误信息:
Remove-Item : 无法删除项目 C:\Test Folder\Test Folder\Target: 目录不为空。
尤其是在 Windows 资源管理器中打开要删除的目录时会发生这种情况。
现在,虽然可以通过关闭 Windows 资源管理器或不浏览该位置来避免这种情况,但我在多用户环境中工作我的脚本,人们有时只是忘记关闭 Windows 资源管理器窗口,我对删除整个文件夹和目录的解决方案,即使它们是在 Windows 资源管理器中打开的。
我可以设置比-Force
更强大的选项来实现此目的吗?
为了可靠地重现这一点,请创建文件夹 C:\Test Folder\Origin
并用一些文件和子文件夹(重要)填充它,然后使用以下脚本或类似的脚本并执行一次。现在打开C:\Test Folder\Target
的一个子文件夹(在我的例子中,我使用了包含A third file.txt
的C:\Test Folder\Target\Another Subfolder
),然后再次尝试运行脚本。您现在将收到错误消息。如果您第三次运行脚本,您将不会再次收到错误(取决于我尚未确定的情况,但有时会第二次发生错误,然后再也不会发生,有时会发生 每隔第二次)。
$SourcePath = "C:\Test Folder\Origin"
$TargetPath = "C:\Test Folder\Target"
if (Test-Path $TargetPath)
Remove-Item -r $TargetPath -Force
New-Item -ItemType directory -Path $TargetPath
Copy-Item $SourcePath -Destination $TargetPath -Force -Recurse -Container
【问题讨论】:
根据您的实际操作,您可以查看 xcopy。 【参考方案1】:更新:从(至少[1])Windows 10 版本20H2
开始(我不知道Windows Server 版本和对应的构建;运行 winver.exe
以检查您的版本和构建),DeleteFile
Windows API 函数现在表现出同步行为,这隐式解决了 PowerShell 的 Remove-Item
和 .NET 的 System.IO.File.Delete
/ System.IO.Directory.Delete
的问题(但奇怪的是,不是 cmd.exe
的 rd /s
)。
这最终只是一个时机问题:在尝试删除父目录时,子目录的最后一个句柄可能尚未关闭 -这是一个基本问题,不限于打开文件资源管理器窗口:
令人难以置信的是,Windows 文件和目录删除 API 是异步的:也就是说,在函数调用返回时,不能保证删除有 尚未完成。
很遗憾,Remove-Item
没有考虑到这一点 - cmd.exe
的 rd /s
和 .NET 的 [System.IO.Directory]::Delete()
也没有 - 有关详细信息,请参阅 this answer。
这会导致间歇性的、不可预测的故障。
解决方法来自this YouTube video(从 7:35 开始),其 PowerShell 实现如下:
同步目录删除功能Remove-FileSystemItem
:
重要:
仅在Windows 上需要同步自定义实现,因为类 Unix 平台上的文件删除系统调用一开始是同步的。因此,该函数只是在类 Unix 平台上遵循Remove-Item
。在 Windows 上,自定义实现:
不会妨碍可靠移除的因素:
文件资源管理器,至少在 Windows 10 上,不锁定它显示的目录,因此它不会阻止删除。
PowerShell 也不锁定目录,因此拥有另一个当前位置是目标目录或其子目录之一的 PowerShell 窗口不会阻止删除(相比之下,cmd.exe
确实 锁定 -见下文)。
在目标目录的子树中使用FILE_SHARE_DELETE
/ [System.IO.FileShare]::Delete
(很少见)打开的文件也不会阻止删除,尽管它们确实存在于父目录中的临时名称下,直到它们的最后一个句柄是关闭。
什么会阻止删除:
如果存在权限问题(如果 ACL 阻止删除),则会中止删除。
如果遇到无限期锁定文件或目录,则中止删除。值得注意的是,这包括:
cmd.exe
(命令提示符),与 PowerShell 不同,确实锁定作为其当前目录的目录,因此如果您打开了一个 cmd.exe
窗口,其当前目录是目标目录或其子目录,删除将失败。
如果应用程序在目标目录的子树中保持打开的文件未以文件共享模式FILE_SHARE_DELETE
/[System.IO.FileShare]::Delete
(很少使用此模式)打开,则删除将失败。请注意,这仅适用于在处理其内容时保持文件打开的应用程序。 (例如,Microsoft Office 应用程序),而文本编辑器(例如记事本和 Visual Studio Code)则不保持加载打开状态。
隐藏文件和具有只读属性的文件:
这些已悄悄删除;换句话说:这个函数总是表现得像Remove-Item -Force
。
但是请注意,为了将隐藏文件/目录定位为 input,您必须将它们指定为 literal 路径,因为它们不会通过通配符表达式。
Windows 上可靠的自定义实现是以降低性能为代价的。
function Remove-FileSystemItem
<#
.SYNOPSIS
Removes files or directories reliably and synchronously.
.DESCRIPTION
Removes files and directories, ensuring reliable and synchronous
behavior across all supported platforms.
The syntax is a subset of what Remove-Item supports; notably,
-Include / -Exclude and -Force are NOT supported; -Force is implied.
As with Remove-Item, passing -Recurse is required to avoid a prompt when
deleting a non-empty directory.
IMPORTANT:
* On Unix platforms, this function is merely a wrapper for Remove-Item,
where the latter works reliably and synchronously, but on Windows a
custom implementation must be used to ensure reliable and synchronous
behavior. See https://github.com/PowerShell/PowerShell/issues/8211
* On Windows:
* The *parent directory* of a directory being removed must be
*writable* for the synchronous custom implementation to work.
* The custom implementation is also applied when deleting
directories on *network drives*.
* If an indefinitely *locked* file or directory is encountered, removal is aborted.
By contrast, files opened with FILE_SHARE_DELETE /
[System.IO.FileShare]::Delete on Windows do NOT prevent removal,
though they do live on under a temporary name in the parent directory
until the last handle to them is closed.
* Hidden files and files with the read-only attribute:
* These are *quietly removed*; in other words: this function invariably
behaves like `Remove-Item -Force`.
* Note, however, that in order to target hidden files / directories
as *input*, you must specify them as a *literal* path, because they
won't be found via a wildcard expression.
* The reliable custom implementation on Windows comes at the cost of
decreased performance.
.EXAMPLE
Remove-FileSystemItem C:\tmp -Recurse
Synchronously removes directory C:\tmp and all its content.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium', DefaultParameterSetName='Path', PositionalBinding=$false)]
param(
[Parameter(ParameterSetName='Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]] $Path
,
[Parameter(ParameterSetName='Literalpath', ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string[]] $LiteralPath
,
[switch] $Recurse
)
begin
# !! Workaround for https://github.com/PowerShell/PowerShell/issues/1759
if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Ignore) $ErrorActionPreference = 'Ignore'
$targetPath = ''
$yesToAll = $noToAll = $false
function trimTrailingPathSep([string] $itemPath)
if ($itemPath[-1] -in '\', '/')
# Trim the trailing separator, unless the path is a root path such as '/' or 'c:\'
if ($itemPath.Length -gt 1 -and $itemPath -notmatch '^[^:\\/]+:.$')
$itemPath = $itemPath.Substring(0, $itemPath.Length - 1)
$itemPath
function getTempPathOnSameVolume([string] $itemPath, [string] $tempDir)
if (-not $tempDir) $tempDir = [IO.Path]::GetDirectoryName($itemPath)
[IO.Path]::Combine($tempDir, [IO.Path]::GetRandomFileName())
function syncRemoveFile([string] $filePath, [string] $tempDir)
# Clear the ReadOnly attribute, if present.
if (($attribs = [IO.File]::GetAttributes($filePath)) -band [System.IO.FileAttributes]::ReadOnly)
[IO.File]::SetAttributes($filePath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
$tempPath = getTempPathOnSameVolume $filePath $tempDir
[IO.File]::Move($filePath, $tempPath)
[IO.File]::Delete($tempPath)
function syncRemoveDir([string] $dirPath, [switch] $recursing)
if (-not $recursing) $dirPathParent = [IO.Path]::GetDirectoryName($dirPath)
# Clear the ReadOnly attribute, if present.
# Note: [IO.File]::*Attributes() is also used for *directories*; [IO.Directory] doesn't have attribute-related methods.
if (($attribs = [IO.File]::GetAttributes($dirPath)) -band [System.IO.FileAttributes]::ReadOnly)
[IO.File]::SetAttributes($dirPath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
# Remove all children synchronously.
$isFirstChild = $true
foreach ($item in [IO.directory]::EnumerateFileSystemEntries($dirPath))
if (-not $recursing -and -not $Recurse -and $isFirstChild) # If -Recurse wasn't specified, prompt for nonempty dirs.
$isFirstChild = $false
# Note: If -Confirm was also passed, this prompt is displayed *in addition*, after the standard $PSCmdlet.ShouldProcess() prompt.
# While Remove-Item also prompts twice in this scenario, it shows the has-children prompt *first*.
if (-not $PSCmdlet.ShouldContinue("The item at '$dirPath' has children and the -Recurse switch was not specified. If you continue, all children will be removed with the item. Are you sure you want to continue?", 'Confirm', ([ref] $yesToAll), ([ref] $noToAll))) return
$itemPath = [IO.Path]::Combine($dirPath, $item)
([ref] $targetPath).Value = $itemPath
if ([IO.Directory]::Exists($itemPath))
syncremoveDir $itemPath -recursing
else
syncremoveFile $itemPath $dirPathParent
# Finally, remove the directory itself synchronously.
([ref] $targetPath).Value = $dirPath
$tempPath = getTempPathOnSameVolume $dirPath $dirPathParent
[IO.Directory]::Move($dirPath, $tempPath)
[IO.Directory]::Delete($tempPath)
process
$isLiteral = $PSCmdlet.ParameterSetName -eq 'LiteralPath'
if ($env:OS -ne 'Windows_NT') # Unix: simply pass through to Remove-Item, which on Unix works reliably and synchronously
Remove-Item @PSBoundParameters
else # Windows: use synchronous custom implementation
foreach ($rawPath in ($Path, $LiteralPath)[$isLiteral])
# Resolve the paths to full, filesystem-native paths.
try
# !! Convert-Path does find hidden items via *literal* paths, but not via *wildcards* - and it has no -Force switch (yet)
# !! See https://github.com/PowerShell/PowerShell/issues/6501
$resolvedPaths = if ($isLiteral) Convert-Path -ErrorAction Stop -LiteralPath $rawPath else Convert-Path -ErrorAction Stop -path $rawPath
catch
Write-Error $_ # relay error, but in the name of this function
continue
try
$isDir = $false
foreach ($resolvedPath in $resolvedPaths)
# -WhatIf and -Confirm support.
if (-not $PSCmdlet.ShouldProcess($resolvedPath)) continue
if ($isDir = [IO.Directory]::Exists($resolvedPath)) # dir.
# !! A trailing '\' or '/' causes directory removal to fail ("in use"), so we trim it first.
syncRemoveDir (trimTrailingPathSep $resolvedPath)
elseif ([IO.File]::Exists($resolvedPath)) # file
syncRemoveFile $resolvedPath
else
Throw "Not a file-system path or no longer extant: $resolvedPath"
catch
if ($isDir)
$exc = $_.Exception
if ($exc.InnerException) $exc = $exc.InnerException
if ($targetPath -eq $resolvedPath)
Write-Error "Removal of directory '$resolvedPath' failed: $exc"
else
Write-Error "Removal of directory '$resolvedPath' failed, because its content could not be (fully) removed: $targetPath`: $exc"
else
Write-Error $_ # relay error, but in the name of this function
continue
[1] 我已亲自验证问题已在版本20H2
中得到解决,通过在GitHub issue #27958 中运行测试数小时而没有失败; this answer 建议问题早在版本 1909
就已解决,从构建 18363.657
开始,但 Dinh Tran 发现问题在构建 18363.1316
时未已解决大型目录树,例如node_modules
。我找不到有关该主题的任何官方信息。
【讨论】:
我已经更新到 Windows 20H2,这个问题实际上已经消失了。成功删除了node_moodules
,没有额外的解决方法。现在我只需要等待窗口服务器更新:)以上是关于无法删除项目,目录不为空的主要内容,如果未能解决你的问题,请参考以下文章