環境構築しなくても使えるPowerShellのテキスト処理7つ道具

テキストデータはサーバ上で処理されるだけでなく、サーバを離れWindows環境に保存されてから処理されることも少ないない。そのような場合にExcelが使われることが多いが、Excelは特定の行だけ抽出（もしくは除外）して出力するといった処理は得意ではない。CSV形式で保存しようとして「互換性のない機能が含まれている可能性があります」というダイアログに混乱する人も多いはずだ。かといって自分だけが使うならまだしも他人のWindows環境にPythonなどの環境を構築するなんて面倒でしょうがない。

このように自分以外の誰かに処理させるような場合、拡張子を「ps1」とするだけで動いてくれるPowerShellの便利さは唯一無二だろう。

下記のような形式のCSVファイルを題材に、PowerShellでテキスト処理する７つ道具を紹介する。

```text:date_monster.csv
date,monster
2021-07-01,ピカチュウ
2021-07-02,イーブイ
2021-07-02,ピカチュウ
2021-07-03,カモネギ
```

なお、環境構築不要と書いたが、PowerShellは初期状態では無効になっているので、もしスクリプトを実行して「このシステムではスクリプトの実行が無効になっているため～」というエラーメッセージが表示されたら、下記のコマンドで有効にする必要がある。

```
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process
```

# allow - 特定の文字列を含む行だけ出力する

grepでやっていることをPowerShellで再現する。ブラックリスト/ホワイトリストという呼び方もあるが、LinuxなどOSSがブラックリスト/ホワイトリストという呼び方をしない流れなのでallow/denyという呼び方で統一する。

```text:allow.txt
ピカチュウ
イーブイ
```

```powershell:allow.ps1
$list = Get-Content -Encoding utf8 ".\csv_sample\allow.txt"
$input = Get-Content -Encoding utf8 ".\csv_sample\date_monster.csv"
$out_file = ".\csv_sample\out.txt"
$null | Out-File $out_file

$input | ForEach-Object {
    $flag = $FALSE
    $temp = $_
    $list | ForEach-Object {
        if( $temp.Contains($_) ){
            if(!$flag){
                $flag = $TRUE
            }
        }
    }
    if($flag){
        Write-Host $_
        $_ | Out-File $out_file -Append
    }
}
```

冒頭の２行でファイル名を指定し、allowのファイルと入力ファイルを`Get-Content`で読み込む。処理結果をout.txtというファイルに追記するので最初にnullを書き込んでファイルをクリアにしている。なお通常の言語でnullと書くところをPowerShellでは$NULLと書く必要がある。trueやfalseも同様だ。

次に入力ファイルを読み込んだ`$input`をForEach-Objectでループさせる。このループの中では変数は`$_`に格納される。各行がallow.txtのどれかの行と一致したら`$flag=$TRUE`として、$TRUEとなっている行のみ出力する。

# deny - 特定の文字列を含む行だけを除外する

こちらはallowの反対のことをやっているだけなので解説は省略。

```powershell:deny.ps1
$list = Get-Content -Encoding utf8 ".\csv_sample\deny.txt"
$input = Get-Content -Encoding utf8 ".\csv_sample\date_monster.csv"
$out_file = ".\csv_sample\out.txt"
$null | Out-File $out_file

$input | ForEach-Object {
    $flag = $TRUE
    $temp = $_
    $list | ForEach-Object {
        if( $temp.Contains($_) ){
            if($flag){
                $flag = $FALSE
            }
        }
    }
    if($flag){
        Write-Host $_
        $_ | Out-File $out_file -Append
    }
}
```

# replace - 大量に置換する

文字列の置換もニーズが多い処理だろう。allow/denyと違い、変更前(old)と変更後(new)という２つの文字列を扱うため、`Get-Content`ではなく`Import-Csv`を使う。

```text:replace.txt
old,new
ピカチュウ,ライチュウ
```

```powershell:replace.ps1
$list = Import-Csv -Encoding utf8 ".\csv_sample\replace.txt"
$input = Get-Content -Encoding utf8 ".\csv_sample\date_monster.csv"
$out_file = ".\csv_sample\out.txt"
$null | Out-File $out_file

$input | ForEach-Object {
    $temp = $_
    $list | ForEach-Object {
        if( $temp.Contains($_.old) ){
            $temp = $temp.Replace($_.old,$_.new)
        }
    }
    Write-Host $temp
    $temp | Out-File $out_file -Append
}
```

`Import-Csv`で取り出した値はForEach-Objectの中で`$_.old`のようにヘッダで指定した文字列で取り出すことができるので、これを1行ごとにReplaceする。


# count - 出現回数を調べる

allowでは、ピカチュウとイーブイを含む行のみ出力したが、実際の現場では本当にピカチュウとイーブイがモレなくダブりなく出力されているか確認したい場合もある。そのような場合を想定し、allow.ps1を書き加えてみる。

```powershell:allow_count.ps1
$list = Get-Content -Encoding utf8 ".\csv_sample\allow.txt"
$input = Get-Content -Encoding utf8 ".\csv_sample\date_monster.csv"
$out_file = ".\csv_sample\out.txt"
$null | Out-File $out_file

$list_hash = @{} # カウント用のハッシュテーブルを追加
$list | ForEach-Object {
    $list_hash.add($_,0) # ハッシュテーブルの初期値は0
}

$input | ForEach-Object {
    $flag = $FALSE
    $temp = $_
    $list | ForEach-Object {
        if( $temp.Contains($_) ){
            $list_hash[$_] = $list_hash[$_] + 1 # 一致したらハッシュテーブルの値に1を足す
            if(!$flag){
                $flag = $TRUE
            }
        }
    }
    if($flag){
        Write-Host $_
        $_ | Out-File $out_file -Append
    }
}

$list_hash.GetEnumerator() | ForEach-Object { # ハッシュテーブルの中身を出力する
    Write-Host $_.Key,$_.Value
}
```

