TL;DR —
Shellはプログラムを起動し、そのプログラムがまだ何もしないうちに即座に戻ります。 返してくるのはタスク ID(Double)であって、終了コードでも、出力でも、成功フラグでもありません。 そして次の行は、プログラムがまだ起動している最中に走ります。プログラムを待つ、あるいは生み出した ものを読む必要があるなら、Shellは間違った道具です —WScript.Shell(待機付きの.Run、または.Exec)を使いましょう。
Sub ShellDemo()
Dim taskId As Double
taskId = Shell("notepad.exe", vbNormalFocus) ' タスク ID を返し、そのまま先へ進む
Debug.Print taskId ' 例: 4180 — 終了コードでは「ない」
' この下の行は、Notepad がまだ開いている最中に「今」実行される
End Sub
Shell は、Excel の外に手を伸ばして別の Windows プログラムを起動する、VBA でただ一つの動詞です。
厄介なのは、その振る舞いがマクロの他の部分とまるで違うことです。他の場所ではどこでも、一つの行が
仕事を終えてから次の行が走り、失敗すれば大きな音を立ててエラーが上がります。Shell はその両方の
習慣を破ります。プログラムを起動した瞬間に戻り、しかもそのプログラムがうまくいったかどうかを
ほとんど何も教えてくれません。これを撃ちっぱなしだと思い描けたとたん、あの不可解なバグ —
「コンバーターが書き終える前に、コードがファイルを開いてしまった」 — は不可解ではなくなります。
この記事で学べること
- 考え方の軸 —
Shellは撃ちっぱなし:起動したら、すぐに戻る - 戻り値が終了コードでもプログラムの出力でもなく、タスク ID である理由
Shellの次の行が早すぎるタイミングで走る理由と、プログラムを実際に待つ方法- 空白を含むパスを引用符で囲む方法と、
Shell単独では PDF や URL を開けない理由 Shellを捨ててWScript.Shellの.Run(待機+終了コード)や.Exec(出力の取得)に移るべき場面- プログラムが見つからないときに
Shellが出すエラーと、その防ぎ方
考え方の軸:Shell は撃ちっぱなし
Shell のあらゆる驚きを説明するたった一つの考えはこれです。Shell はプログラムを実行しない —
起動して、立ち去る。 Windows に実行ファイルの起動を頼み、新しいタスクを識別する番号を受け取り、
すぐにあなたのマクロへ戻ります。プログラムはいまや、あなたのコードの中ではなく、コードと並行して
動いています。
Shell "calc.exe" ' Windows が電卓を起動し...
MsgBox "done" ' ...この MsgBox は即座に現れる。電卓はまだ読み込み中
Range("A1").Copy のような普通のメソッド呼び出しと比べてみてください。あちらは次の行が走る瞬間には
もう終わっています。Shell は正反対で、非同期です。あなたのマクロと起動されたプログラムは、
それぞれ別の道を行きます。以下のルールはどれも、このたった一つの事実の帰結です。
第 2 引数はウィンドウスタイルです — vbNormalFocus、vbMinimizedNoFocus、vbHide など。これは
新しいウィンドウの見え方を制御するだけで、Shell を待たせることはありません。Shell を
同期的にするウィンドウスタイルは存在しません。
Shell が返すのは結果ではなくタスク ID
Shell は Double を返します — Windows が新しいプログラムに割り当てたプロセス(タスク)ID
です。人はこの番号を、終了コードや「うまくいったか」のフラグだと思って手に取りますが、そのどちらでも
ありません。
Dim result As Double
result = Shell("robocopy.exe C:\a C:\b", vbHide)
' result は 7820 のようなタスク ID — robocopy が成功したかどうかは何も語らない
呼び出しはプログラムが仕事を終える前に戻るので、報告すべき意味のある結果はまだ何もありません。
Shell はプログラムの終了コードを渡せず、コンソール出力を取り込めず、プログラムがあとでクラッシュ
したかどうかも教えてくれません。タスク ID が役に立つのはハンドルとしてだけ — たとえば、待つと決めた
なら OpenProcess/WaitForSingleObject のような Windows API に渡すことができます。あなたのロジックが
プログラムの返したものに依存しているなら、Shell は構造的にそれを供給できず、Shell の周りに
いくらコードを足しても変わりません。
次の行が早すぎるタイミングで実行される理由
これが目玉のバグで、撃ちっぱなしから直接導かれます。ファイルを生み出すツールを起動し、その ファイルをすぐに使う。
Shell "C:\Tools\convert.exe report.docx report.pdf"
Workbooks.Open "C:\Tools\report.pdf" ' 失敗する — PDF はまだ存在しない
convert.exe は動くのに 1、2 秒かかりますが、Workbooks.Open は今走ります — コンバーターが
ようやく起動しかけているそのときに。ファイルは存在せず、あなたは間違った行を指す「ファイルが
見つかりません」エラーを受け取ります — 本当の原因はそのすぐ上、待たなかった Shell にあります。
直し方は、固定した Application.Wait や Sleep の当て推量ではありません — 短すぎればやはり
失敗し、長すぎればユーザーの時間を無駄にし、どちらにせよ遅いマシンでは壊れます。プログラムが終わる
まで本当に待つには、Shell 以外の何かが要ります。きれいな答えは、待機フラグ付きの
WScript.Shell.Run(この下)。低レベルな答えは、タスク ID に対する WaitForSingleObject API です。
やめるべきなのは、Sleep 2000 をあちこちに撒いて祈ることです。
空白を含むパスと、exe ではなく文書を開く方法
ほとんど誰もがはまる、実践的な罠が 2 つあります。
パスの中の空白。 Shell は 1 本のコマンドライン文字列を取り、空白がプログラムと引数を区切り
ます。だから C:\Program Files\... のようなパスは、間違ったところで分割されます。
Shell "C:\Program Files\App\app.exe" ' error 53 — Windows は「C:\Program」を探す
Shell """C:\Program Files\App\app.exe""" ' 正しい — exe のパスを引用符で囲む
VBA 文字列の中では、"" の 1 組がリテラルの引用符 1 つになるので、"""...""" はパスの周りに本物の
引用符を置きます。フォルダーに空白が含まれうるなら(たいていは含まれます)、実行ファイルは必ず
引用符で囲みましょう。
プログラムではなく文書を開く。 Shell が起動するのは実行ファイルです — .pdf を PDF
リーダーで開くべきだとか、.xlsx を Excel で開くべきだといったことは知りません。文書を指させば
失敗します。
Shell "C:\Reports\Q1.pdf" ' error 53 — 実行ファイルではない
Shell "cmd /c start """" ""C:\Reports\Q1.pdf""" ' 動く — 既定のアプリはシェルに解決させる
start コマンド(cmd /c 経由)は、そのファイルに登録されているプログラム — ダブルクリックと同じ
もの — でファイルを開くよう Windows に頼みます。start の後ろの空の "" はウィンドウタイトルの
プレースホルダーで、パスが引用符で囲まれているとき start が要求します。文書や URL を開くには、
この方法がよく使われます。より整った代替は ShellExecute API か WScript.Shell で、次に扱います。
本当に結果が必要なとき:WScript.Shell
プログラムを待つ、終了コードを読む、あるいは出力を取り込む必要が生じた瞬間、Shell から
一段上がって、CreateObject で生成する WScript.Shell オブジェクトへ
移ります。
Dim sh As Object
Set sh = CreateObject("WScript.Shell")
' .Run — 第 3 引数の True は、プログラムが終了するまで「待つ」という意味。
' そのとき戻り値は本物の終了コードになる。
Dim exitCode As Long
exitCode = sh.Run("robocopy.exe C:\a C:\b", 0, True) ' 0 = 非表示ウィンドウ
If exitCode >= 8 Then MsgBox "robocopy failed: " & exitCode
' .Exec — 実行して、プログラムの標準出力テキストを読む
Dim p As Object, output As String
Set p = sh.Exec("cmd /c dir C:\")
output = p.StdOut.ReadAll
.Run(command, windowStyle, waitOnReturn) は、待って終了コードを得たいときの、そのまま差し替え
られるアップグレードです。.Exec はさらに進んで、StdOut を読める生きたプロセスオブジェクトを
渡します — コンソールプログラムのテキストを Excel に引き戻す唯一の方法です。コマンドのパスは
Environ から組み立ててどのマシンでも動くようにし、全体を
エラー処理で包んで、プログラムが見つからないときにクラッシュではなく
きれいに報告されるようにしましょう。
率直な結論:撃ちっぱなしは Shell、それ以外は WScript.Shell
Shell は一芸の動詞で、その芸は狭い — プログラムを起動して、忘れる。まさにそのためには本物で
役に立ちます — ビューアーを立ち上げる、フォルダーを開く、追跡する必要のない長時間実行のツールを
起動する。バグは、Shell が一度も約束していないことを求めるところから来ます。4 つのルールで全体を
覆えます。
- 撃ちっぱなし →
Shellはすぐに戻り、プログラムは並行して動く。待たせるウィンドウスタイルは 存在しない。 - 戻り値は結果ではなくタスク ID → 終了コードも、出力も、成功フラグもない。ロジックがプログラムの
結果を必要とするなら、
Shellは間違った道具。 - 空白を含むパスは引用符で囲む。文書は実行ファイルではない → exe は
"""..."""で囲む。文書はShellで直接ではなく、cmd /c startかShellExecuteで開く。 - 待つ、あるいは出力を読む必要がある?
WScript.Shellを使う →.Run(..., True)は待って 終了コードを返し、.ExecはStdOutを取り込む。Shellの沈黙が問題になった瞬間に手を伸ばす。
道具を仕事に合わせれば、「プログラムが終わる前にファイルを開いてしまった」種類のバグは消えます — あなたは、待って確認する仕事を、撃ちっぱなしのランチャーでやろうとしていたのです。
ExcelMaster の活用
本当に時間を奪う Shell のバグは、タイミングのバグです。起動したプログラムがまだ書き出していない
ファイルを開くマクロ、自分のマシンでは動くのに遅いマシンでは失敗する Sleep 2000、Shell が何も
報告しないせいで失敗が見えないコンバーター。どれも、実際には待って確認する必要があった場面で、
撃ちっぱなしの動詞を使ったところから生まれます。
ExcelMaster は、起動して待つ
ロジックを正しく書きます。やりたいことを説明すれば — 「このコンバーターを実行してから、その出力を
開いて」、あるいは「このコマンドラインツールを呼んで、失敗したかどうか教えて」 — 適切な仕組みを
選びます。撃ちっぱなしで本当に構わないなら Shell、完了を待つ・終了コードを読む・出力を取り込む
必要があるなら WScript.Shell.Run/.Exec。あなたは動かしたいプログラムを説明するだけ。それを
動かし、しかもいつ終わったかを知っているコードを書くのは、それがやります。
よくある質問
VBA の Shell でプログラムの終了を待つには?
Shell 自体は待てません — つねにすぐに戻ります。待つには、代わりに WScript.Shell オブジェクトを
使います:CreateObject("WScript.Shell").Run(command, windowStyle, True)。第 3 引数の True
(waitOnReturn)が、プログラムが終了するまでブロックさせ、そのとき戻り値がプログラムの本物の
終了コードになります。もう一つは Windows API による方法 — Shell が返すタスク ID を
WaitForSingleObject に渡す — ですが、日常づかいには WScript.Shell.Run のほうがはるかに簡単です。
VBA の Shell 関数は何を返しますか?
Shell は Double を返します — Windows が新しく起動したプログラムに割り当てたタスク(プロセス)
ID です。終了コードでも、プログラムの出力でも、成功フラグでもありません。Shell はプログラムが
終わる前に戻るので、報告すべき結果はまだ存在しないのです。タスク ID は、Windows API に渡せる
ハンドルにすぎません。プログラムの終了コードが必要なら、代わりに WScript.Shell.Run(..., True) を
使いましょう。
VBA の Shell が error 53(ファイルが見つかりません)を出すのはなぜですか?
よくある原因は 2 つです。第一に、パスに空白が含まれ、引用符で囲まれていない場合 —
Shell "C:\Program Files\..." は空白で分割されるので、Windows は C:\Program を探します。実行
ファイルのパスを二重の引用符で囲みましょう:Shell """C:\Program Files\App\app.exe"""。第二に、
Shell を実行ファイルではなく文書(.pdf や .xlsx)に向けた場合 — Shell が起動するのは
プログラムであって、ファイルではありません。文書を既定のアプリで開くには、
Shell "cmd /c start """" ""C:\file.pdf""" か ShellExecute API を使います。
VBA からバッチファイルやコマンドラインのコマンドを実行するには?
.bat ファイルなら、Shell が直接起動できます:Shell "C:\scripts\build.bat"(パスに空白があれば
引用符で囲む)。実行ファイルではない生のコマンド — dir、copy、パイプなど — は、コマンド
インタープリター経由で実行します:Shell "cmd /c copy a.txt b.txt"。終了コードや出力を待って読む
必要があるなら、Shell ではなく CreateObject("WScript.Shell").Run("cmd /c ...", 0, True) か
.Exec を使いましょう。
VBA から実行したプログラムの出力を取り込むには?
Shell は出力をまったく取り込めません。WScript.Shell オブジェクトの .Exec メソッドを使いましょう。
読み取れる StdOut ストリームを持つプロセスオブジェクトを返します:
Set p = CreateObject("WScript.Shell").Exec("cmd /c dir C:\") : output = p.StdOut.ReadAll。これが、
コンソールプログラムのテキストを VBA に引き戻す標準的なやり方です。終了コードでしか成功を知らせない
プログラムなら、代わりに .Run(command, 0, True) を使って戻り値を調べます。
検証環境
検証環境: Excel 365 (Windows 11), VBA 7.1 — 最終確認 2026-08-30。
関連ガイド: VBA CreateObject・GetObject — 遅延バインディングと COM 自動化 · VBA Environ — 環境変数を読む · VBA Wait・Sleep — 処理を一時停止する · VBA On Error — エラー処理 · VBA FreeFile・Open — ファイル番号とテキストファイル
