Skip to content

About

PowerShell から Excel を操作する基本サンプル

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

PowerShell で Excel を操作する基本

PowerShell から Excel を動かすサンプル集です。セルの読み書きから始めて、VBA マクロの呼び出し、bat ファイルとの連携、エラーの扱い、人に渡して動かすときの注意、セルへの書き込みの詳しい書き方までを 9 つの step で順に学びます。

各 step には見本のスクリプト(stepN.ps1)があります。この README の「打ち込むコード」を自分で入力して動かし、見本と比べながら進めてください。

全体像

flowchart LR
    bat["bat ファイル"] -->|"pwsh -File(step6)"| ps["PowerShell スクリプト"]
    ps -->|"& と .(step3・6)"| sub["別のスクリプト・関数"]
    ps -->|"COM(step1)"| excel["Excel"]
    excel --> book["処理対象のブック .xlsx"]
    excel -->|"Run(step2)"| xlsm["マクロのブック .xlsm"]
    excel -->|"Run(step4)"| bas["取り込んだ .bas"]
    excel -->|"Run(step5)"| xlam["アドイン .xlam"]
Loading
step テーマ 見本 使うもの
1 Excel の起動と終了、セルの読み書き step1.ps1
2 .xlsm のマクロを呼ぶ(引数、戻り値、配列) step2.ps1 macros.xlsm
3 マクロを使わず、PowerShell の関数を別ファイルに分ける step3.ps1 excel-lib.ps1
4 VBA を .bas(テキスト)で管理し、実行時に取り込む step4.ps1 vba\Module1.bas
5 アドイン(.xlam)のマクロを呼ぶ step5.ps1 macros.xlam
6 bat から ps を呼ぶ、ps から ps を呼ぶ、値を返す step6.ps1 step6.bat step6-return.ps1 step6-sub.ps1 step6-macro.ps1 step6-scope-child.ps1
7 エラーが起きたときの扱い方 step7.ps1 step7.bat macros.xlsm
8 人に渡して動かす(起動用の bat、5.1 と 7、実行ポリシー) step8.ps1 step8.bat step6-sub.ps1
9 セルへの書き込みを詳しく(範囲、値の種類、数式、書式、速さ) step9.ps1

step1 が土台で、step2〜5 は「処理をどこに書くか」の選択肢です。新しく書くなら step3、すでにある VBA を使うなら step2・4・5 を選びます。step6〜8 はどの方法とも組み合わせて使います。step9 は step1 の書き込みを詳しくしたもので、step1 の後ならいつ進めても構いません。

準備

  • 必要なもの: Windows、Excel、PowerShell 7(pwsh)。
  • 置き場所: スクリプトは E:\dev\excel に置く前提で書かれています。
git clone https://github.com/ZawaaDon/powershell-excel-basics.git E:\dev\excel

別の場所に置く場合は、各スクリプトの E:\dev\excel を置いた場所に書き換えてください。

  • 元データ: sample-org.xlsx の Sheet1 には、次の 5 行 5 列の表が入っています。スクリプトはこのファイルを書き換えず、毎回コピーしてから処理します。
A B C D E
1 a ka sa ta na
2 i ki si ti ni
3 u ku su tu nu
4 e ke se te ne
5 o ko so to no

練習の進め方

  1. 見本の stepN.ps1 の冒頭コメント(学ぶこと、覚えておくこと)を読みます。
  2. 同じフォルダに my-stepN.ps1 を作り、下の「ひな形」に各 step の「打ち込むコード」を足して入力します。
  3. PowerShell で .\my-stepN.ps1 を実行し、「実行結果」と同じになるか確かめます。
  4. 見本の .\stepN.ps1 も実行して、結果とコードを比べます。

my- で始まるファイルは git の管理対象外にしてあるので、自由に作って構いません。練習用のスクリプトは my-sample.xlsx に書き込みます。step ごとに my-sample-step1.xlsx のように名前を変えておくと、あとで結果を見比べられます。

ひな形

どの step でも使う「Excel を起動して、最後に必ず終了する」部分です。最初に my-step1.ps1 として入力し、step2 以降はこのファイルをコピーして始めます。

$srcPath = "E:\dev\excel\sample-org.xlsx"
$dstPath = "E:\dev\excel\my-sample.xlsx"
Copy-Item $srcPath $dstPath -Force -ErrorAction Stop

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false

