2010年5月23日日曜日

EXCELでDOS コマンドの実行結果を取得する方法

http://www.f3.dion.ne.jp/~element/msaccess/AcTipsGetDosResult.html

DOS コマンドの実行結果を取得する方法


概要

Access VBA と DOS コマンド。

昨今ではあまり馴染みのない組み合わせです。

主要 DOS コマンドは VBA に移植されていますし、VBA に移植されていないものは API や COM の Shell オブジェクト経由で呼べるので、「なんで今さら DOS コマンド?」という感覚が一般的かもしれません。

しかし、中には DOS コマンドの方が簡単に実行できるケースも有ります。

たとえば ping を打ってネットワーク上の任意のホストが生きているか確認したい場合や、あるフォルダの配下から、サブフォルダの中も含めて、特定の拡張子を持つファイルの一覧を取得したい場合などが該当します。

これらはいずれも VBA で実装可能ではありますが、複雑な再帰処理や API との連携が必要です。

一方 CUI であるコマンド インタープリタ上からであれば、たった 1 行の DOS コマンドを入力するだけで実行できます。

しかし OS 標準のコマンド インタープリタ(9x 系は Command.com、NT 系は CMD.exe)はオートメーションに対応していないので、DOS コマンドの実行結果を直接取得できないという欠点もあります。

結果をいったんリダイレクトしてテキストファイルに書き出し、そのファイルを読み込んで削除すれば結果を取得出来なくもないのですが、いかんせん、どうしようもなくダサいという印象は拭いきれません。

とは言っても、一度知ってしまえば DOS コマンドの簡便さを捨てがたいのもまた事実です。

ここではそのジレンマを解消すべく、Shell オブジェクトを経由することで DOS コマンドの実行結果を直接取得する方法を提示します。

※ 本トピックは DOS コマンドそのものについては解説しません。興味の有る方は、Web 上のリソースやヘルプを当たってください。

基本

まずは、基本を確認することにしましょう。

ご存知の方は詳細まで飛ばしてください。

実行

Access VBA から DOS コマンドを実行するだけで良く、結果を取得する必要が無い場合は、Shell 関数を使います。

コマンド インタープリタの実行ファイル名は OS によって異なるため、以下のような使い分けが必要です。

' 9x 系 OS の場合 Call Shell("Command.com <コマンド>")  ' NT 系 OS の場合 Call Shell("CMD.exe <コマンド>")

これでは OS によって構文を変える必要が出てきてしまいますが、コマンド インタープリタの名称は環境変数 ComSpec から取得できるため、以下のような記述で汎用化することが出来ます。

' 9x 系/NT 系 OS 共用の記述 Call Shell(Environ$("ComSpec") & " <コマンド>")

さらに、NT 系 OS で実行後にコマンド プロンプトのウィンドウを自動的に閉じさせたい場合は、オプションスイッチ /c を付ける必要が有ります。

したがって最終系は以下の通りです。

' 9x 系/NT 系 OS 共用の記述(ウィンドウを自動的に閉じる) Call Shell(Environ$("ComSpec") & " /c <コマンド>")

逆に、9x 系 OS で実行後もコマンド プロンプトのウィンドウを開いておきたい場合は、オプションスイッチ /k を付ける必要が有ります。

' 9x 系/NT 系 OS 共用の記述(ウィンドウを自動的に閉じない) Call Shell(Environ$("ComSpec") & " /k <コマンド>")

パスの処理

たとえば C:\TEMP にあるファイル一覧を表示したい場合は次のように記述します。

sPath = "C:\TEMP" Call Shell(Environ$("ComSpec") & " /k dir " & sPath, vbNormalFocus)

しかし、C:\Documents and Settings\YU-TANG\デスクトップ にあるファイル一覧を表示させるための以下の記述は失敗します。

sPath = "C:\Documents and Settings\YU-TANG\デスクトップ" Call Shell(Environ$("ComSpec") & " /k dir " & sPath, vbNormalFocus)

これは途中のフォルダ名「Documents and Settings」中のスペース文字がコマンドラインの区切り文字として認識されるために発生します。

言い換えるなら、コマンド インタープリタはこれを「"C:\Documents and Settings\YU-TANG\デスクトップ" のファイル一覧を表示せよ」という命令ではなく、「"C:\Documents" と "and" と "Settings\YU-TANG\デスクトップ" のファイル一覧を表示せよ」という命令として解釈します。

C:\Documents などというフォルダは(偶然それを作成でもしていない限り)見つからないので、失敗します。

コマンド プロンプト(9x 系 OS では DOS プロンプト)上であれば、パスの前後を二重引用符で括ることによって回避できます。

C:\> dir "C:\Documents and Settings\YU-TANG\デスクトップ"

