无法删除项目,目录不为空

Cannot remove item, The Directory is not empty

提问人:Kira Resari 提问时间:11/30/2018 最后编辑:Kira Resari 更新时间:6/22/2022 访问量:12535

问:

使用该命令时,即使使用 and 参数,有时也会返回以下错误消息:Remove-Item-r-Force

Remove-Item:无法删除项目 C:\Test Folder\Test Folder\Target:目录不为空。

特别是,当在 Windows 资源管理器中打开要删除的目录时,会发生这种情况。

现在,虽然可以通过关闭 Windows 资源管理器或不浏览该位置来避免这种情况,但我在多用户环境中工作我的脚本,人们有时会忘记关闭 Windows 资源管理器窗口,我对删除整个文件夹和目录的解决方案感兴趣,即使它们在 Windows 资源管理器中打开。

有没有比我设置的更强大的选项来实现这一点?-Force

为了可靠地重现此内容,请创建文件夹并用一些文件和子文件夹(重要)填充它,然后采用以下脚本或类似的脚本并执行一次。现在打开其中一个子文件夹(在我的情况下,我使用了包含),然后再次尝试运行脚本。您现在将收到错误。如果第三次运行脚本,则不会再次出现错误(但是,根据我尚未确定的情况,错误有时会第二次发生,然后永远不会再次发生,而在其他时候,它每隔两次发生一次)。C:\Test Folder\OriginC:\Test Folder\TargetC:\Test Folder\Target\Another SubfolderA third file.txt

$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 
Windows PowerShell

评论

0赞 Seth 11/30/2018
根据您实际正在做的事情,您可以查看 xcopy。

答:

32赞 mklement0 12/1/2018 #1

更新从(至少 [1])Windows 10 版本 20H2 开始(我不知道对应的 Windows Server 版本和内部版本;运行以检查您的版本和内部版本),DeleteFile Windows API 函数现在在受支持的文件系统(包括 NTFS)上表现出同步行为,这隐式解决了 PowerShell 的 Remove-Item 和 .NET 的 System.IO.File.Delete / System.IO.Directory.Delete(但奇怪的是,没有 cmd.exerd /s)。winver.exe


最终只是一个时问题:在尝试删除父目录时,子目录的最后一个句柄可能尚未关闭 - 这是一个基本问题,不仅限于打开文件资源管理器窗口:

令人难以置信的是,Windows 文件和目录删除 API 是异步的:也就是说,当函数调用返回时,不能保证删除已完成

遗憾的是,Remove-Item 没有考虑到这一点 - 's 和 .NET - 有关详细信息,请参阅此答案。 这会导致间歇性、不可预测的故障。cmd.exerd /s[System.IO.Directory]::Delete()

YouTube 视频(从 7:35 开始)提供了解决方法,其 PowerShell 实现如下:


同步目录删除函数 Remove-FileSystemItem

重要:

  • 同步自定义实现仅在 Windows 上是必需的,因为类 Unix 平台上的文件删除系统调用首先是同步的。因此,该函数只是服从于类 Unix 平台。在 Windows 上,自定义实现:Remove-Item

    • 要求要删除的目录的目录是可写的,同步自定义实现才能正常工作。
    • 在删除任何网络驱动器上的目录时也应用。
  • 什么不会阻止可靠的移除:

    • 至少在 Windows 10 上,文件资源管理器不会锁定它显示的目录,因此它不会阻止删除。

    • PowerShell 也不会锁定目录,因此拥有另一个当前位置为目标目录或其子目录之一的 PowerShell 窗口不会阻止删除(相比之下,锁定 - 见下文)。cmd.exe

    • 在目标目录的子树中用 /(这种情况很少见)打开的文件也不会阻止删除,尽管它们确实在父目录中以临时名称存在,直到它们的最后一个句柄被关闭。FILE_SHARE_DELETE[System.IO.FileShare]::Delete

  • 什么会阻止移除

    • 如果存在权限问题(如果 ACL 阻止删除),则删除将中止。

    • 如果遇到无限期锁定的文件或目录,则删除将中止。值得注意的是,这包括:

      • cmd.exe与 PowerShell 不同,(命令提示符)锁定其当前目录的目录,因此,如果打开的窗口当前目录是目标目录或其子目录之一,则删除将失败cmd.exe

      • 如果应用程序在目标目录的子树中保持打开状态,而该文件使用文件共享模式 / 打开(很少使用此模式),则删除将失败。请注意,这仅适用于在处理文件内容时保持文件打开状态的应用程序。(例如,Microsoft Office应用程序),而文本编辑器(如记事本和Visual Studio Code)则不会保持它们已加载打开状态。FILE_SHARE_DELETE[System.IO.FileShare]::Delete

  • 隐藏文件和具有只读属性的文件:

    • 这些被悄悄地删除了;换句话说:此函数的行为总是类似于 Remove-Item -Force
    • 但请注意,为了将隐藏文件/目录作为输入,您必须将它们指定为文本路径,因为无法通过通配符表达式找到它们。
  • 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 问题 #27958 中运行测试数小时而没有失败;此答案表明,该问题早在版本 1909 中就已解决,从内部版本 18363.657 开始,但 Dinh Tran 发现,在删除大型目录树(如 node_modules)时,该问题在内部版本 18363.1316得到解决。我找不到任何关于这个主题的官方信息。

评论

1赞 Dinh Tran 1/26/2021
我已经更新到 Windows 20H2,这个问题实际上已经永远消失了。成功删除了,没有额外的解决方法。现在我只需要等待窗口服务器更新:)node_moodules