try {
    $book  = $excel.Workbooks.Open($dstPath)
    $sheet = $book.Worksheets.Item("Sheet1")

    # ← ここに各 step のコードを書く

    $book.Save()
}
finally {
    if ($book)      { $book.Close($false) }
    if ($macroBook) { $macroBook.Close($false) }    # step2 以降で使う
    $excel.Quit()
    $sheet     = $null
    $book      = $null
    $macroBook = $null
    [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
    $excel = $null
    [GC]::Collect()
    [GC]::WaitForPendingFinalizers()
}

このまま実行すると何も表示されず、my-sample.xlsx ができます。実行後に Get-Process excel で何も出なければ、Excel は正しく終了しています。

finally の中は、エラーで止まっても必ず実行されます。ここを省くと、見えない Excel がプロセスとして残り続けます。

Step 1 : セルの読み書き

学ぶこと: セルを読む、行を順にたどる、セルに書く。

ひな形の「ここに各 step のコードを書く」の位置に入力します。

    # 読む
    Write-Host "A1 = $($sheet.Range('A1').Value2)"

    # A列を上から最終行まで読む
    $xlUp    = -4162
    $lastRow = $sheet.Cells.Item($sheet.Rows.Count, 1).End($xlUp).Row
    for ($r = 1; $r -le $lastRow; $r++) {
        Write-Host $sheet.Cells.Item($r, 1).Value2
    }

    # 書く(表の外の G 列)
    $sheet.Range("G1").Value2    = "練習"
    $sheet.Range("G1").Font.Bold = $true

実行結果:

A1 = a
a
i
u
e
o

my-sample.xlsx を開くと、G1 に太字で「練習」と入っています。

つまずきやすい点

  • 行、列、シートの番号はすべて 1 から始まります。
  • VBA の定数名(xlUp など)は使えないので、数値(-4162)で指定します。
  • プロパティ名を間違えて読むと、エラーにならず $null が返ります。エラーにして気づきたいときは、Step 7 の Set-StrictMode を使います。

試してみる: B列を読むように変える。G2 に数式 =SUM(1,2,3) を書く(.Formula を使う)。

Step 2 : .xlsm のマクロを呼ぶ

学ぶこと: $excel.Run("'ブック名'!マクロ名", 引数...) でマクロを呼ぶ。

macros.xlsm には次のマクロが入っています(中身は macros.txt で読めます)。

マクロ 内容
BoldHeader() アクティブなシートの 1 行目を太字にする
WriteText(addr, text) 指定セルに文字を書く
AddTwo(a, b) 2 つの数を足して返す
SumArray(values) 配列の中身をすべて足して返す

my-step1.ps1 をコピーして my-step2.ps1 を作り、Step 1 で書いた部分を次のコードに入れ替えます。

    # マクロのブックも、同じ Excel で開く
    $macroBook = $excel.Workbooks.Open("E:\dev\excel\macros.xlsm", 0, $true)

    # マクロは「いま選ばれているシート」に対して動くので、対象を選んでおく
    $book.Activate()
    $sheet.Activate()

    $excel.Run("'macros.xlsm'!BoldHeader")                      # 引数なし
    $excel.Run("'macros.xlsm'!WriteText", "G1", "マクロから")    # 引数あり
    $sum = $excel.Run("'macros.xlsm'!AddTwo", 3, 4)             # 戻り値あり
    Write-Host "AddTwo = $sum"
    $total = $excel.Run("'macros.xlsm'!SumArray", @(1, 2, 3))   # 配列を渡す
    Write-Host "SumArray = $total"

実行結果:

AddTwo = 7
SumArray = 6

呼び出しを短く書く

毎回 $excel.Run("'macros.xlsm'!...") と書くのは長いので、関数にまとめます。次の関数を try の前に足します。

function Invoke-Macro {
    $target    = "'$($macroBook.Name)'!$($args[0])"
    $macroArgs = @()
    if ($args.Count -gt 1) { $macroArgs = $args[1..($args.Count - 1)] }
    $excel.GetType().InvokeMember('Run', 'InvokeMethod', $null, $excel, @($target) + $macroArgs)
}

そして、先ほどのコードの下に次を足します。

    $sum2 = Invoke-Macro AddTwo 5 6
    Write-Host "AddTwo = $sum2"

AddTwo = 11 が増えれば成功です。

つまずきやすい点

  • 呼び出し名はファイル名で書きます(フルパスではありません)。
  • 呼べるのは、標準モジュールに書いた Public な Sub と Function だけです。
  • マクロに MsgBox があると、画面が非表示のまま待ち続けて止まります。

試してみる: Invoke-Macro で WriteText を呼んで G2 に文字を書く。SumArray に渡す配列を変える。

Step 3 : PowerShell の関数を別ファイルに分ける

学ぶこと: 共通の処理を関数にして別ファイルに置き、. ファイル で読み込む。マクロのブックは使いません。

my-step1.ps1 をコピーして my-step3.ps1 を作ります。まず、ファイルの先頭に次の 1 行を足します。

. "$PSScriptRoot\excel-lib.ps1"

次に、Step 1 で書いた部分を次のコードに入れ替えます。

    Set-BoldHeader $sheet
    Write-CellText $sheet "G1" "関数から"
    $sum = Add-Two 3 4
    Write-Host "Add-Two = $sum"

実行結果:

Add-Two = 7

つまずきやすい点

  • 関数の呼び出しは、カッコとカンマを使わず空白で区切ります。Add-Two(3, 4) と書くと「配列 1 個を渡した」ことになり、7 になりません。
  • $PSScriptRoot は、実行中のスクリプトがあるフォルダです。

試してみる: excel-lib.ps1 を読み、同じ書き方で「指定セルを黄色にする関数」を my-lib.ps1 に書いて呼ぶ($sheet.Range($addr).Interior.ColorIndex = 6)。

Step 4 : VBA を .bas で管理して取り込む

学ぶこと: VBA のコードをテキスト(vba\Module1.bas)で持ち、実行時に一時ブックへ取り込んで呼ぶ。

事前の設定(必須): Excel のオプション → トラスト センター → トラスト センターの設定 → マクロの設定 で、「VBA プロジェクト オブジェクト モデルへのアクセスを信頼する」をオンにします。セキュリティを下げる設定なので、使わなくなったらオフに戻してください。

my-step1.ps1 をコピーして my-step4.ps1 を作り、Step 1 で書いた部分を次のコードに入れ替えます。

    # 一時ブックを作り、.bas を取り込む(保存しないので .xlsm は作られない)
    $macroBook = $excel.Workbooks.Add()
    $macroBook.VBProject.VBComponents.Import("E:\dev\excel\vba\Module1.bas") | Out-Null
    $macroName = "'" + $macroBook.Name + "'!"       # 一時ブックの名前は実行時に決まる

    $book.Activate()
    $sheet.Activate()

    $sum = $excel.Run($macroName + "AddTwo", 3, 4)
    Write-Host "AddTwo = $sum"

実行結果:

AddTwo = 7

つまずきやすい点

  • 事前の設定がオフだと、VBProject が $null になり「null に対してメソッドを呼べない」で止まります。
  • .bas の改行は CRLF にします。1 行目の Attribute VB_Name = "Module1" は消しません。

試してみる: vba\Module1.bas に自分の Function を 1 つ足して呼ぶ。

Step 5 : アドイン(.xlam)のマクロを呼ぶ

学ぶこと: アドインを自分で開いてから、Step 2 と同じ書き方で呼ぶ。

my-step1.ps1 をコピーして my-step5.ps1 を作り、Step 1 で書いた部分を次のコードに入れ替えます。

    # アドインは自動では読み込まれないので、自分で開く
    $macroBook = $excel.Workbooks.Open("E:\dev\excel\macros.xlam")

    $book.Activate()
    $sheet.Activate()

    $excel.Run("'macros.xlam'!WriteText", "G1", "アドインから")
    $sum = $excel.Run("'macros.xlam'!AddTwo", 3, 4)
    Write-Host "AddTwo = $sum"

実行結果:

AddTwo = 7

つまずきやすい点

  • PowerShell から起動した Excel は、Excel に登録してあるアドインを自動では読み込みません。
  • アドインは非表示のブックで、$excel.Workbooks の一覧に出ません。Open() の戻り値を変数に取っておきます。
  • ファイルの場所は UNC パス(\\サーバー名\共有名\...)でも指定できます。呼び出し名はファイル名だけのままです。

試してみる: $excel.AddIns を foreach で回し、登録済みアドインの Name と FullName を表示する。

Step 6 : スクリプトから別のスクリプトを呼ぶ

学ぶこと: 引数を受け取るスクリプトを書き、ps と bat の両方から呼んで、値を受け取る。

この step は Excel を使わずに練習します(見本の step6-sub.ps1 は Excel に書き込みます)。

まず、呼ばれる側を my-sub.ps1 として入力します。

param(
    [string]$Name = "名無し",
    [int]$Count = 1
)
$ErrorActionPreference = "Stop"

"$Name さんに $Count 回あいさつしました"

次に、ps から呼ぶ側を my-step6.ps1 として入力します。

& "$PSScriptRoot\my-sub.ps1"                            # 引数なし
& "$PSScriptRoot\my-sub.ps1" "太郎" 3                    # 引数あり(順番で渡す)
& "$PSScriptRoot\my-sub.ps1" -Count 2 -Name "花子"       # 引数あり(名前で渡す)

$result = & "$PSScriptRoot\my-sub.ps1" "次郎" 5          # 戻り値を受け取る
Write-Host "戻り値 = $result"

実行結果:

名無し さんに 1 回あいさつしました
太郎 さんに 3 回あいさつしました
花子 さんに 2 回あいさつしました
戻り値 = 次郎 さんに 5 回あいさつしました

最後に、bat から呼ぶ側を my-step6.bat として入力します。bat の中には日本語を書かないでください(文字コードによって化けます)。

@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0my-sub.ps1" Taro 3
echo exit code = %ERRORLEVEL%

set RESULT=
for /f "usebackq delims=" %%A in (`pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0my-sub.ps1" Hanako 2`) do set RESULT=%%A
echo result = %RESULT%

pause

my-step6.bat をダブルクリックして実行します。実行結果:

Taro さんに 3 回あいさつしました
exit code = 0
result = Hanako さんに 2 回あいさつしました

戻り値のしくみ

PowerShell では「最後の行」や「return に書いた値」だけが返るのではありません。途中で出力したものが全部まとめて戻り値になります。関数でもスクリプトでも同じです。

function Test {
    "途中の値"
    return "returnの値"
}
$r = Test    # $r は「途中の値」「returnの値」の 2 つが入った配列

return は「その値を出力して、そこで終わる」という意味です。C 言語のように「return の値だけを返す」わけではありません。

書き方 戻り値に入るか
"文字" や $変数 をそのまま書く 入る
戻り値のあるメソッドを、変数に入れずに呼ぶ 入る(気づきにくい)
return 値 入る(そこで終了)
Write-Host 入らない(画面に出るだけ)
$x = ... や | Out-Null 入らない

気づきにくいのは、Excel のメソッドの戻り値が混ざる場合です。

function Get-Total($sheet) {
    $sheet.Columns.AutoFit()             # True が出力に混ざる
    return 10
}
$r = Get-Total $sheet                    # $r は 10 ではなく (True, 10)

いらない出力は | Out-Null、[void]、$null = のどれかで捨てます(step1.ps1 の AutoFit() | Out-Null はこのためです)。値を返したいときは、返したいものだけを出力します。

複数の値を返す

この節と、続く「関数の中の変数」「& で呼んだスクリプトの変数」の見本は step6-return.ps1 です。.\step6-return.ps1 で実行できます(Excel は sample-org.xlsx を読み取り専用で開き、保存しません)。

方法は 3 つあります。ふだんは 2 のオブジェクトにまとめる方法を使います。

# 1. 複数の値を出力して、左側に変数を並べて受け取る(順番で対応する)
function Get-MinMax($nums) {
    $min = ($nums | Measure-Object -Minimum).Minimum
    $max = ($nums | Measure-Object -Maximum).Maximum
    return $min, $max
}
$lo, $hi = Get-MinMax @(3, 8, 1, 5)     # lo = 1、hi = 8
$r = Get-MinMax @(3, 8, 1, 5)           # 1 つで受けると配列:$r[0] = 1、$r[1] = 8

# 2. オブジェクトにまとめて返す(名前で取り出せる)
function Get-Score {
    [pscustomobject]@{ Name = "太郎"; Total = 145; Average = 72.5 }
}
$s = Get-Score
Write-Host "$($s.Name) $($s.Total) $($s.Average)"    # 太郎 145 72.5

# 3. [ref] で引数に書き戻してもらう
function Get-Info([ref]$count) {
    $count.Value = 3
    return "ok"
}
$n = 0
$st = Get-Info ([ref]$n)                # st = ok、n = 3

1 は順番だけで対応させるので、途中に余計な出力が 1 つ混ざると全部ずれます。たとえば関数の中で ArrayList の $list.Add("x") を変数に入れずに呼ぶと、Add() が返す位置(0)が先頭に混ざり、$p, $q = ... で受けた $p が 0 になります。2 は名前で取り出すので、値を増やしても呼び出し側を直さずに済みます。スクリプト(& "xxx.ps1")から返すときも同じ書き方です。

受け取り方 結果
$a, $b, $c = 1, 2 $c は $null(足りない分は空)
$a, $b = 1, 2, 3 $b に 2 と 3 の配列が入る(余った分は最後の変数へ)

Excel のマクロ($excel.Run)が返せる値は 1 つだけです。複数返したいときは VBA 側で配列を返し、PowerShell では $r[0]、$r[1] のように取り出します。

関数の中の変数(スコープ)

関数の中の変数は、外の変数と「半分だけ」別になっています。読むときは外の変数も見えますが、代入すると関数の中だけの新しい変数になります。

$x = "外の値"
function Test-Read  { Write-Host "読む : $x" }                  # 外の値
function Test-Write { $x = "中で代入"; Write-Host "代入 : $x" }  # 中で代入
Test-Read
Test-Write
Write-Host "外 : $x"                                            # 外の値(変わらない)
関数の中ですること 外の変数はどうなるか
$x を読む 外の値が読める
$x = ... と代入する 変わらない(関数の中だけの $x ができる)
$x += ... 変わらない。しかも外の値に足すのではなく、空から始まる
引数名を外と同じ $x にする 変わらない(関数の中では引数が使われる)
$script:x = ... と書く 変わる(スクリプト全体の $x に代入する)
$arr[0] = ...(配列の中身を書き換える) 変わる

最後の行は、変数に代入しているのではなく、変数が指している配列の中身を書き換えているので、外にも反映されます。Excel の $sheet も同じで、excel-lib.ps1 の関数が引数の $sheet に書き込めるのはこのためです。

外の変数が読めるのは便利ですが(Step 2 の Invoke-Macro は外の $excel と $macroBook を読んでいます)、どこの値を使っているか分かりにくくなります。関数で使う値は、なるべく引数で渡します。外の変数を書き換えたいときは $script: を使うより、戻り値で返して呼び出し側で代入します。

& で呼んだスクリプトの変数

& で呼んだスクリプトの変数も、関数と同じ動きです。親(呼んだ側)の変数を読めますが、代入しても親は変わらず、終わると子の中で作った変数と関数は消えます。. で呼ぶと親の中でそのまま実行され、pwsh -File で呼ぶと別のプロセスで動きます。

見本の step6-return.ps1 の 6 では、子のスクリプト step6-scope-child.ps1 を 3 通りで呼んでいます。子は、親の $x を読んでから代入し、$script:y に代入し、新しい変数 $newVar と関数 Child-Func を作り、親の配列 $arr[0] を書き換えます。

呼び方 子から親の $x を読む 子で代入した $x $script:y 子で作った変数・関数 $arr[0] の書き換え
& "子.ps1" 読める 親は変わらない 親は変わらない 終わると消える 親に反映
. "子.ps1" 読める 親が書き換わる 親が書き換わる 親に残る 親に反映
pwsh -File "子.ps1" 読めない(空) 親は変わらない 親は変わらない 残らない 親の配列が見えない
  • & で呼んだ子の中の $script: は「子のスクリプト自身」のことなので、親には届きません。
  • . で呼ぶと変数も関数も親に残ります。Step 3 の . excel-lib.ps1 で関数を読み込めるのはこのためです。そのかわり、同じ名前の親の変数を上書きします。
  • pwsh -File は別の PowerShell で動くので、やり取りできるのは引数、出力(戻り値)、終了コードだけです。bat から呼んだときも同じです。

& で呼んだ子が親の変数を読めるのは、便利に見えて危険です。子を単体で実行したり bat から呼んだりすると、その変数は空になり、動きが変わります。子のスクリプトで使う値は param(...) で受け取り、結果は戻り値で返します(step6-sub.ps1 はこの形です)。

つまずきやすい点

  • param(...) はファイルの先頭に書きます。上に置けるのはコメントだけです。
  • %~dp0 は bat ファイル自身があるフォルダです。
  • for /f は、スクリプトが画面に出したものをすべて受け取ります。値を返すスクリプトでは、返したい 1 行のほかは出力しません。
  • そのまま出力した値はすべて戻り値になります(上の「戻り値のしくみ」)。Write-Host の表示は戻り値に入りません。
  • %ERRORLEVEL% は cmd が自動で値を入れる名前です。宣言せずに読めます。自分で set ERRORLEVEL=... はしません。
  • for /f の中で実行したスクリプトの終了コードは %ERRORLEVEL% に入りません(別の cmd で実行され、終了コードは捨てられます)。成功したかどうかは、受け取った値が空かどうかで判断します。終了コードも見たいときは、出力をファイルに書き出して set /p で読みます。

試してみる: step6.bat を実行し、step6-macro.ps1 がマクロで計算した値(21.5)を bat が受け取る流れを読む。最後の2つは同じ値を別の書き方で受け取っています(長い部分を変数にまとめる書き方と、ファイル経由で終了コードも見る書き方)。続けて .\step6-return.ps1 を実行し、複数の値の返し方と、関数の中の変数の動きを見る。6 は子のスクリプトを &、pwsh -File、. で呼び比べる例、最後の 7 は、シートの大きさをオブジェクトで返す関数と、関数の中から $sheet に書く例です。

Step 7 : エラーが起きたときの扱い方

学ぶこと: エラーを try / catch で受ける。計算できない値を見分ける。失敗を呼び出し元に伝える。

my-step1.ps1 をコピーして my-step7.ps1 を作り、Step 1 で書いた部分を次のコードに入れ替えます。

    $macroBook = $excel.Workbooks.Open("E:\dev\excel\macros.xlsm", 0, $true)

    # 1. PowerShell 側のエラーを受ける
    try {
        $excel.Workbooks.Open("E:\dev\excel\nothing.xlsx")
    }
    catch {
        Write-Host "開けませんでした"
    }

    # 2. マクロ側でエラーを処理し、結果を戻り値で受け取る
    foreach ($x in 9, -1) {
        $result = $excel.Run("'macros.xlsm'!SafeCalc", "sqrt", $x)
        if ($result -is [string]) {
            Write-Host "sqrt($x) : 計算できません"
        }
        else {
            Write-Host "sqrt($x) = $result"
        }
    }

    # 3. PowerShell の計算はエラーにならないので、自分で確かめる
    $v = [math]::Sqrt(-1)
    if ([double]::IsNaN($v)) {
        Write-Host "Sqrt(-1) : 計算できません($v)"
    }

実行結果:

開けませんでした
sqrt(9) = 3
sqrt(-1) : 計算できません
Sqrt(-1) : 計算できません(NaN)

つまずきやすい点

  • PowerShell は、エラーが起きてもメッセージを出して次の行へ進むことが多く、最後まで進むと終了コードは 0(成功)になります。先頭に $ErrorActionPreference = "Stop" を書くと、エラーの行で止まります。
  • マクロの中で起きたエラーは、PowerShell の try / catch では受けられません。Excel がエラーのダイアログを出して待ち続けます。マクロ側で On Error を書き、戻り値でエラーを知らせます(macros.txt の SafeCalc を参照)。
  • Excel が異常終了すると、その後の呼び出しはすべてエラーになります。Quit() も失敗するので、後始末にも try / catch が要ります(下の「後始末を止まらない形にする」)。
  • 名前の打ち間違いは、エラーにならないことがあります。作っていない変数($cases のつもりの $case など)を読むと $null になり、foreach は 1 回も回らずに先へ進みます。先頭に Set-StrictMode -Version Latest を書くと、作っていない変数や、Excel にないプロパティ($excel.Workbookz など)を読んだ時点でエラーになります。ひな形の finally は、step によっては作っていない $macroBook を読むので、使うときは try の前に $book = $null と $macroBook = $null を書いておきます。
  • Start-ThreadJob で動かした見張り役の中のエラーは、画面に出ません。途中でエラーが起きても次の行へ進み、状態も Completed になります。たとえば Start-Sleep -Seconds の秒数を書き忘れると、待たずにすぐ Excel を終了させます。おかしいときは Receive-Job $watchdog を実行すると、ジョブの中のエラーが表示されます。

後始末を止まらない形にする

ひな形の finally は、Excel が動いている前提で書いてあります。Excel が落ちた後でも止まらないように、次の関数を try の前に足します(見本の step7.ps1 と同じ)。

function Stop-ExcelSafely($excel) {
    try { $excel.Quit() } catch { }
    try { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel) } catch { }
}

