.NETでディレクトリを消すのがこんなに面倒なわけがない

# 目的
PowerShellでファイルシステムのディレクトリを削除する方法について、よくある例と、それを行うとうまくいかない、場合によっては破壊的な動作をする例を説明します。

なお、最終的に.NETの問題にぶつかるので、PowerShellからC#使えばいいじゃない教の人もハマります。


# よくある例
以下のようなコマンドでファイルシステムにテストデータを作成します。

```powershell
New-Item -Path ./testdir  -Type Directory -Force
New-Item -Path ./testdir/test -Type Directory -Force
New-Item -Path ./testdir/test -Name 1.txt -Type File -Value 'Test1'
New-Item -Path ./testdir/test -Name 2.txt -Type File -Value 'Test2'
New-Item -Path ./testdir/test/sub1 -Type Directory -Force
New-Item -Path ./testdir/test/sub1 -Name s1.txt -Type File -Value 'Sub-Test1'


```

以下のようなディレクトリが作成されます。

```test
TESTDIR
└─test
    │  1.txt
    │  2.txt
    │
    └─sub1
            s1.txt
```

この時にtestフォルダを削除するにはどうすればいいでしょうか？
よくある説明では下記のコマンドを入力すると行えるとあります。

```powershell
Remove-Item -LiteralPath ./testdir/test -Recurse -Force
```


# 破壊的に動作する例
先の例は、多くの場合、正しく動きますが、特定の条件下でユーザが意図しないような**破壊的**動作をします。

まずファイルシステムの場合、シンボリックリンクやジャンクションという種類が存在しています。

**Windowsのシンボリックリンクとジャンクションとハードリンクの違い**
https://www.atmarkit.co.jp/ait/articles/1306/07/news111.html

これらの機能は簡単にいうとディレクトリへのリンクを作成することができる機能になっています。
これらを実際に作成するには下記のスクリプトを管理者権限で実行してください。


```powershell
New-Item -Path ./testdir  -Type Directory -Force

New-Item -Path ./testdir/src1 -Type Directory -Force
New-Item -Path ./testdir/src1 -Name src1.txt -Type File -Value 'src1'

New-Item -Path ./testdir/src2 -Type Directory -Force
New-Item -Path ./testdir/src2 -Name src2.txt -Type File -Value 'src2'

# シンボリックリンクの作成には管理者権限が必要
New-Item -Path ./testdir/test -Type Directory -Force
New-Item -Path ./testdir/test -Name 1.txt -Type File -Value 'Test1'
New-Item -Value './testdir/src1' -Path './testdir/test/symbolic_src1'  -ItemType SymbolicLink
New-Item -Value './testdir/src2' -Path './testdir/test/junction_src2'  -ItemType Junction

# PowerShellのNew-Itemでサポートしていないバージョンの場合
#cmd /c "mklink /d ./testdir/test/symbolic_src1 c:\share\testdir\src1"
#cmd /c "mklink /j ./testdir/test/junction_src2 ./testdir/src2"

```

以下のようなディレクトリになります。

```text
C:\DEV\PS\FILE\TESTDIR
├─src1
│      src1.txt
│
├─src2
│      src2.txt
│
└─test
    │  1.txt
    │
    ├─junction_src2
    │      src2.txt
    │
    └─symbolic_src1
            src1.txt
```

ジャンクションやシンボリックリンクを用いることでディレクトリへのリンクが作成されていることが確認できます。

ではsrc1フォルダとsrc2フォルダを残してtestフォルダのみを削除します。

```powershell
Remove-Item -LiteralPath ./testdir/test -Recurse -Force
```

この結果はPowerShellのバージョンによって異なります。

##PowerShell 2.0の場合

```text
C:\SHARE\TESTDIR
├─src1
└─src2
```

testディレクトリは削除されましたが、リンク先のファイルが全て削除されてしまいます。
これがユーザーの意図しない破壊的結果になります。

この挙動は以下で修正されています。
https://github.com/PowerShell/PowerShell/issues/621