VBA 上では既にコマンドライン自体が文字列リテラルであることを示す二重引用符で括られているため、その中でさらに二重引用符記号自体を表現する場合は、下記のように二重引用符を 2 回重ねる必要が有ります。

sPath = """C:\Documents and Settings\YU-TANG\デスクトップ""" Call Shell(Environ$("ComSpec") & " /k dir " & sPath, vbNormalFocus)

この記述は慣れると入力速度も速く簡便なのですが、最初のうちは混乱するかもしれません。

その場合は以下のような記述でも、最終的に同じ文字列を生成できます。

sPath = Chr$(34) & "C:\Documents and Settings\YU-TANG\デスクトップ" & Chr$(34) Call Shell(Environ$("ComSpec") & " /k dir " & sPath, vbNormalFocus)

なお、パスの括り方には 1 つだけ例外が有ります。

start コマンドは、ファイルのパスを指定するだけで拡張子に応じたアプリケーションを起動してくれる DOS コマンドです。

現在では、拡張子に応じた自動起動は ShellExecute API を使用するのが一般的ですが、API 宣言不要なので私はいまだに start コマンドを使う傾向があります。

これもやはり DOS コマンドなので、パス中のスペース文字が問題になります。

しかし、下記のように単純にパスの前後を二重引用符で括るだけでは、実際には失敗します。

' 以下のコードは正常動作しません。 sPath = """C:\Documents and Settings\YU-TANG\デスクトップ\Book1.xls""" Call Shell(Environ$("ComSpec") & " /c start " & sPath)

正解は、下記のようにスペース文字を含むディレクトリ名あるいはファイル名だけを、二重引用符で括ります。

' 以下のコードは正常動作します。 sPath = "C:\""Documents and Settings""\YU-TANG\デスクトップ\Book1.xls" Call Shell(Environ$("ComSpec") & " /c start " & sPath)

処理完了を待機する

Shell 関数は非同期実行です。

そのため、VBA のインタープリタは、Shell 関数によって起動されたプロセスが完了したかどうかにはお構いなしに、次のステートメントに進んでしまいます。

それが、ときに問題を引き起こす場合があります。

以下はバックアップ フォルダ内のファイルおよびサブディレクトリをローカルの一時フォルダ内にコピーし、db1.mdb を起動するコードです。

Call Shell(Environ$("ComSpec") & " /c XCOPY C:\Bkup %temp%\Bkup") Call Shell("MSACCESS.EXE " & Environ$("temp") & "\Bkup\db1.mdb", vbNormalFocus)

このコードの実行は保証されません。

運が良ければ成功するかもしれませんし、失敗するかもしれません。

もし db1.mdb をコピーし終わる前に起動しようとすれば、当然失敗するでしょう。

このような場合は、最初の Shell 関数によって起動されたプロセスが完了するまで、処理を待機させる必要があります。

かつては API を使って待機したり、必要なファイルが作成されたかどうかをループでチェックするなどの方法が取られていました。

Windows Scripting Host が登場して数年が経過した現在では、その簡便さから WshShell オブジェクトの Run メソッドを使用する方法に主流が移っています。

次のコードは、XCOPY コマンドによるコピーが完了するのを待機してから、db1.mdb を起動します。

Dim oShell As Object Set oShell = CreateObject("WScript.Shell") oShell.Run "%ComSpec% /c XCOPY C:\Bkup %temp%\Bkup", , True Set oShell = Nothing Call Shell("MSACCESS.EXE " & Environ$("temp") & "\Bkup\db1.mdb", vbNormalFocus)

Run メソッドの第 3 引数 WaitOnReturn に True を指定することによって、同期実行(プロセスの完了を待機)になります。

また Run メソッドのコマンド中では環境変数が使えるので、Environ$ 関数を使わずに直接「%ComSpec%」でコマンドインタープリタを指定しています。

あまり行儀は良くありませんが、次のように簡略化することも可能ではあります。

CreateObject("WScript.Shell").Run _     "%ComSpec% /c XCOPY C:\Bkup %temp%\Bkup", , True Call Shell("MSACCESS.EXE " & Environ$("temp") & "\Bkup\db1.mdb", vbNormalFocus)

詳細

さて、本題です。

DOS コマンドの実行結果をダイレクトに取得するには、Windows Scripting Host ライブラリの WshShell オブジェクトの Exec メソッドを使います。