finally の中の $excel.Quit() と ReleaseComObject の 2 行を、Stop-ExcelSafely $excel の 1 行に置き換えます。Excel が落ちた後は $book.Close() も失敗するので、見本の 4 と 5 では Close() を呼ばずに Stop-ExcelSafely だけにしています。

試してみる: SafeCalc の第 1 引数を "log" "acos" "exp" "div" に変え、どの値で計算できなくなるか確かめる。続けて見本の .\step7.ps1 を実行し、時間切れと異常終了の扱い(4 と 5)を読む。実行には 10 秒ほどかかります。最後に step7.bat をダブルクリックし、終了コードで OK と FAILED に分かれるところを見る(2 つ目の呼び出しは、わざと失敗させています)。

Step 8 : 人に渡して動かす

学ぶこと: bat は入口だけにして、処理は ps に書く。PowerShell のバージョンを決める。実行ポリシーで止まらないようにする。

この step は Excel を使わずに練習します(見本の step8.ps1 は step6-sub.ps1 を呼んで Excel に書き込みます)。

bat と ps の役割分担

bat から値を受け取るには for /f が要り、終了コードも取れなくなります(Step 6)。そこで、bat は起動するだけにして、値のやり取りは ps どうしで行います。

flowchart LR
    user["ダブルクリック"] --> bat["bat(入口)"]
    bat -->|"引数 %*"| main["本体の ps"]
    main -->|"& で呼ぶ・戻り値を受け取る"| sub["処理の ps"]
    main -.->|"終了コードだけ"| bat