元となったallow.ps1から書き足した個所にコメントを付けた。ハッシュテーブルはキーとバリューが組になって保存されるデータ形式で「ピカチュウ」というキーに「０」というバリューを格納すると、`list_hash["ピカチュウ"]`といった記法でバリューを取り出すことができる。

冒頭でファイル名を指定した後に`list_hash = @{}`という記法でハッシュテーブルであることを宣言し、ここにlistに含まれる「ピカチュウ」や「イーブイ」といったキーに初期値である０を設定している。

これをファイルを処理するループの中で、一致する文字列があれば＋１をして、最後に各キーの値を取り出している。なおハッシュテーブルをそのままでforEach-Objectでループさせても何も出てこないので、`$list_hash.GetEnumerator()`と書いてEnumerate(繰り返し処理)できるように変換している。

もし入力ファイルにイーブイが存在しなかったら、イーブイの行が「0」と出力される。


# count2 - 出現回数を調べる（項目別）

countでは「ピカチュウ」という文字列が出てきた回数をハッシュテーブルに記録したが、実際のテキスト処理では「入会日」「予約日」のように同じ文字列が格納される可能性のある。こうなるとallow/denyでは何をやっているかわからなくなるので、入力ファイルをGet-ContentではなくImport-Csvで読み込み、特定の項目に何が含まれているかをチェックしながらハッシュテーブルに格納してみる。

allowはピカチュウの出現回数を数えるという機能追加をしたので、denyの方に「ピカチュウ」が出現している行の日付の出現回数を数えるという追加をする。

```powershell:deny_datecount.ps1
$list = Get-Content -Encoding utf8 ".\csv_sample\deny.txt"
$input = Get-Content -Encoding utf8 ".\csv_sample\date_monster.csv"
$out_file = ".\csv_sample\out.txt"
$null | Out-File $out_file

$date_hash = @{} # 日付カウント用ハッシュテーブル

$input | ForEach-Object {
    $flag = $TRUE
    $temp = $_
    $list | ForEach-Object {
        if( $temp.monster.Contains($_) ){ # $tempではなく$temp.monsterを比較
            if($date_hash.ContainsKey($temp.date)){ # 日付が存在していたら、すでに存在するキーに追記する
                $date_hash[$temp.date] = +1
            }else{
                $date_hash.add($temp.date,1) # 日付が存在しなかったら新たなキーを作る
            }
        }
    }
}

$date_hash.GetEnumerator() | ForEach-Object {
    Write-Host $_.Key,$_.Value
}

$date_hash | forEach-Object {
    Write-Host $_.Key,$_.Value
}
```

こちらもdeny.ps1から書き足した個所にコメントをした。Import-Csvで読み込んだのでforEach-Objectの中で`$_`を参照しようとすると`@{date=2021-07-01; monster=ピカチュウ}`という形式となり直接比較できないので、`$_.monster`といった形式で要素を取り出してから比較している。

`$date_hash`は最初は空で、ループの中でContainsKey()でキーが存在するかを確認し、存在する場合はバリューを追加、存在しない場合はキーを追加している。

# join - 別の値を結合する

別のテキストファイルの値を結合するというパターンを紹介する。SQLで言えばJOINに相当する。

サンプルとなるファイルは、ひとつのファイルにはdateとmonsterが書かれ、もうひとつのファイルにはモンスターと日付が書かれている。これを結合し、2021-07-01にはどれだけvalueを集められたかを集計できるというわけだ。

```text:date_monster.csv
date,monster
2021-07-01,ピカチュウ
2021-07-02,イーブイ
2021-07-02,ピカチュウ
2021-07-03,カモネギ
```

```text:monster_value.csv
monster,value
ピカチュウ,100
イーブイ,10
```

```powershell:join.ps1
$monster_value = Import-Csv -Encoding UTF8 ".\csv_sample\monster_value.csv"

$monster_hash = @{}
$monster_value | ForEach-Object {
    $monster_hash.add($_.monster,$_.value)
}

$date_monster = Import-Csv -Encoding UTF8 ".\csv_sample\date_monster.csv"

$date_monster | ForEach-Object {
    if ($monster_hash[$_.monster]){
        Write-Host "$($_.date),$($_.monster),$($monster_hash[$_.monster])"
    }
}
```

最初にmonster_valueを読み込み、ハッシュに格納する。次にdate_monsterを読み込みながら、もしハッシュに存在する場合は末尾に値を付与している。慣れないうちは結合する側を先に読み込むなど順序に混乱するかもしれない。

# dialog - 画面上に文字列を入力して分岐する

これまでファイル名などパラメータはスクリプト上にハードコーディングしていたが、後から手入力した方が楽な場合がある。そんなときは`Read-Host`を使って入力した値を処理することができる。

時間がかかる処理を始める前の確認などにも使えるかもしれない。

```powershell:dialog.ps1
Write-Output "処理を選択してください"
Write-Output "1:ハードディスクを初期化する"
Write-Output "2:OSを再インストールする"

$input = Read-Host "input(1-2)"

if ( $input -eq 1 ){
    Write-Output "1"
}

if ( $input -eq 2 ){
    Write-Output "2"
}
```