##Window10+PowerShell 5.1の場合
Windows10+PowerShell 5.1の場合以下のようになります。


```powershell
Remove-Item : 要求で指定したタグと再解析ポイントにあるタグが一致しません。
発生場所 行:1 文字:1
+ Remove-Item  -LiteralPath ./testdir/test -Recurse -Force
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Remove-Item], Win32Exception
    + FullyQualifiedErrorId : System.ComponentModel.Win32Exception,Microsoft.PowerShell.Commands.RemoveItemCommand

フォルダの内容：
C:\DEV\PS\FILE\TESTDIR
├─src1
│      src1.txt
│
├─src2
│      src2.txt
│
└─test
    │  1.txt
    │
    └─symbolic_src1
            src1.txt
```

ジャンクションは正しく削除されましたが、シンボリックリンクは残ってしまいます。
シンボリックリンクが消えない事象は、以下のコメントと同様の事象と思われます。
https://github.com/PowerShell/PowerShell/issues/621#issuecomment-515729308


# プランBの削除方法
よく上記の話が出た際にプランBとして紹介される削除方法は以下になります。

```powershell
(Get-Item -LiteralPath "./testdir/test").Delete($True)

[System.IO.Directory]::Delete("c:\dev\ps\file\testdir\test", $True)
```

これは２つの問題を引き起こします。
１つ目は読み取り専用のファイルが混在していた場合に削除できません。
２つ目はジャンクションを含むサブディレクトリを削除した場合、エラーになります。

先に挙げたジャンクションを含むディレクトリを削除した場合、以下のようになります。


```
PS C:\dev\ps\file> (gi "./testdir/test").Delete($true)
"1" 個の引数を指定して "Delete" を呼び出し中に例外が発生しました: "パラメーターが間違っています。
"
発生場所 行:1 文字:1
+ (Get-Item -LiteralPath "./testdir/test").Delete($True)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : IOException
```

```
PS C:\dev\ps\file> [System.IO.Directory]::Delete("c:\dev\ps\file\testdir\test", $True)
"2" 個の引数を指定して "Delete" を呼び出し中に例外が発生しました: "パラメーターが間違っています。
"
発生場所 行:1 文字:1
+ [System.IO.Directory]::Delete("c:\dev\ps\file\testdir\test", $True)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : IOException

```

このときのフォルダ構成は以下のようになっており、ジャンクションを含むディレクトリ以外は削除されています。

```
C:\DEV\PS\FILE\TESTDIR
├─src1
│      src1.txt
│
├─src2
│      src2.txt
│
└─test
```

このため、もう一度、再実行すると正常に削除されます。

この事象は、PowerShell固有の問題でなく、C#で実装した場合も発生します。
例えば、C#で以下のような実装で削除した場合も同様の事象になります。

```c#
System.IO.Directory.Delete(@"c:\dev\ps\file\testdir\test", true);
```

```
{"パス 'junction_src2' へのアクセスが拒否されました。"} System.UnauthorizedAccessException
```

**VisualStudio2019の下記にて検証**
 ・.NET Framework 4.7.2 + Win10
 ・.NET Core 2.1 + Win10.
※ＴＯＤＯ：Linuxで動かした場合はそのうち。


なぜこの事象が発生するかの理由は、わかりませんでしたが、おそらく、以下と類似の問題だと思います。

**Cannot delete directory with Directory.Delete(path, true)**
https://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true


ジャンクションポイントが削除されきっていない状態で親フォルダを削除しようとしてエラーになっているのではないかと思いますが、正直わかりません。

# どうやって消すべきか

## PowerShellや.NETにはたよらない。
昔ながらのcmd.exeにやらせる。