Loading

bat が向いているのは「起動する」ことです。

  • ダブルクリックで実行できます(.ps1 をダブルクリックすると、メモ帳で開くだけです)。
  • -ExecutionPolicy Bypass を付けて、実行ポリシーで止められずに動かせます。
  • pwsh か powershell かを決めて起動できます。
  • 最後の pause で、結果を読むまでウィンドウを閉じずにおけます。
  • タスクスケジューラやほかのツールから呼びやすい形です。

PowerShell 5.1 と 7

PowerShell 5.1 PowerShell 7
コマンド powershell pwsh
入手 Windows に最初から入っている 自分でインストールする
BOM なしの .ps1 の読み方 Windows の文字コード(日本語版は Shift-JIS)。日本語が化ける UTF-8
  • どちらで動くかは、起動したコマンドで決まります。& "パス" で呼んだスクリプトは、呼んだ側と同じバージョンで動きます。
  • スクリプトの先頭に #Requires -Version 7 と書くと、5.1 で実行したときに 1 行目も動かずに止まります。
  • 今どちらで動いているかは $PSVersionTable.PSVersion で確かめます。

打ち込むコード

呼ばれる側には、Step 6 で作った my-sub.ps1 を使います(まだなければ Step 6 のコードで作ってください)。

本体を my-step8.ps1 として入力します。

