C#(csc.exe)だけで導入できるフォルダ構成比較ツール・・・つくりかけ【自分用】

色々とインストールができないお堅い環境にいて、フォルダ比較すらままならないので、つくってみた。
Windowsでcsc.exeさえあれば導入可能。(Windows7以降なら入っている)

# 画面キャプチャ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/3bb82b4a-2926-9a5b-8dba-8dab9a1e7b7e.png)

## インプット

※各txtファイルは0 byte

```text

├─test
│  │  bbb.txt
│  │
│  ├─foo3
│  │      ccc.txt
│  │
│  └─hoge2
│      │  ddd.txt
│      │
│      └─xxxyyy
│              ff.txt
│              ggg.txt
│
└─testxx
    │  aaa.txt
    │  bbb.txt
    │
    ├─hoge
    └─hoge2
        │  ddd.txt
        │
        └─xxxyyy
                e.txt
                ff.txt
```

# ソースコード

```csharp:MyDiff.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

class MyDiff : Form
{
    SplitContainer spl1;
    SplitContainer spl2;
    TextBox txtPathL;
    TextBox txtPathR;
    ListView lsvFiles;

    string _leftPath;
    string _rightPath;

    enum DiffType {
        Unknown, Same, Different, LOnly, ROnly,
    };

    MyDiff( string path1, string path2 )
    {
        Text = "Diff tool";
        ClientSize = new Size(600, 300);

        try {
            if ( path1 != null ) {
                path1 = Path.GetFullPath(path1);
            }
            if ( path2 != null ) {
                path2 = Path.GetFullPath(path2);
            }
        }
        catch(ArgumentException e                ){ Console.WriteLine(e); path1=null; path2=null; }
        catch(System.Security.SecurityException e){ Console.WriteLine(e); path1=null; path2=null; }
        catch(NotSupportedException e            ){ Console.WriteLine(e); path1=null; path2=null; }
        catch(PathTooLongException e             ){ Console.WriteLine(e); path1=null; path2=null; }
        _leftPath = path1;
        _rightPath = path2;
        

        Controls.Add(spl1 = new SplitContainer(){
            Dock = DockStyle.Fill,
            Orientation = Orientation.Horizontal, // 横線で上下に分割
            // SplitterDistance = 250,                // 分割線の位置を指定
            BorderStyle = BorderStyle.FixedSingle,
            SplitterWidth = 2,
            FixedPanel = FixedPanel.Panel1, // spl1が属すコントロールがリサイズされたときに、Panel1側のサイズ（分割線）を保持（固定）する
        });

        spl1.Panel2.Controls.Add(spl2 = new SplitContainer(){
            Dock = DockStyle.Fill,
            Orientation = Orientation.Vertical, // 縦線で左右に分割
            // SplitterDistance = 100,// 分割線の位置を指定
            BorderStyle = BorderStyle.Fixed3D,
            SplitterWidth = 2,
            IsSplitterFixed = true,
        });

        spl1.Panel1.Controls.AddRange(
            new Control[]{
                txtPathL = new TextBox(){
                    Location = new Point(0, 0),
                    ReadOnly = true,
                    AllowDrop = true,
                },
                txtPathR = new TextBox(){
                    Location = new Point(0, 25),
                    ReadOnly = true,
                    AllowDrop = true,
                },
                lsvFiles = new ListView(){
                    Location = new Point(0, 50),
                    //Dock = DockStyle.Fill,
                    View = View.Details,
                    FullRowSelect = true,
                    HideSelection = false,
                    MultiSelect = false,
                    GridLines = true,
                },
            }
        );
        txtPathL.Text = _leftPath ??"";
        txtPathR.Text = _rightPath??"";

        txtPathL.DragEnter += TxtPathX_DragEnter;
        txtPathL.DragDrop  += TxtPathX_DragDrop;
        txtPathR.DragEnter += TxtPathX_DragEnter;
        txtPathR.DragDrop  += TxtPathX_DragDrop;

        // 列ヘッダを登録
        lsvFiles.Columns.Add("Path"     , 170, HorizontalAlignment.Left);
        lsvFiles.Columns.Add("FileName" , 170, HorizontalAlignment.Left);
        lsvFiles.Columns.Add("Ext"      ,  40, HorizontalAlignment.Left); // extension of file
        lsvFiles.Columns.Add("Diff"     ,  40, HorizontalAlignment.Left); // == <> o- -o   (same, not same, left only, right only)
        lsvFiles.Columns.Add("Size"     ,  70, HorizontalAlignment.Right); // file size
        lsvFiles.Columns.Add("LastModif",  70, HorizontalAlignment.Left); // last modified time stamp

        Load += (s,e)=>{
            spl1.SplitterDistance = 150; // 分割線の位置を指定
            spl2.SplitterDistance = 200; // 分割線の位置を指定 
        };
        Load += (s,e)=>{MyResize();};
        Resize += (s,e)=>{MyResize();};
        ResizeEnd += (s,e)=>{MyResize();};
        spl1.SplitterMoved += (s,e)=>{MyResize();};

        LoadPathesAndDiffToListViewWithCheck();
    }

    void MyResize()
    {
        int w = spl1.Panel1.ClientSize.Width;
        int tmpH = txtPathL.Height;
        int h = spl1.Panel1.ClientSize.Height - tmpH*2;
        if (h<50){h=50;}
        txtPathL.Width = w;
        txtPathR.Top = tmpH;
        txtPathR.Width = w;
        lsvFiles.Size = new Size(w, h);
        lsvFiles.Top  = tmpH*2;
    }


	void TxtPathX_DragEnter(object sender, DragEventArgs e)
	{
		if ( e.Data.GetDataPresent(DataFormats.FileDrop) ) {
			e.Effect = DragDropEffects.Copy;
		}
		else {
			e.Effect = DragDropEffects.None;
		}
	}
	
	void TxtPathX_DragDrop(object sender, DragEventArgs e)
	{
		if ( e.Data.GetDataPresent(DataFormats.FileDrop) ) {
			string[] fileNames;
			fileNames = (string[])e.Data.GetData(DataFormats.FileDrop, false);
			if ( fileNames.Length == 1 ) {
                string fn = fileNames[0];
                if ( (TextBox)sender == txtPathL ) {
                    _leftPath = fn;
                    txtPathL.Text = fn;
                }
                else {
                    _rightPath = fn;
                    txtPathR.Text = fn;
                }

                LoadPathesAndDiffToListViewWithCheck();
            }
		}
	}

    static string DiffTypeToString(DiffType t)
    {
        if ( t == DiffType.Same ) {return "==";}
        if ( t == DiffType.Different ) {return "<>";}
        if ( t == DiffType.LOnly ) {return "o-";}
        if ( t == DiffType.ROnly ) {return "-o";}
        return "  ";
        /*
            if ( t == DiffType.Same ) {return "";}
            if ( t == DiffType.Different ) {return "Differ";}
            if ( t == DiffType.LOnly ) {return "L only";}
            if ( t == DiffType.ROnly ) {return "R only";}
            return "?";
        */
    }

    ListViewItem MakeListViewItem(string subDir, string fileName, DiffType t)
    {
        string ext;
        string diffMark = DiffTypeToString(t);
        string sizeText;
        string lastModifText;

        if ( fileName == null ) {
            // directory
            ext = "/";
            sizeText = "";
            lastModifText = "";
        }
        else {
            ext = Path.GetExtension(fileName);
            sizeText = "";
            lastModifText = "";
        }
        
        var item = new ListViewItem(new string[]{subDir, fileName, ext, diffMark, sizeText, lastModifText});
        // item.Tag = ...;
        return item;
    }

    void LoadPathesAndDiffToListViewWithCheck()
    {
        if ( _leftPath != null && _rightPath != null ) {
            if ( Directory.Exists(_leftPath) && Directory.Exists(_rightPath) ) {
                LoadPathesAndDiffToListView();
            }
            else if ( File.Exists(_leftPath) && File.Exists(_rightPath) ) {
                LoadFileDiffToListView();
            }
        }
    }

    void LoadPathesAndDiffToListView()
    {
        //Console.WriteLine(_leftPath);
        //Console.WriteLine(_rightPath);

        lsvFiles.BeginUpdate();
        try {
            lsvFiles.Items.Clear();
            LoadSubFiles("", true, true);
            LoadSubDir("", true, true);
        }
        finally {
            lsvFiles.EndUpdate();
        }
    }

    void LoadFileDiffToListView()
    {
        lsvFiles.BeginUpdate();
        try {
            lsvFiles.Items.Clear();
            // todo
            // load
        }
        finally {
            lsvFiles.EndUpdate();
        }
    }

    void LoadSubDir(string relativeDirFromRoot, bool lDirExists, bool rDirExists)
    {
        string[] subdirs1 = lDirExists ? Directory.GetDirectories(Path.Combine(_leftPath , relativeDirFromRoot)) : new string[0];
        string[] subdirs2 = rDirExists ? Directory.GetDirectories(Path.Combine(_rightPath, relativeDirFromRoot)) : new string[0];

        int n1 = _leftPath.Length;
        int n2 = _rightPath.Length;

        foreach ( string sd1 in subdirs1 ) {
            string relativeSubDir = sd1.Substring(n1+1); // +1 intend to trim last path separator
            //Console.WriteLine(relativeSubDir);
            string tmpPath = Path.Combine(_rightPath, relativeSubDir);
            //Console.WriteLine(tmpPath);
            int index2 = Array.IndexOf(subdirs2, tmpPath);
            if ( index2 >= 0 ) {
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, null, DiffType.Unknown));
                subdirs2[index2] = null; // mark to avoid double pickup
                LoadSubFiles(relativeSubDir, true, true);
                LoadSubDir(relativeSubDir, true, true);
            }
            else {
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, null, DiffType.LOnly));
                LoadSubFiles(relativeSubDir, true, false);
                LoadSubDir(relativeSubDir, true, false);
            }
        }

        // to pick up exists in subdirs2 and not exists in subdirs1
        foreach ( string sd2 in subdirs2 ) {
            if ( sd2 != null ) {
                string relativeSubDir = sd2.Substring(n2+1);
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, null, DiffType.ROnly));
                LoadSubFiles(relativeSubDir, false, true);
                LoadSubDir(relativeSubDir, false, true);
            }
        }
    }
    
    void LoadSubFiles(string relativeDirFromRoot, bool lDirExists, bool rDirExists)
    {
        string subdir1 = Path.Combine(_leftPath , relativeDirFromRoot);
        string subdir2 = Path.Combine(_rightPath, relativeDirFromRoot);

        string[] files1 = lDirExists ? Directory.GetFiles(subdir1) : new string[0];
        string[] files2 = rDirExists ? Directory.GetFiles(subdir2) : new string[0];

        int n1 = _leftPath.Length;
        int n2 = _rightPath.Length;

        string relativeSubDir = relativeDirFromRoot;//(relativeDirFromRoot=="")?"":subdir1.Substring(n1+1);
        //Console.WriteLine(relativeSubDir);

        foreach ( string filePath1 in files1 ) {
            string fileName = Path.GetFileName(filePath1);
            string filePath2 = Path.Combine(subdir2, fileName);

            int index2 = Array.IndexOf(files2, filePath2);
            if ( index2 >= 0 ) {
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, fileName, JudgeFileContentDiff(filePath1, filePath2)));
                files2[index2] = null; // mark to avoid double pickup
            }
            else {
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, fileName, DiffType.LOnly));
            }
        }

        // to pick up exists in subdirs2 and not exists in subdirs1
        foreach ( string filePath2 in files2 ) {
            if ( filePath2 != null ) {
                string fileName = Path.GetFileName(filePath2);
                lsvFiles.Items.Add(MakeListViewItem(relativeSubDir, fileName, DiffType.ROnly));
            }
        }
    }

    DiffType JudgeFileContentDiff(string path1, string path2)
    {
        FileInfo fi1, fi2;
        fi1 = new FileInfo(path1);
        fi2 = new FileInfo(path2);

        try {
            if ( fi1.Length != fi2.Length ) {
                return DiffType.Different;
            }

            using (     FileStream fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read) ) {
                using ( FileStream fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read) ) {
                    byte[] buf1 = new byte[0x1000];
                    byte[] buf2 = new byte[buf1.Length];
                    while ( true ) {
                        int readSize1 = fs1.Read(buf1, 0, buf1.Length);
                        int readSize2 = fs2.Read(buf2, 0, buf2.Length);

                        if ( readSize1 != readSize2 ) { return DiffType.Unknown; } // unexpected.
                        
                        for ( int i=0; i<readSize1; i++ ) {
                            if ( buf1[i] != buf2[i] ) {
                                return DiffType.Different;
                            }
                        }
                        
                        //ファイルをすべて読み込んだときは終了する
                        if ( readSize1 == 0 ) { return DiffType.Same; }
                    }
                }
            }
        }
        catch(FileNotFoundException      ){return DiffType.Unknown;}
        catch(DirectoryNotFoundException ){return DiffType.Unknown;}
        catch(UnauthorizedAccessException){return DiffType.Unknown;}
        catch(PathTooLongException       ){return DiffType.Unknown;}
        catch(DriveNotFoundException     ){return DiffType.Unknown;}
        catch(IOException                ){return DiffType.Unknown;}
    }

    [STAThread]
    static void Main(string[] args)
    {
        string target1 = null;
        string target2 = null;
        if ( args.Length > 2) {
            return;
        }
        if ( args.Length >= 1 ) {
            target1 = args[0];
        }
        if ( args.Length >= 2 ) {
            target2 = args[1];
        }
        Application.Run(new MyDiff(target1, target2));
    }
}
```