无法删除项目,目录不为空 [英] Cannot remove item, The Directory is not empty

查看:87
本文介绍了无法删除项目,目录不为空的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

When using the Remove-Item command, even utilizing the -r and -Force parameters, sometimes the following error message is returned:

删除项目:无法删除项目 C:\Test Folder\Test Folder\Target:目录不为空.

Remove-Item : Cannot remove item C:\Test Folder\Test Folder\Target: The directory is not empty.

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

Particularly, this happens when the directory to be removed is opened in Windows Explorer.

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

Now, while it is possible to avoid this simply by having Windows Explorer closed or not browsing that location, I work my scripts in a multi-user environment where people sometimes just forget to close Windows Explorer windows, I am interested in a solution for deleting entire folders and directories even if they are opened in Windows Explorer.

是否有比 -Force 更强大的选项可以实现这一目标?

Is there an option more powerful than -Force that I can set to achieve this?

要可靠地重现这一点,请创建文件夹 C:\Test Folder\Origin 并在其中填充一些文件和子文件夹(重要),然后使用以下脚本或类似脚本并执行一次.现在打开 C:\Test Folder\Target 的子文件夹之一(在我的例子中,我使用了 C:\Test Folder\Target\Another Subfolder 包含 A第三个文件.txt),然后再次尝试运行脚本.您现在将收到错误消息.如果您第三次运行该脚本,您将不会再次出现错误(根据我尚未确定的情况,该错误有时会出现第二次,然后再也不会出现,而在其他时候它会出现 每隔次).

To reliably reproduce this, create the folder C:\Test Folder\Origin and populate it with some files and subfolders (important), then take the following script or one like it and execute it once. Now open one of the subfolders of C:\Test Folder\Target (in my case, I used C:\Test Folder\Target\Another Subfolder containing A third file.txt), and try running the script again. You will now get the error. If you run the script a third time, you will not get the error again (depending on circumstances that I have yet to determine, though, the error sometimes occurs the second time and then never again, and at other times it occurs every second time).

$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 

推荐答案

更新:从(至少 [1])Windows 10 版本开始20H2(我不知道对应的 Windows Server 版本和构建;运行 winver.exe 以检查您的版本并构建),DeleteFile Windows API 函数现在表现出同步行为,它隐式地解决了 PowerShell 的 Remove-Item 和 .NET 的 System.IO.File.Delete/System.IO.Directory.Delete(但奇怪的是,不是cmd.execode> 的 rd/s).

Update: Starting with (at least [1]) Windows 10 version 20H2 (I don't know that Windows Server version and build that corresponds to; run winver.exe to check your version and build), the DeleteFile Windows API function now exhibits synchronous behavior, which implicitly solves the problems with PowerShell's Remove-Item and .NET's System.IO.File.Delete / System.IO.Directory.Delete (but, curiously, not with cmd.exe's rd /s).

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

This is ultimately only a timing issue: the last handle to a subdirectory may not be closed yet at the time an attempt is made to the delete the parent directory - and this is a fundamental problem, not restricted to having File Explorer windows open:

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

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

Regrettably, Remove-Item fails to account for that - and neither do cmd.exe's rd /s and .NET's [System.IO.Directory]::Delete() - see this answer for details. This results in intermittent, unpredictable failures.

解决方法来自此 YouTube 视频(7:35 开始),PowerShell 实现如下:

The workaround comes courtesy of in this YouTube video (starts at 7:35), a PowerShell implementation of which is below:

同步目录删除功能Remove-FileSystemItem:

重要提示:

  • 同步自定义实现仅在 Windows 上是必需的,因为类 Unix 平台上的文件删除系统调用一开始就是同步的.因此,该函数在类 Unix 平台上简单地遵循 Remove-Item.在 Windows 上,自定义实现:

  • The synchronous custom implementation is only required on Windows, because the file-removal system calls on Unix-like platforms are synchronous to begin with. Therefore, the function simply defers to Remove-Item on Unix-like platforms. On Windows, the custom implementation:

  • 要求被删除目录的目录可写,同步自定义实现才能工作.
  • 也适用于删除任何网络驱动器上的目录.
  • requires that the parent directory of a directory being removed be writable for the synchronous custom implementation to work.
  • is also applied when deleting directories on any network drives.

什么不会阻止可靠的删除:

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

  • File Explorer, at least on Windows 10, does not lock directories it displays, so it won't prevent removal.

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

PowerShell doesn't lock directories either, so having another PowerShell window whose current location is the target directory or one of its subdirectories won't prevent removal (by contrast, cmd.exe does lock - see below).

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

Files opened with FILE_SHARE_DELETE / [System.IO.FileShare]::Delete (which is rare) in the target directory's subtree also won't prevent removal, though they do live on under a temporary name in the parent directory until the last handle to them is closed.

什么会阻止删除:

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

  • If there's a permissions problem (if ACLs prevent removal), removal is aborted.

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

If an indefinitely locked file or directory is encountered, removal is aborted. Notably, that includes:

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

  • cmd.exe (Command Prompt), unlike PowerShell, does lock the directory that is its current directory, so if you have a cmd.exe window open whose current directory is the target directory or one of its subdirectories, removal will fail.

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

If an application keeps a file open in the target directory's subtree that was not opened with file-sharing mode FILE_SHARE_DELETE / [System.IO.FileShare]::Delete (using this mode is rare), removal will fail. Note that this only applies to applications that keep files open while working with their content. (e.g., Microsoft Office applications), whereas text editors such as Notepad and Visual Studio Code, by contrast, do not keep they've loaded open.

隐藏文件和只读属性文件:

Hidden files and files with the read-only attribute:

  • 这些被悄悄删除;换句话说:这个函数总是表现得像Remove-Item -Force.
  • 但是请注意,为了将隐藏文件/目录定位为输入,您必须将它们指定为文字路径,因为它们不会通过通配符表达式.
  • 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 literal paths, because they won't be found via a wildcard expression.

在 Windows 上可靠的自定义实现是以降低性能为代价的.

The reliable custom implementation on Windows comes at the cost of decreased performance.

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] 通过在 GitHub 问题 #27958 几个小时没有失败;这个答案表明该问题早在版本 1909 中就已解决,从 build 开始>18363.657,但Dinh Tran 发现问题解决从构建 18363.1316 开始,删除大型目录树(例如 node_modules).我找不到有关该主题的任何官方信息.


[1] I've personally verified that the issue is resolved in version 20H2, by running the tests in GitHub issue #27958 for hours without failure; this answer suggests that the problem was resolved as early as version 1909, starting with build 18363.657, but Dinh Tran finds that the issue is not resolved as of build 18363.1316 when removing large directory trees such as node_modules. I couldn't find any official information on the subject.

这篇关于无法删除项目,目录不为空的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