#Requires -Version 7
param(
    [string]$Name = "名無し"
)
$ErrorActionPreference = "Stop"

Write-Host "PowerShell = $($PSVersionTable.PSVersion)"
Write-Host "実行ポリシー = $(Get-ExecutionPolicy)"

$result = & "$PSScriptRoot\my-sub.ps1" $Name 2
Write-Host "戻り値 = $result"

入口を my-step8.bat として入力します。2 つ目の呼び出しは、5.1 で止まることを確かめるためのものです。

@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0my-step8.ps1" %*
echo exit code = %ERRORLEVEL%

powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0my-step8.ps1" %*
echo exit code = %ERRORLEVEL%

pause

コマンドプロンプトで my-step8.bat Hanako と実行します。実行結果:

PowerShell = 7.6.6
実行ポリシー = Bypass
戻り値 = Hanako さんに 2 回あいさつしました
exit code = 0
The script 'my-step8.ps1' cannot be run because it contained a "#requires" statement for Windows PowerShell 7.0. ...
exit code = 1

バージョンの数字は、入っている PowerShell によって変わります。5.1 のエラーは英語で表示されることがあります。

つまずきやすい点

  • %* は、bat が受け取った引数をすべてそのまま渡します。ダブルクリックで実行したときは引数なしになり、param に書いた省略時の値が使われます。
  • #Requires はコメントに見えますが、命令です。# と Requires の間に空白を入れません。
  • 実行ポリシーは PC の設定です。-ExecutionPolicy Bypass はその 1 回の実行だけに効き、PC の設定は変えません。PowerShell から直接 Get-ExecutionPolicy を実行すると、RemoteSigned など PC の設定が表示されます。
  • ネットやメールから受け取ったファイルには「別の PC から来た」印が付いていて、RemoteSigned の PC では実行が止められます。中身を確かめてから Unblock-File .\ファイル名.ps1 で印を外します(ファイルのプロパティの「許可する」でも外せます)。bat から -ExecutionPolicy Bypass で起動した場合は止まりません。
  • pwsh が入っていない PC では、bat の pwsh の行が「認識されていません」で失敗し、終了コードは 9009 になります。

