質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.99%

VBAからPowershellスクリプトを完全非表示、非同期、引数付で実行して、尚且つ 返値を受け取りたい

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 6,844

kamikazelight

score 168

 前提・実現したいこと

VBAからpowershellスクリプトを利用したいです。
尚且つ 非同期で実行したいです。

ホントは引数や返値で配列、もっと欲を言えばオブジェクトをやり取りしたいところですが
引数はVBA側で自身の最新状態のコピーを作成してpowershell側でそれを別プロセスで開いてアクセスすればいけそう。。。
返値もpowershell側でExcelを開ければVBA側でオブジェクトを特定できる文字列が返せる気がするので
後回しにします...(でもいい方法があれば知りたいです!)

powershell非表示で実行で調べるとWshのRunを使って非表示で実行する方法が出てくるのですが
返値がテキストファイルに書き出して読み込みとかしないと受け取れなかったりとか
Exec ならステータスの確認が出来たり, stdin なるもの が何かに使えそうだと思ったので
今のところはそちらを使いたいと考えています。(もっといい方法があればためらいなく乗り換えます。)

 試したこと

とりあえず powershellのウィンドウが一瞬表示されてしまうのは
諦めて作成してみました。
windows script host object modelを参照設定しています。

使用例ではStdOut.ReadAllを実行した際 処理が終わってなければ勝手に待機してくれることが分かったので
Statusの確認はしていません。

Run等で実行した後にプロセスを取得できないか?等といろいろ探してみたのですがいい方法が見つけられませんでした。

何か良い手はないのでしょうか

作成したコード

Function MyPowershell(Optional ByVal ScriptName As String, Optional ByVal FunctionName As String, Optional Argument As String, Optional Exec As WshExec, Optional ByVal StdinClose As Boolean = True) As WshExec
'Powershellスクリプトを実行してWshExecオブジェクトとして返す

  Dim Wsh As New WshShell
    If StdinClose And FunctionName <> "" And Exec Is Nothing Then
      '処理一括実行
      Dim Cmd As String
        If ScriptName <> "" Then
            Cmd = ". '" & ScriptName & "';"
        End If
        Set Exec = Wsh.Exec("powershell -NoLogo -ExecutionPolicy RemoteSigned -windowstyle hidden -command " & "Set-Location -Path('" & ThisWorkbook.Path & "');" & Cmd & FunctionName & " " & Argument)
    Else
      '処理随時実行
        If Exec Is Nothing Then
            Set Exec = Wsh.Exec("powershell -NoLogo -ExecutionPolicy RemoteSigned -windowstyle hidden")
        End If
        Call Exec.StdIn.WriteLine("Set-Location -Path('" & ThisWorkbook.Path & "')")
        If ScriptName <> "" Then
            Call Exec.StdIn.WriteLine(". '" & ScriptName & "'")
        End If
        If FunctionName <> "" Then
            Call Exec.StdIn.WriteLine(FunctionName & " " & Argument)
        Else
            StdinClose = False
        End If
        If StdinClose Then
            Exec.StdIn.Close
        End If
    End If
        Set MyPowershell = Exec
End Function

Function MyPowershellStdOut(ByVal Exec As WshExec) As String
'Powershellの返値を余分な文字を削除して返す

  '標準出力を受け取る
  Dim Str As String
    Str = Exec.StdOut.ReadAll

  '除外対象の文字列の削除
  Dim BefStr As String
  Dim RegExp_ As New RegExp
    RegExp_.Pattern = "PS .:\\.+?\n"
    Do
        BefStr = Str
        Str = RegExp_.Replace(BefStr, "")
    Loop While (BefStr <> Str)
    RegExp_.Pattern = "PS .:\\.+?> $"
    Str = RegExp_.Replace(BefStr, "")
    BefStr = Str
    RegExp_.Pattern = "\n$"
    Str = RegExp_.Replace(BefStr, "")
    MyPowershellStdOut = Str
End Function

実行されるpowershellスクリプト

function test([String]$Str){
    Start-Sleep -s 3; write-host $Str
}

使用例

Sub test()
'LoopCountに設定されている数分同時実行を行う
'Boundary の指定以上は随時実行に変更

  Const LoopCount As Long = 10
  Const Boundary As Long = 5

  Dim Exec() As WshExec
  Dim i As Long
    ReDim Exec(LoopCount)
    For i = 0 To LoopCount
        If i <= Boundary Then
            Set Exec(i) = MyPowershell(".\t est.ps1", "test", """入力テスト" & i & "`r`n二行目の入力テスト""")
        Else
            Set Exec(i) = MyPowershell(".\t est.ps1")
        End If
    Next i

  '追加の実行
  Dim j As Long
    For j = 0 To LoopCount
        If j > Boundary Then
            Set Exec(j) = MyPowershell(, "test", """入力テスト" & j & "`r`n二行目の入力テスト""", Exec(j))
        End If

    Next j

  '結果の取得
  Dim Str() As String
  Dim k As Long
    ReDim Str(LoopCount)
    For k = 0 To LoopCount
        Str(k) = MyPowershellStdOut(Exec(k))
    Next k

  '結果の表示
  MsgBox Join(Str, vbCrLf)