```powershell:PowerShellの例
cmd /c "rmdir /s/q `"c:\dev\ps\file\testdir\test`""
```

```c#:C#の実装例
Process.Start("cmd.exe", "/c " + @"rmdir /s/q c:\dev\ps\file\testdir\test");
```


## 2回やってしまう
PowerShellの場合、以下のようにSystem.IO.DirectoryInfo.Deleteで消せなかった残りの項目をRemove-Itemを強制＋再帰的に呼び出してしまう方法もあります。

```powershell
try { (Get-Item -LiteralPath "./testdir/test").Delete($True) } catch { Remove-Item ./testdir/test -Recurse -Force  }
```

## 再帰呼び出しをしてうまいことやる
FileAttributes.ReparsePointを判定してシンボリックリンクやジャンクションの場合は再帰を行わず、該当のディレクトリだけを削除します。
それ以外の場合は読み取り属性を外してファイルを削除後、各ディレクトリについて再起呼び出しをします。
**※よくあるサンプルだとFileAttributes.ReparsePointの判定をしていないので、リンク先の属性を変更してしまっているサンプルがあります。**


```csharp
        // using System.IO;
        // Delete
        public static void DeleteDirectoryNest(string target_dir)
        {
            DirectoryInfo di = new DirectoryInfo(target_dir);
            if ((di.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint)
            {
                Directory.Delete(target_dir, false);
                return;
            }

            string[] files = Directory.GetFiles(target_dir);
            string[] dirs = Directory.GetDirectories(target_dir);

            foreach (string file in files)
            {
                File.SetAttributes(file, FileAttributes.Normal);
                File.Delete(file);
            }

            foreach (string dir in dirs)
            {
                DeleteDirectoryNest(dir);
            }

            Directory.Delete(target_dir, false);
        }

```

## SHFileOperationを利用する
SHFileOperationを利用してエクスプローラーから削除したものと同じ動作を行う方法もあります。
この場合、削除中の進捗プログレスなどが表示されます。
ファイルロック中等でエラーなどが表示された場合に以下のダイアログが表示されて処理が止まるので**バッチ処理には向いていません**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47856/a44bbffd-87f5-064f-92ea-b2b36c9ec618.png)


```csharp
        // TODO :using System.Runtime.InteropServices;
        private const int FO_DELETE = 0x0003;
        private const int FOF_ALLOWUNDO = 0x0040;           // Preserve undo information, if possible. 
        private const int FOF_NOCONFIRMATION = 0x0010;      // Show no confirmation dialog box to the user

        // Struct which contains information that the SHFileOperation function uses to perform file operations. 
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        public struct SHFILEOPSTRUCT
        {
            public IntPtr hwnd;
            [MarshalAs(UnmanagedType.U4)]
            public int wFunc;
            public string pFrom;
            public string pTo;
            public short fFlags;
            [MarshalAs(UnmanagedType.Bool)]
            public bool fAnyOperationsAborted;
            public IntPtr hNameMappings;
            public string lpszProgressTitle;
        }

        [DllImport("shell32.dll", CharSet = CharSet.Auto)]
        static extern int SHFileOperation(ref SHFILEOPSTRUCT FileOp);

        public static void DeleteFileOrFolder(string path)
        {
            SHFILEOPSTRUCT fileop = new SHFILEOPSTRUCT();
            fileop.wFunc = FO_DELETE;
            fileop.pFrom = path + '\0' + '\0';
            //fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION;
            fileop.fFlags =  FOF_NOCONFIRMATION;
            SHFileOperation(ref fileop);
        }
```

これと同様のことはMicrosoft.VisualBasicを参照後以下のコードでも実現可能です。

```csharp
            Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(
                @"c:\dev\ps\file\testdir\test",
                Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
                Microsoft.VisualBasic.FileIO.RecycleOption.DeletePermanently);
```

# まとめ
シンボリックリンクやジャンクションを含むディレクトリの削除について記載しました。
C#のディレクトリ削除やPowerShellのディレクトリ削除でグーグル検索した場合の上位に出てくるものは、この辺りを考慮していないので注意してください。

# 参考：
http://m-hiyama.hatenablog.com/entry/20150914/1442195413
https://kristofmattei.be/2012/12/15/powershell-remove-item-and-symbolic-links/
https://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true
https://www.fluxbytes.com/csharp/delete-files-or-folders-to-recycle-bin-in-c/