試してみる: my-step8.bat をダブルクリックして、引数なしの結果を見る。続けて見本の step8.bat を実行し、本体(step8.ps1)が step6-sub.ps1 を呼んで Excel に書き込む流れを読む。

Step 9 : セルへの書き込みを詳しく

学ぶこと: 2次元配列で範囲にまとめて書く。値の種類によって変わる書き方。数式と書式の書き方。

my-step1.ps1 をコピーして my-step9.ps1 を作り、Step 1 で書いた部分を次のコードに入れ替えます。

    # 1. 2次元配列を作って、範囲にまとめて書く
    $data = New-Object 'object[,]' 2, 4
    $data[0, 0] = "名前"; $data[0, 1] = "国語"; $data[0, 2] = "数学"; $data[0, 3] = "合計"
    $data[1, 0] = "太郎"; $data[1, 1] = 80;     $data[1, 2] = 65
    $sheet.Range("G1").Resize(2, 4).Value2 = $data

    # 2. 値の種類で変わる書き方
    $sheet.Range("G4").Value2 = "001"                    # 数値の 1 になる
    $sheet.Range("G5").Value2 = "'001"                   # 001 のまま
    $sheet.Range("G6").Value  = [datetime]"2026-10-06"   # 日付は Value で書く
    $sheet.Columns.Item(7).AutoFit() | Out-Null          # 幅が足りないと Text は ### になる
    Write-Host "G4 = $($sheet.Range('G4').Value2)"
    Write-Host "G5 = $($sheet.Range('G5').Value2)"
    Write-Host "G6 = $($sheet.Range('G6').Text)"

    # 3. 数式は '...' で書く
    $sheet.Range("J2").Formula = '=SUM($H$2:$I$2)'
    Write-Host "J2 = $($sheet.Range('J2').Value2)"

    # 4. 書式
    $sheet.Range("G1:J2").Borders.LineStyle = 1
    $sheet.Range("G1:J1").Interior.Color = 221 + 235 * 256 + 247 * 65536

