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

标签 windows powershell

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

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



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

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

我可以设置比-Force更强大的选项来实现此目的吗?

为了可靠地重现此代码,请创建文件夹C:\Test Folder\Origin并在其中填充一些文件和子文件夹(重要),然后采用以下脚本或类似脚本并执行一次。现在打开C:\Test Folder\Target的子文件夹之一(在我的例子中,我使用了包含C:\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 

最佳答案

更新:从(至少为[1])Windows 10版本20H2 (我不知道Windows Server的版本和与之相对应的版本;运行winver.exe来检查您的版本和内部版本),现在显示DeleteFile Windows API函数同步行为隐式解决了PowerShell的Remove-Item和.NET的System.IO.File.Delete/System.IO.Directory.Delete的问题(但奇怪的是,不是cmd.exerd /s)。

这是,最终仅是一个计时问题:尝试删除父目录时,子目录的最后一个句柄可能尚未关闭-这是的基本问题,不限于具有文件资源管理器 window 打开:
令人难以置信的是,Windows文件和目录删除API的是异步的:即,在函数调用返回时,不能保证删除已完成。
遗憾的是, Remove-Item无法解释该-且cmd.exerd /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阻止删除),则删除将中止。
  • 如果遇到无限期锁定的文件或目录,则中止删除。值得注意的是,这包括:

    不同于PowerShell,
  • cmd.exe(命令提示符)确实锁定了当前目录,因此,如果打开了cmd.exe窗口,其当前目录是目标目录或其子目录之一,则删除将失败。
  • 如果应用程序在文件共享模式FILE_SHARE_DELETE/[System.IO.FileShare]::Delete(很少使用此模式)下未打开的目标目录的子树中保持文件打开,则删除将失败。请注意,这仅适用于在处理文件内容时保持打开状态的应用程序。 (例如Microsoft Office应用程序),而文本编辑器(例如记事本和Visual Studio Code)则不会保持打开状态。


  • 隐藏的文件和具有只读属性的文件:
  • 这些被悄悄地删除了。换句话说:该函数的行为始终类似于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]我已经通过在GitHub issue #27958中运行测试数小时而没有失败,亲自验证了20H2版本中的问题已解决; this answer建议早于1909版本就解决了该问题,从build 18363.657开始,但是Dinh Tran发现,当删除诸如18363.1316之类的大型目录树时,该问题从node_modules构建开始还没有解决。我找不到有关此主题的任何官方信息。

    关于windows - 无法删除项目,目录不为空,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53553729/

    相关文章:

    c# - 使用 Windows 传真发送和接收传真

    html - 电源外壳 : split HTML

    python - 为 Windows 7 编译 IP2Location Python 扩展

    c# - Windows 8.1 平板电脑应用程序开发 - 从哪里开始?

    电源外壳 : How to capture powershell output

    powershell - 2goarray 适用于 cmd 但不适用于 powershell

    android - 多个网络 - React Native 在安装 Virtualbox/Vmware 时选择了错误的 ip

    c# - 从 C# 调用 COM 对象

    powershell - 修改值时的Powershell选择对象和更改属性名称

    powershell - 如何将对象列表输出到带有分隔符的单行