標準モジュールに以下のような汎用関数を用意しましょう。

  1. ' 関数名:ExecCommand
  2. ' 目 的:DOS コマンドの実行結果を取得します。
  3. ' 戻り値:エラーの有無を Boolean 型で返します。
  4. '     エラー発生時は True、正常終了時は False です。
  5. ' 引 数:sCommand-> 必須/入力用です。実行コマンドを文字列型で渡します。
  6. '     sResult -> 必須/出力用です。実行結果を文字列型で受け取ります。
  7. '          失敗した場合はエラー内容を示します。
  8. ' 注 意:実行中はコマンドプロンプト ウィンドウが開きます。
  9. '     また実行後は自動的にウィンドウが閉じます。
  10. Public Function ExecCommand(sCommand As String, sResult As String) _
  11. As Boolean
  12. ' 変数宣言部
  13. Dim oShell As Object, oExec As Object
  14. ' オブジェクト変数に参照をセットします。
  15. Set oShell = CreateObject("WScript.Shell")
  16. Set oExec = oShell.Exec("%ComSpec% /c " & sCommand)
  17. ' 処理完了を待機します。
  18. Do Until oExec.status: DoEvents: Loop
  19. ' 戻り値をセットします。
  20. If Not oExec.StdErr.AtEndOfStream Then
  21. ExecCommand = True
  22. sResult = oExec.StdErr.ReadAll
  23. ElseIf Not oExec.StdOut.AtEndOfStream Then
  24. sResult = oExec.StdOut.ReadAll
  25. End If
  26. ' オブジェクト変数の参照を解放します。
  27. Set oExec = Nothing: Set oShell = Nothing
  28. End Function

Exec メソッドによって取得できるWshScriptExec オブジェクトから、StdIn、StdOut、および StdErr の各ストリームへアクセスすることが出来ます。

ということは、言い換えればコマンド プロンプトをリモート操作できる、ということです。

では、幾つか実例をば。

あるフォルダ内の特定の拡張子のファイル一覧を取得する(Dir コマンド)

これには下記のような様々な手法が存在します。

  1. VBA ライブラリの Dir/Dir$ 関数
  2. Windows API の FindFirstFile、FindNextFile 関数
  3. Windows Scriting Host ライブラリの Files コレクション
  4. Office ライブラリの FileSearch オブジェクト
  5. WMI のサービスプロバイダ
  6. DOS の Dir コマンド

他にも有るかもしれません。

どれを使っても構いませんが、一瞬コマンド プロンプトが表示されることさえ許容できれば、Dir コマンドが一番簡単だったりします。

Dir コマンドはファイル一覧を列挙するコマンドですが、オプションを指定することによって様々な条件に対応させることが可能です。

以下は、あるディレクトリの配下から、拡張子が .xls のファイルを検索し、一覧をフルパスで返します。

Dim sCmd As String, sRet As String sCmd = "dir ""C:\Documents and Settings\YU-TANG\*.xls"" /b /s" Debug.Print IIf(ExecCommand(sCmd, sRet), "【エラー内容】:", "【実行結果】:") Debug.Print sRet

オプションスイッチ /b は、ファイル名のみを返すためのスイッチで、これを指定しないと見出しや要約が付きます。今回は見出しや要約が不要なので、/b を指定します。

またオプションスイッチ /s を付けることで、サブディレクトリも検索対象になります。

Access 2000 以降であれば、Split 関数を使うことによって簡単に結果を配列に分割することが出来ます。

ネットワーク内の特定ホストが生きているか確認する(Ping コマンド)

Ping を VBA で実装するためにこのページをご覧になる方が多いようなので、補足します。

この章は、役には立たないが興味深い DOS コマンドとの連携の単なるネタとして Ping を引き合いに出しているに過ぎません。

業務で真剣に方法を調べている方は、このページを飛ばして以下のリンク先を参照してください。

Accessむかむか - PINGを実行する

TechNet スクリプトセンター > リモート コンピュータが使用可能かどうかの調査

では、ムダ知識でヒマをつぶしたい方は、続きをどうぞ。

Ping コマンド同等の処理は API でも実装可能ですが、非常に複雑なコードになりがちです。

以下は、Ping コマンドの結果を返します。

Dim sCmd As String, sRet As String ' IP アドレスの場合 sCmd = "ping 123.0.0.99" ' 下記のようにホスト名でも可 sCmd = "ping hostname" Debug.Print IIf(ExecCommand(sCmd, sRet), "【エラー内容】:", "【実行結果】:") Debug.Print sRet

結果は単なる文字列なので、必要であれば文字列操作関数で目的の情報だけ切り出してもいいでしょう。

別ドメイン内の共有資源に接続する(Net Use コマンド)

複数のドメインが存在するネットワーク内での処理では、しばしば目的のホストや共有フォルダに実行環境が接続していないために問題が発生する場合があります。

たとえば別ドメインのホスト内の MDB ファイルからリンクテーブルが作成されていても、実行環境が接続していないためにリンクテーブルの内容を参照できなかったり、プリンタサーバに接続していないためにレポートの印刷が失敗したり、といったケースが想定されます。