実行結果:

G4 = 1
G5 = 001
G6 = 2026/10/6
J2 = 145

my-sample.xlsx を開くと、G1:J2 に罫線付きの表があり、見出しの背景が薄い青になっています。

変数を埋め込んで書く

"..."(二重引用符)の中に変数を書くと、値に置き換わった 1 つの文字列になり、1 つのセルに入ります。見本の step9.ps1 では「値の種類」シートの G 列に書いています。$a = "りんご"; $b = "みかん"; $c = "ぶどう"、$x = 1; $y = 2; $z = 3 のときの結果です。

書き方 セルに入った値 型
"$a, $b, $c" りんご, みかん, ぶどう 文字列
'$a, $b, $c' $a, $b, $c(置き換わらない) 文字列
"$x, $y, $z" 1, 2, 3 文字列
"$x,$y,$z" 1,2,3 文字列
"$x,234" 1234 数値
"$x/$y" 46024(1 月 2 日) 日付
"'$x/$y" 1/2 文字列
"合計は $($x + $y + $z) 個" 合計は 6 個 文字列
"シート名は $($vs.Name)" シート名は 値の種類 文字列
"シート名は $vs.Name" シート名は System.__ComObject.Name 文字列
@("x", "y", "z") -join ", " x, y, z 文字列
"$x`n$y`n$z" 1、2、3(セルの中で改行) 文字列
  • '...'(単一引用符)の中では置き換わりません。数式を書くときとちょうど逆です。
  • 置き換わった後の文字が数値や日付に見えると、"001" と同じように Excel が変換します。文字のまま残したいときは、先頭に ' を付けます。
  • 計算やプロパティは $( ) で囲みます。囲まないと $vs の部分だけが置き換わり、.Name は文字のまま残ります。

配列の要素を埋め込むときも、$( ) で囲みます。見本では、1. で作った $names(1次元配列:一郎, 二郎, 三郎)と $data(2次元配列:見出し、太郎、花子の 3 行 3 列)を使っています。

書き方 セルに入った値
"$names[0], $names[1]" 一郎 二郎 三郎[0], 一郎 二郎 三郎[1]
"$($names[0]), $($names[1]), $($names[2])" 一郎, 二郎, 三郎
$names -join ", " 一郎, 二郎, 三郎
$names[0..1] -join ", " 一郎, 二郎(0〜1 番目だけ)
"$data[1,0], $data[1,1]" 名前 国語 数学 太郎 80 65 花子 72 90[1,0], …
"$($data[1,0]), $($data[1,1]), $($data[1,2])" 太郎, 80, 65
$data -join ", " 名前, 国語, 数学, 太郎, 80, 65, 花子, 72, 90
(0..2 | ForEach-Object { $data[1, $_] }) -join ", " 太郎, 80, 65(1 行目だけ)
  • "$names[0]" と書くと、まず $names が配列全体を空白でつないだ文字に置き換わり、その後ろに [0] がそのまま付きます。
  • 2次元配列の添字は [行, 列] の 2 つです。-join を使うと行の区切りがなくなり、全部が 1 列につながります。1 行分だけつなぎたいときは、列の番号を回して取り出します。

書き方の一覧

