Windows 无法删除项,目录不是空的

Windows 无法删除项,目录不是空的,windows,powershell,Windows,Powershell,使用Remove Item命令时,即使使用-r和-Force参数,有时也会返回以下错误消息: 删除项:无法删除项C:\Test Folder\Test Folder\Target:目录不为空 尤其是在Windows资源管理器中打开要删除的目录时,会发生这种情况 现在,虽然可以通过关闭Windows资源管理器或不浏览该位置来避免这种情况,但我在多用户环境中使用脚本,人们有时会忘记关闭Windows资源管理器窗口,我对删除整个文件夹和目录的解决方案感兴趣,即使它们是在Windows资源管理器中打开的

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

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

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

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

有没有比
-Force
更强大的选项可以实现这一点

要可靠地重现此结果,请创建文件夹
C:\Test folder\Origin
,并用一些文件和子文件夹填充它(很重要),然后使用以下脚本或类似脚本执行一次。现在打开
C:\Test Folder\Target
的其中一个子文件夹(在我的例子中,我使用了
C:\Test Folder\Target\另一个子文件夹,其中包含
第三个文件.txt
),然后再次尝试运行脚本。现在您将得到错误。如果第三次运行脚本,则不会再次出现错误(不过,这取决于我尚未确定的情况,错误有时会第二次出现,然后再也不会出现,有时每秒钟都会出现一次)

更新:从(至少[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是异步的:也就是说,当函数调用返回时,还不能保证删除已经完成

遗憾的是,
删除项
无法解释这一点,
cmd.exe
rd/s
和.NET的
[System.IO.Directory]::Delete()
-有关详细信息,请参阅。 这会导致间歇性、不可预测的故障。

解决方案来自in(7:35开始),其PowerShell实现如下:


同步目录删除功能
删除文件系统emitem

重要提示:

  • 只有在Windows上才需要同步自定义实现,因为类Unix平台上的文件删除系统调用从一开始就是同步的。因此,在类Unix平台上,该函数只需执行
    删除项
    。在Windows上,自定义实现:

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

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

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

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

  • 什么会阻止删除

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

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

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

      • 如果应用程序在目标目录的子树中保持未使用文件共享模式打开的文件处于打开状态,则删除操作将失败。请注意,这仅适用于在处理文件内容时保持文件打开的应用程序。(例如,Microsoft Office应用程序),而文本编辑器(如记事本和Visual Studio代码)则没有保持其已加载的打开状态

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

    • 这些被悄悄地移除;换句话说:此函数的行为总是类似于
      删除项目-Force
    • 但是,请注意,为了将隐藏文件/目录作为输入,必须将它们指定为文本路径,因为找不到它们
      $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 
      
      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
                }
              }
            }
          }
      }