【PowerShell】Excelを読み書きする

# はじめに
PowerShellでも、ほぼVBAと同じ書き方でExcelの編集が行える。
正確には.NET Frameworkを使ってCOMオブジェクトを操作できるということだけど、その構文がほぼVBAと同じなので、VBAと似た感覚で記述できると思う。

# PowerShellで書くメリット
## バージョン管理しやすくなる
VBAはExcelファイルに内包されており、そのExcelファイル(xlsx)はバイナリ形式ということもあって、VBAのバージョン管理がしづらいことは問題になっている。
それを解消するために、ExcelファイルからVBAスクリプトをエクスポートしてバージョン管理をやりやすくするためのツールも公開されている。
[vbacでエクセルVBAのソースコードをGitバージョン管理する方法](https://tonari-it.com/vba-vbac-git/)
しかし、PowerShellならテキスト形式そのものなので、手軽にバージョン管理が行える。

## バッチ処理に向いている
夜間に、社内システムから出力されたcsvファイルをExcelに取り込んでおきたい…など、ユーザーがExcelファイルを開いていない間に実行しておきたい編集作業に向いている。

# PowerShellで書くデメリット
## 開かれたブックの編集には不向き
あくまでも外部からExcelファイルを編集するので、ユーザーがブックを開いているときにマクロを実行させたい場合などは不向き。

# 基本的な書き方

```powershell:基本形サンプル

$excel = New-Object -ComObject Excel.Application
$book = $null
try {
    # VBAでいう「Application.Visible」などにあたる
    # このほか、Applicationオブジェクトから呼び出すメソッド等は$excelから呼び出すことができる
    $excel.Visible = $false
    $excel.DisplayAlerts = $false

    # ブックを開く(VBAと書き方がほぼおなじ
    $book = $excel.Workbooks.Open("C:\test\test.xlsx")

    # 1シート目を取得(インデックスは1から始まる）
    $sheet = $book.Sheets(1)

    $range = $sheet.Range("A1")

    # セルへの書き込み
    $range.Value = "bbbbb"

    # セルの値の取得
    # Valueの後ろにカッコが必要
    Write-Host $range.Value()

    [void]$book.Save()

} finally {
    if ($book -ne $null) {
        [void]$book.Close($false)
        [System.Runtime.Interopservices.Marshal]::ReleaseComObject($book)
    }

    # Excelの終了
    [void]$excel.Quit()

    # オブジェクトの開放
    # ApplicationとBookに対して行えばよい
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel)
}

```

## こうしたいときはどうすれば？を知りたいとき
VBAでの実現方法を調べ、それをそのままPowerShellの構文へ置き換えるだけで大体動く。

## プロパティに括弧が必要な場合がある
(@nukie_53様 情報提供ありがとうございます)

例えば、上記サンプルの値の取得で`$range.Value()`としたように、プロパティを参照する場合でも、引数ありのプロパティを取得する場合は後ろに括弧が必要。VBAでは省略できるのでValueプロパティに引数があることを忘れがちだが、PowerShellで括弧を忘れると意図しない結果が返ってくる。

参照：[Range.Value プロパティ (Excel) | Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range.value)

↓試してみたコード

```powershell:括弧有無のお試しサンプル抜粋
Write-Host "括弧なし"
Write-Host $range.Value
Write-Host "括弧あり"
Write-Host $range.Value()
```

↓出力結果

```
括弧なし
Variant Value (Variant) {get} {set} 
括弧あり
bbbbb
```

これに対し、`Width`プロパティなど引数のないプロパティは、括弧なしでも値が取得できる(括弧ありでも取得ができる)。

```powershell:Widthの括弧有無お試しサンプル抜粋
Write-Host "括弧なし"
Write-Host $range.Width
Write-Host "括弧あり"
Write-Host $range.Width()
```

```
括弧なし
52.8
括弧あり
52.8
```

## VBAの列挙型を使いたいとき
たとえば、VBAでいう`Range("A1").End(xlDown)`したいとき、`xlDown`はどうすればよいか。

### 値を調べて直接使用する
MSのリファレンス(例：[xldirection 列挙 (Excel) | Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/api/excel.xldirection))を調べたり、VBAのイミディエイトウィンドウで`Debug.Print`したりして調べた数値をそのまま使用する。

↓VBAのイミディエイトウィンドウで数値を調べた様子
![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/424975/a57aacd1-d822-3d28-0600-0c92f184e259.png)

```powershell:数値を使うサンプル
$sheet.Range("A1").End(-4121)
```

### アセンブリをロードして列挙型を使用する
.NET側にも同じ列挙型が用意されているので、それをロードして使用する。

```powershell:アセンブリをロードするサンプル
# 最初に1度ロードすればよい
[void][Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Interop.Excel")

# 型名が長いのでいったん変数に入れる
$xlDirection = [Microsoft.Office.Interop.Excel.XlDirection]

# 列挙型を使う
[void]$range.End($xlDirection::xlDown).Select()

```

## メソッドの引数を省略したいとき
たとえば、シートをコピーするとき、第1引数の`Before`は指定せずに、第2引数の`After`のみ指定したい場合がある。
VBAでは`Worksheets("Sheet1").Copy After:=Worksheets("Sheet3")`と書けばよいが、PowerShellでは以下のようにする。

```powershell:シートコピーのサンプル
$sheet.Copy([System.Reflection.Missing]::Value, "Sheet3")
```

`[System.Reflection.Missing]::Value`はメソッドの引数を省略していることを示す値。

# コンソールに意図しない「0」などが出るとき
メソッドの戻り値が標準出力に書かれている可能性がある。PowerShellはメソッドや関数の戻り値を変数に格納しない場合、標準出力に出てきたりする。
それを避けるためには、メソッドの頭に`[void]`を付けるとよい。

```powershell:voidをつけるサンプル
[void]$sheet.Range("B2").Select()
```