End Sub
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+4

よく使われている(私が知っている)COMオブジェクトの範囲では、条件を完全に満たすものは無いと思われます。

ぱっと思いついた方法を簡単にまとめたので参考になれば。

 PowerShellの実行方法

 WshShell.Exec()で実行し、WshExecを受け取る

  • ウィンドウが一瞬表示される
  • 文字で表現出来る情報しか返せない

という点が許容できれば一番無難な方法。

 VBA.Shell()WshShell.Run()で終了を待たずに実行

ウィンドウを完全に非表示にしたい場合。
結果を受け取るには外部の何かを使う必要がある。

終了を待たない場合、WshShell.Run()を使う意味はほとんど無い。

 結果を受け取る方法

WshExecを使っている場合はそちらから読み取れば良いので割愛。

 ファイルに出力

基本文字列情報のやり取り。

ファイル名の生成にはFileSystemObject.GetTempName()を使う手も(到底一意とは言えませんが)。

 クリップボード経由で受け取り

ユーザー操作に影響を与える割に扱いにくい。

 PowerShellから任意のVBAプロシージャをコールバック

非常にデバッグが困難、お互いの結合が強くなりますが一つの手として。

Excel・PowerPoint・Wordなど一部のVBAホストは、COM操作で外部から任意のVBAのプロシージャを実行出来る機能を提供しています。

PowerShellはCOM操作ができるため、上記の機能でVBAに直接情報を送ることが出来ます。

やり取りとしては以下のようなパターンが考えられます。

  • 直接VBAの処理を呼ぶ(お互いの依存がより強くなる)
  • 適当なSetterを定義しておき、PowerShellはSetter経由で情報を渡す。あとはVBAが適当なタイミングで取りに行く(外乱の影響が大きい)

なお、標準コマンドレットではExcel・Wordの既存のインスタンスを取得できないため、.NETのメソッドを直接呼び出す必要があります(PowerPointはシングルインスタンスなのでOK)。

 処理の中断方法

安全性を度外視するならPowerShellプロセスの終了が手っ取り早いでしょう。

WshShell.ExecならTerminate メソッド (WshScriptExec)が使用できます。

Terminate メソッド (WshScriptExec)
Terminate メソッドは最後の手段としてのみ使用します。

WshShell.Runから実行した場合は不可能ですが、VBA.Shellなら返り値がプロセスIDとなります。
そのため、WMIなどを使えば該当プロセスの取得・終了ができるはずです。

プロセスの終了以外となるとPowerShell側からVBAに何か状況を問い合わせる必要があります。


VBAプロシージャコールバックのサンプル(Excel用)

PowerShellを10個起動して、コールバックでそれぞれの番号をDebug.Printさせています。
AsyncPsExecTestを実行するとAsyncPsExecTest終了後にバラバラに結果が出力されるのがわかると思います。

Sub CallBack(a)
    Debug.Print a
End Sub

Sub AsyncPsExecTest()
    Const PS_CMD_BASE = _
            "PowerShell.exe -NoProfile -Command " & """" & _
                "$appXl = [Runtime.InteropServices.Marshal]::BindToMoniker('{0}').Application;" & _
                "$appXl.Run('{0}!CallBack' , {1} )" & """"

    Debug.Print "AsyncPsExecTest Start"

    Dim i As Long
    For i = 1 To 10
        Dim psCmd As String
        psCmd = VBA.Replace(VBA.Replace( _
            PS_CMD_BASE, _
                "{0}", ThisWorkbook.Name), _
                "{1}", i)

        Dim pId As Double
        pId = VBA.Shell(psCmd, vbHide)
        Debug.Print "ProcessID:="; pId, i
    Next i
    Debug.Print "AsyncPsExecTest End"
End Sub

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/08/11 16:26

    ごめんなさい
    何度やっても、いろいろ変えてみても コールバックが働かないです............

    キャンセル

  • 2018/08/11 16:35

    ShellのvbHideの指定を外す、-NoExitオプションを付けるなどしてコンソールを残すようにし、エラーが発生していないか確認してみてください

    非常に強引な方法のため、このようにデバッグが難しく、ある程度自己解決できないと難しいです

    キャンセル

  • 2018/08/13 10:50

    Add-Type -AssemblyName Microsoft.Visualbasic を入れなければならないのを見逃してました。
    まだ、確認途中ですがうまく行きそうです。
    今回もありがとうございました。

    キャンセル

-3

VBAから手軽にDOSコマンドやPowerShellを実行して結果を取得するモジュールを作成

ざっと読んだだけなので内容の保証はしかねます。
自分で確かめてみてください。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/08/13 14:27

    最初の一行だけ読みました。

    キャンセル

  • 2018/08/13 14:28

    あ、最後の二行も読みました。

    キャンセル

  • 2018/08/13 14:35

    読んでみました。先ほどから繰り返されているプログラムに関係のない無意味な主張が言葉を換えて書いてあるだけで、読む価値はありませんでした。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.99%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る

  • トップ
  • VBAに関する質問
  • VBAからPowershellスクリプトを完全非表示、非同期、引数付で実行して、尚且つ 返値を受け取りたい