この問題を回避するためには事前に目的の資源に接続すればよいわけですが、これも方法は一種類だけではなく、Net Use コマンド以外に WNetAddConnection 系 API(WNetAddConnection、WNetAddConnection2、WNetAddConnection3)や WSH の WScript.Network オブジェクトを使用する方法があります。

Net Use コマンドを使うのは、単に簡単だからです。

以下は、Net Use コマンドを使ってプリンタサーバ PrtSrv に接続し、実行結果を返します。

またその際ドメイン Domain にユーザー UID としてパスワード Password でログインし、接続は一時的なものとします。

Dim sCmd As String, sRet As String sCmd = "net use \\PrtSrv\IPC$ /u:Domain\UID Password /PERSISTENT:NO" ' sCmd = "net use \\PrtSrv\IPC$ /DELETE"  ' ← 切断する場合 Debug.Print IIf(ExecCommand(sCmd, sRet), "【エラー内容】:", "【実行結果】:") Debug.Print sRet

まとめ

VBA で DOS コマンドを使おうなんて発想は、明らかに時流に逆行しています。

コマンドプロンプト ウィンドウが前面に出てくるようでは、製品化を前提としたアプリ開発なんかじゃ、とても使えません。

ただ、API だの COM だのを組み合わせなくても、とにかくコマンド 1 行で簡単に呼び出せるお手軽さが最大の魅力なので、個人用の趣味 DB や業務用の内作アプリであれば、選択肢としてアリかな、というのが YU-TANG の感想です。

付録

概要で述べたように、今までは DOS コマンドの実行結果をリダイレクトして読み込むという方法が主流でした。

今回は標準入出力からダイレクトに読み込む方法を紹介しましたが、既存の方法だとコマンドプロンプト ウィンドウを表示しないで済むというメリットがあるため、むしろそちらの古い方法を知りたいという方もいらっしゃるかもしれません。

そこで、付録としてリダイレクトのサンプルも掲載しておきます。これは特に珍しいものではありません。

簡易版で、特にエラートラップもしていません。適宜改変してお使いください。

  1. ' 関数名:ExecCommand2
  2. ' 作成日:2004/10/5
  3. ' 作成者:YU-TANG@http://www.f3.dion.ne.jp/~element/msaccess/
  4. ' 目 的:DOS コマンドの実行結果を取得します。
  5. ' 戻り値:DOS コマンドの実行結果を String 型で返します。
  6. '     コマンドが正常に実行できた場合はその内容が、エラーが発生した
  7. '     場合はそのエラー内容が返ります。
  8. ' 引 数:sCommand-> 必須/入力用です。実行コマンドを文字列型で渡します。
  9. ' 注 意:実行中はコマンドプロンプト ウィンドウは表示されません。アイコン
  10. '     もタスクバーに表示されません。また DOS コマンドは同期実行され
  11. '     ます。したがってコマンドの実行が完了するまで制御は戻りません。
  12. Public Function ExecCommand2(sCommand As String) As String
  13. ' 定数/変数宣言部
  14. Const TemporaryFolder = 2
  15. Dim oShell As Object, fso As Object, fdr As Object, ts As Object
  16. Dim sFileName As String
  17. ' オブジェクト変数に参照をセットします。
  18. Set oShell = CreateObject("WScript.Shell")
  19. Set fso = CreateObject("Scripting.FileSystemObject")
  20. Set fdr = fso.GetSpecialFolder(TemporaryFolder)
  21. ' リダイレクト先のファイル名を生成します。
  22. Do: sFileName = fso.BuildPath(fdr.Path, fso.GetTempName)
  23. Loop While fso.FileExists(sFileName)
  24. ' コマンドを実行します。
  25. oShell.Run "%ComSpec% /c " & sCommand & ">" & sFileName & " 2<&1" _
  26. , 0, True
  27. ' 戻り値をセットします。
  28. If fso.FileExists(sFileName) Then
  29. Set ts = fso.OpenTextFile(sFileName)
  30. ExecCommand2 = ts.ReadAll
  31. ts.Close
  32. Kill sFileName
  33. End If
  34. ' オブジェクト変数の参照を解放します。
  35. Set ts = Nothing: Set fdr = Nothing
  36. Set fso = Nothing: Set oShell = Nothing
  37. End Function

この関数は、実行結果を戻り値に返すだけの単純な作りになっています。

現状ではエラーが発生したかどうかは戻り値の内容で判断するしかありませんが、StdErr のリダイレクト先を振り分けるように改変すれば、比較的簡単に真偽の判定が可能になるでしょう。

コード中では Scripting Runtime 関連オブジェクトを作成していますので、何らかの理由で本関数を連続して呼び出す可能性がある場合は、クラスモジュール化した方が実行効率は上がることが予想されます。

使用例は以下の通りです。

Debug.Print ExecCommand2("ping YU-TANG")

0 件のコメント: