【PowerShell】Unixのdiffライクな文字列差異比較をPowerShellで行う

# はじめに
Unix系のdiffみたいな差分比較を行いたい...

DFやWinmergeから卒業したい...

# 文字列の差異比較が難しいPowershell
文字列差異比較なんて単純に思えるし、CLIならばそれができるコマンドが当たり前に整備されていそうなもの

差異を比較するコマンドレット..
あります。その名は``Compare-Object``


```text:事前に準備した差分ファイル
#testDiff１
cat .\testDiff1.txt
Hello!
This is testDiff1

#testDiff2
cat .\testDiff2.txt
Hello!
This is testDiff2
```

```powershell:compare-object
Compare-Object .\testDiff1.txt .\testDiff2.txt

InputObject     SideIndicator
-----------     -------------
.\testDiff2.txt =>
.\testDiff1.txt <=

```

何か思ってたんと違う^^
よくみるとファイル名の差分を比較しているではないか...

まぁよく考えてみると``Compare-Object``だから「ファイル内のテキスト情報を読み取って比較する」なんてニュアンスはどこにもないですね。

そうこのコマンドレットは**2つのオブジェクトを差分比較するコマンドレット**なのでした(紛らわしいー)


# テキストを差分比較はどうすればいいのか？
オブジェクト同士を比較するのであれば、それ用のオブジェクト同士を比較すればいいのではないだろうか？
つまり
1. ファイルを読み取りその内容を変数に格納(Stringオブジェクトの作成)
1. ファイル１・ファイル2の各Stringオブジェクトを作成し、それを比較(Compare-Object)する。

これでいけるかも...

```Powershell:Stringオブジェクト同士で比較
Compare-Object $txt1 $txt2

InputObject       SideIndicator
-----------       -------------
This is testDiff2 =>
This is testDiff1 <=
```

なんかそれっぽくなった

でも差異が存在する行位置の表示が無いからちょっとわかりにくいね...

# 文字列比較用の関数を作成する

上記の処理を比較毎にいちいち実施するのも面倒なので関数化してしまいましょう。
おまけで行数の情報もいれて扱いしやすくしましょう。
## ソースコード
```powershell:Compare-Text
    function Compare-Text() {
        #パラメータ定義
        Param(
            [switch]$guiDialog,
            [Parameter(Mandatory=$false)]
            [ValidateSet("Default","UTF8")]
            $Encoding = "Default",
            [Parameter(Position=0)]
            [string]$referencePath = "",
            [Parameter(Position=1)]
            [string]$differencePath = ""
        )

        #ファイル選択
        if($guiDialog){
            $getFile = "Select-File -guidialog"
        }else{
            $getFile = "Select-File"
        }
        if($referencePath -eq ""){
            Write-Host "比較元ファイルを選択してください"
            $referencePath = Invoke-Expression $getFile
        }
        $referenceText = Get-Content -Encoding $Encoding $referencePath

        if($differencePath -eq ""){
            Write-Host "差分ファイルを選択してください"
            $differencePath = Invoke-Expression $getFile
        }
        $differenceText = Get-Content -Encoding $Encoding $differencePath

        #差分比較
        $tmp=Compare-Object -ReferenceObject $referenceText -DifferenceObject $differenceText
        $diffObjects = $tmp | ForEach-Object {
            [PSCustomObject]@{
                Line = $_.inputobject.Readcount
                Text = $_.inputobject
                Indicator = $_.sideindicator
            }
        }
        $diffObjects = $diffObjects | Sort-Object -Property Line
        return $diffObjects
    }
```

## 書式

```powershell:書式
Compare-Text [-guiDialog] [-Encoding Default | UTF8 ] ファイル1 ファイル2
```
## 出力サンプル
```Text
Line Text              Indicator
---- ----              ---------
   2 This is testDiff1 <=
   2 This is testDiff2 =>
   3 こんにちは        <=
   3 こんばんは        =>
```

## オプション

| オプション      | 説明                                                                      |
| :-------------- | ------------------------------------------------------------------------ | 
| -guiDialog      | GUIのダイアログを使用してファイルを選択します。                              | 
| -Encoding       | ファイルの文字コードを指定します。<br>DefaultはSJISです。                    | 
| -referencePath  | 比較元ファイルのパスを指定します。<br>Indicatorで"<="で示されるファイルです。 | 
| $differencePath | 比較ファイルのパスを指定します。<br>Indicatorで"=>"で示されるファイルです。   | 

## 返値
以下のプロパティを含むカスタムオブジェクトが返されます。

| プロパティ | 説明                                               | 
| :--------- | ------------------------------------------------ | 
| Line       | 差異発生個所の行位置                               | 
| Text       | 差異内容 　　　　　　　                             | 
| Indicator  | <=：ファイル1にだけ存在<br>=>：ファイル2にだけ存在 | 

## 前提・注意点
比較対象のパスを未指定時に対話形式でファイルを選択するモードになりますが、
これは``Select-File``という別の自作関数を実行することで実現しています。
この機能を使用する場合は以下のページに貼られているモジュールのインポートが必要です。

https://qiita.com/Kit-i/items/d0c20a32d1bb2ec9a841

文字コードはSJISとUTF8に対応しています。
オプション``-Encoding``で指定します。
尚、デフォルトはPowershellの標準文字コードであるSJISに設定されています。

## 補足
関数内の処理で**行番号**を基準に昇順でソートしています。
ただし、関数の処理結果をカスタムオブジェクトとして返すようにしているため関数の出力を``Sort-Object``などでパイプして任意のプロパティでソートかけることで、``Text``や``Indicator``の値で並び替えたり降順で出力したりといった対応も可能です。

## 制作時備忘

### 位置指定パラメータを特定の値で固定する
4つのオプションの内、1つが``Switch``型、残り3つが``String``型のパラメータとなるため、
そのままだと位置指定パラメータが3つ受け入れ可能となってしまう。
オプションは以下の順で定義されている。
1. 文字コード(-Encoding)
1. ファイル1(-referencePath)
1. ファイル2(-differencePath)

即ち、以下のように関数を実行すると第1引数が文字コードを指定したものと判別されてしまう。
```powershell:第1引数
Compare-Text ファイル1 ファイル2
```
```powershell:パラメータエラー
Compare-Text : パラメーター 'Encoding の引数を確認できません。引数 "XXXXX.txt" は、ValidateSet 属性で指定されたセット "Default,UTF8" に属していません。このセットの引数を指定して、コマンドを再度実行してください。
発生場所 行:1 文字:14
+ Compare-Text XXXXX.txt XXXXX.txt
+              ~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Compare-Text]、ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Compare-Text
```

単純に定義する順番を変えて文字コードのオプションを最後に定義することで回避することも可能だが、
並び順がなんか腑に落ちないなと感じた。
そこでファイル1とファイル2が固定の位置パラメータを持たすことができればいいのではないか？と考えた。

特定のオプションに対して任意の位置指定パラメータを割り当てるには``Parameter``属性に`Position=インデックス``を明示することで実現可能だった。
```powershell
    [Parameter(Position=0)]
    [string]$referencePath = "",
    [Parameter(Position=1)]
    [string]$differencePath = ""
```

これにより第1引数は必ず「ファイル1」、第2引数は必ず「ファイル2」を指定したものと認識させることが可能となった。

# 参考
https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/compare-object?view=powershell-7.3

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_functions_advanced_parameters?view=powershell-7.3