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"]
| 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 |
- 見本の
stepN.ps1の冒頭コメント(学ぶこと、覚えておくこと)を読みます。 - 同じフォルダに
my-stepN.ps1を作り、下の「ひな形」に各 step の「打ち込むコード」を足して入力します。 - PowerShell で
.\my-stepN.ps1を実行し、「実行結果」と同じになるか確かめます。 - 見本の
.\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 のコードを書く」の位置に入力します。
# 読む
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 を使う)。
学ぶこと: $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 に渡す配列を変える。
学ぶこと: 共通の処理を関数にして別ファイルに置き、. ファイル で読み込む。マクロのブックは使いません。
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)。
学ぶこと: 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 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 を表示する。
学ぶこと: 引数を受け取るスクリプトを書き、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%
pausemy-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 = 31 は順番だけで対応させるので、途中に余計な出力が 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 に書く例です。
学ぶこと: エラーを 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 つ目の呼び出しは、わざと失敗させています)。
学ぶこと: bat は入口だけにして、処理は ps に書く。PowerShell のバージョンを決める。実行ポリシーで止まらないようにする。
この step は Excel を使わずに練習します(見本の step8.ps1 は step6-sub.ps1 を呼んで Excel に書き込みます)。
bat から値を受け取るには for /f が要り、終了コードも取れなくなります(Step 6)。そこで、bat は起動するだけにして、値のやり取りは ps どうしで行います。
flowchart LR
user["ダブルクリック"] --> bat["bat(入口)"]
bat -->|"引数 %*"| main["本体の ps"]
main -->|"& で呼ぶ・戻り値を受け取る"| sub["処理の ps"]
main -.->|"終了コードだけ"| bat
bat が向いているのは「起動する」ことです。
- ダブルクリックで実行できます(.ps1 をダブルクリックすると、メモ帳で開くだけです)。
-ExecutionPolicy Bypassを付けて、実行ポリシーで止められずに動かせます。pwshかpowershellかを決めて起動できます。- 最後の
pauseで、結果を読むまでウィンドウを閉じずにおけます。 - タスクスケジューラやほかのツールから呼びやすい形です。
| 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 に書き込む流れを読む。
学ぶこと: 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 を参照)。