したいこと 書き方
1 セルに書く $sheet.Range("A1").Value2 = 値
範囲にまとめて書く $sheet.Range("A1:C2").Value2 = 2次元配列
範囲全体に同じ値を書く $sheet.Range("A1:C2").Value2 = 値
起点から広げる・ずらす .Resize(行数, 列数) .Offset(下へ, 右へ)
日付を書く .Value = [datetime]...(Value2 だと数値になる)
文字列のまま書く "'001"、または先に .NumberFormat = "@"
変数を埋め込んで書く .Value2 = "$a, $b, $c"(計算やプロパティは "$($x + 1)")
空欄にする .Value2 = $null
数式を書く .Formula = '=SUM(A1:A3)'
相対位置で数式を書く .FormulaR1C1 = "=RC[-1]*2"(同じ行の 1 列左)
数式を値だけにする $r.Value2 = $r.Value2
表示形式 .NumberFormat = "#,##0" "yyyy/mm/dd"
罫線 .Borders.LineStyle = 1(実線)
文字色・背景色 .Font.Color .Interior.Color = 赤 + 緑*256 + 青*65536
値だけ消す・全部消す .ClearContents()・.Clear()(どちらも | Out-Null を付ける)
速くする 2次元配列で 1 回で書く。$excel.ScreenUpdating = $false、$excel.Calculation = -4135(手動)

つまずきやすい点

  • 2次元配列は New-Object 'object[,]' 行数, 列数 で作ります。書くときの添字は 0 始まりで、範囲を読んで返ってくる配列は 1 始まりです。
  • @(@(1, 2), @(3, 4)) は 2次元配列ではなく「配列の配列」です。範囲に書くと、エラーにならずにエラー値が入ります。
  • 縦 1 列に書くときも 行数, 1 の 2次元配列にします。1次元配列は「横 1 行」とみなされ、縦の範囲に書くと全部が最初の値になります。
  • Excel は書いた文字を、手で入力したときと同じように解釈します。"001" は 1、"1/2" は日付(1 月 2 日)、20 桁の数字は 1.23E+19 になります。
  • Value は読むときに .Value() と書きます。.Value と書くと、値ではなくプロパティの説明が返ります。読むだけなら Value2 で足ります(日付はシリアル値の数値で返ります)。
  • 数式を "..." で書くと、$A$1 の $A が PowerShell の変数とみなされます。"=SUM($A$1:$A$3)" はスクリプト全体が構文エラーで動かず、"=$A$1*2" は =*2 になって 0x800A03EC で止まります。数式は '...' で書きます。変数を埋め込みたいときは "..." を使い、数式の $ は `$ と書きます。
  • 数式の書き間違いは 0x800A03EC というエラーになります。
  • 計算エラーのセル(#DIV/0! など)を Value2 で読むと、エラーにならず -2146826281 などの整数が返ります。Text が # で始まるかで確かめます。
  • Text は画面の表示と同じなので、列の幅が足りないと ####### が返ります。
  • Calculation はブックを開いているときだけ設定できます。手動(-4135)のまま保存すると、次にそのファイルを開いたときも手動計算になるので、保存の前に自動(-4105)に戻します。

試してみる: 見本の .\step9.ps1 を実行し、sample-step9.xlsx の「値の種類」「数式」「速さ」シートを見る。実行結果の最後に、1 セルずつ書いた場合と 2次元配列で書いた場合の時間が出ます(300 行 × 5 列で、1 秒以上と数十ミリ秒ほどの差)。

ファイル一覧

ファイル 内容
step1.ps1〜step9.ps1 各 step の見本
step6.bat step7.bat step8.bat bat から呼ぶ見本
step6-sub.ps1 step6-macro.ps1 step6 で呼ばれる側のスクリプト
step6-return.ps1 step6-scope-child.ps1 step6 の続き(複数の値を返す、関数とスクリプトの中の変数)と、そこで呼ぶ子のスクリプト
excel-lib.ps1 step3 の共通関数
sample-org.xlsx 元データ(書き換えない)
macros.xlsm macros.xlam マクロのブックとアドイン
macros.txt vba\Module1.bas マクロの中身(読む用と、step4 で取り込む用)
sample-stepN.xlsx my-* 実行時にできる作業用ファイル(git の管理対象外)

困ったとき

  • Excel が残った: Get-Process excel で確認し、Stop-Process -Id 番号 で終了します。自分で開いている Excel まで閉じないよう、番号を指定してください。
  • スクリプトが止まったまま戻らない: 見えない Excel がダイアログを出して待っています。ひな形の $excel.Visible = $false を $true にすると、何が出ているか見えます。
  • 「このシステムではスクリプトの実行が無効」と出る: pwsh -ExecutionPolicy Bypass -File .\my-step1.ps1 で実行します(Step 8 を参照)。
  • エラーは出ないのに結果がおかしい: 変数名やプロパティ名の打ち間違いを疑います。先頭に Set-StrictMode -Version Latest を足して実行すると、間違えた行でエラーになります(Step 7 を参照)。
  • 日本語が化ける: Windows 標準の PowerShell 5.1(powershell)ではなく、PowerShell 7(pwsh)で実行してください(Step 8 を参照)。

About

PowerShell から Excel を操作する基本サンプル

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages