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

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

新規登録して質問してみよう
ただいま回答率
85.49%
VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

PowerShell

Windows PowerShellはコマンドラインインターフェースであり、システム管理を含むWindowsタスク自動化のためのスクリプト言語です。

WSH

WSH(Windows Script Host)とは、Windows上でテキストファイルに記述したJavaScriptやVBScriptなどのスクリプトを実行するホスト環境のことです。COMを通じたレジストリ操作やWMIへのアクセスが可能で、複雑な処理も行うことができます。

Q&A

解決済

2回答

5676閲覧

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

kamikazelight

総合スコア305

VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

PowerShell

Windows PowerShellはコマンドラインインターフェースであり、システム管理を含むWindowsタスク自動化のためのスクリプト言語です。

WSH

WSH(Windows Script Host)とは、Windows上でテキストファイルに記述したJavaScriptやVBScriptなどのスクリプトを実行するホスト環境のことです。COMを通じたレジストリ操作やWMIへのアクセスが可能で、複雑な処理も行うことができます。

0グッド

1クリップ

投稿2018/08/10 07:15

編集2018/08/10 11:03

前提・実現したいこと

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

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

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

試したこと

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

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

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

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

作成したコード

vba

1Function 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 2'Powershellスクリプトを実行してWshExecオブジェクトとして返す 3 4 Dim Wsh As New WshShell 5 If StdinClose And FunctionName <> "" And Exec Is Nothing Then 6 '処理一括実行 7 Dim Cmd As String 8 If ScriptName <> "" Then 9 Cmd = ". '" & ScriptName & "';" 10 End If 11 Set Exec = Wsh.Exec("powershell -NoLogo -ExecutionPolicy RemoteSigned -windowstyle hidden -command " & "Set-Location -Path('" & ThisWorkbook.Path & "');" & Cmd & FunctionName & " " & Argument) 12 Else 13 '処理随時実行 14 If Exec Is Nothing Then 15 Set Exec = Wsh.Exec("powershell -NoLogo -ExecutionPolicy RemoteSigned -windowstyle hidden") 16 End If 17 Call Exec.StdIn.WriteLine("Set-Location -Path('" & ThisWorkbook.Path & "')") 18 If ScriptName <> "" Then 19 Call Exec.StdIn.WriteLine(". '" & ScriptName & "'") 20 End If 21 If FunctionName <> "" Then 22 Call Exec.StdIn.WriteLine(FunctionName & " " & Argument) 23 Else 24 StdinClose = False 25 End If 26 If StdinClose Then 27 Exec.StdIn.Close 28 End If 29 End If 30 Set MyPowershell = Exec 31End Function 32 33Function MyPowershellStdOut(ByVal Exec As WshExec) As String 34'Powershellの返値を余分な文字を削除して返す 35 36 '標準出力を受け取る 37 Dim Str As String 38 Str = Exec.StdOut.ReadAll 39 40 '除外対象の文字列の削除 41 Dim BefStr As String 42 Dim RegExp_ As New RegExp 43 RegExp_.Pattern = "PS .:\.+?\n" 44 Do 45 BefStr = Str 46 Str = RegExp_.Replace(BefStr, "") 47 Loop While (BefStr <> Str) 48 RegExp_.Pattern = "PS .:\.+?> $" 49 Str = RegExp_.Replace(BefStr, "") 50 BefStr = Str 51 RegExp_.Pattern = "\n$" 52 Str = RegExp_.Replace(BefStr, "") 53 MyPowershellStdOut = Str 54End Function

実行されるpowershellスクリプト

powershell

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

使用例

vba

1Sub test() 2'LoopCountに設定されている数分同時実行を行う 3'Boundary の指定以上は随時実行に変更 4 5 Const LoopCount As Long = 10 6 Const Boundary As Long = 5 7 8 Dim Exec() As WshExec 9 Dim i As Long 10 ReDim Exec(LoopCount) 11 For i = 0 To LoopCount 12 If i <= Boundary Then 13 Set Exec(i) = MyPowershell(".\t est.ps1", "test", """入力テスト" & i & "`r`n二行目の入力テスト""") 14 Else 15 Set Exec(i) = MyPowershell(".\t est.ps1") 16 End If 17 Next i 18 19 '追加の実行 20 Dim j As Long 21 For j = 0 To LoopCount 22 If j > Boundary Then 23 Set Exec(j) = MyPowershell(, "test", """入力テスト" & j & "`r`n二行目の入力テスト""", Exec(j)) 24 End If 25 26 Next j 27 28 '結果の取得 29 Dim Str() As String 30 Dim k As Long 31 ReDim Str(LoopCount) 32 For k = 0 To LoopCount 33 Str(k) = MyPowershellStdOut(Exec(k)) 34 Next k 35 36 '結果の表示 37 MsgBox Join(Str, vbCrLf) 38End Sub

気になる質問をクリップする

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

よく使われている(私が知っている)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終了後にバラバラに結果が出力されるのがわかると思います。

vba

1Sub CallBack(a) 2 Debug.Print a 3End Sub 4 5Sub AsyncPsExecTest() 6 Const PS_CMD_BASE = _ 7 "PowerShell.exe -NoProfile -Command " & """" & _ 8 "$appXl = [Runtime.InteropServices.Marshal]::BindToMoniker('{0}').Application;" & _ 9 "$appXl.Run('{0}!CallBack' , {1} )" & """" 10 11 Debug.Print "AsyncPsExecTest Start" 12 13 Dim i As Long 14 For i = 1 To 10 15 Dim psCmd As String 16 psCmd = VBA.Replace(VBA.Replace( _ 17 PS_CMD_BASE, _ 18 "{0}", ThisWorkbook.Name), _ 19 "{1}", i) 20 21 Dim pId As Double 22 pId = VBA.Shell(psCmd, vbHide) 23 Debug.Print "ProcessID:="; pId, i 24 Next i 25 Debug.Print "AsyncPsExecTest End" 26End Sub

投稿2018/08/11 04:13

編集2018/08/11 05:30
imihito

総合スコア2166

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kamikazelight

2018/08/11 04:52

>WshShell.Exec()で実行し、WshExecを受け取る 試したことに書いてあります。 クリップボードは思いつきませんでした。ですが上司の指示でExcel機能の履歴管理ができるクリップボードを使っているからクリップボードは使うなと言われていので使えないです。 >PowerShellから任意のVBAプロシージャをコールバック この場合非同期での利用は厳しいですよね?
imihito

2018/08/11 05:35

非同期でコールバックするサンプルを追加しました
kamikazelight

2018/08/11 05:59 編集

コールバックする方法を知らなかったので勉強になりました。 あとVBA.Shellなら非表示で実行出来る上にIDも取得できるのですね ただ、毎回powershellからの戻り値の処理内容と利用用途が違うので これだとそれぞれの実行結果を合わせて使いたいときどうしたらいいのでしょうか
imihito

2018/08/11 06:05

コールバック処理は値の格納だけして、格納した値をVBA側から参照すれば良いでしょう
kamikazelight

2018/08/11 06:22

なるほど 、そうすると格納先はモジュール変数などになるのでしょうか? 同時に異なる処理を複数実行する事が前提なのでそれぞれに格納場所を用意しないといけないですよね? 処理の内容が違うので実行した順番とコールバックの順番が同じとは限らないのでpowershellを実行する時に処理を特定するための情報の付加と戻り値にもその情報が入ってないとどの値がどの処理の結果か分からなくなってしまう気がするのですが、どうなのでしょうか?
imihito

2018/08/11 06:28

結果はモジュール変数などに格納する必要がありますね PowerShellへの引数としてキーとなる文字列を渡して、コールバック時はそのキーを使ってScripting.Dictionaryにでも格納すればよいでしょう
kamikazelight

2018/08/11 07:26

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

2018/08/11 07:35

ShellのvbHideの指定を外す、-NoExitオプションを付けるなどしてコンソールを残すようにし、エラーが発生していないか確認してみてください 非常に強引な方法のため、このようにデバッグが難しく、ある程度自己解決できないと難しいです
kamikazelight

2018/08/13 01:50

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

0

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

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

投稿2018/08/10 23:00

Zuishin

総合スコア28660

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kamikazelight

2018/08/11 02:07

既に見ていたので、もう一度見直してみたのですが Call SharedWshShell.Run("powershell -ExecutionPolicy RemoteSigned -Command Invoke-Expression """ & command_string & " | Out-File -filePath " & tempPath & " -encoding Default""", WshHide, True) Runで実行して結果はテキストに書き込みしていますよね そのあとに Set GetPSCommandResultAsTextStream = SharedFSO.OpenTextFile(tempPath, ForReading) で書き込んだ内容をテキストストリームとして返しているのでこのままだと非同期で実行出来ないですよね? 非同期にするためにはRunのWaitをfalseにして返値はファイル名にして powershell側で書き込み中は排他制御でファイルにロックをかけて 別途対象ファイルが存在して尚且つロックされていなければ返値としてテキストかテキストストリームを返す関数を作る感じにすればいいでしょうか? でもこの場合処理を途中で中断したくなった場合はどうしたらいいでしょうか? 結果が書き込まれたファイルが後で出てくるので削除しないといけないですし...
Zuishin

2018/08/11 03:02

ファイル名をその都度違うものにするのはどうですか? guid を使うのがいいと思います。
Zuishin

2018/08/11 03:05

使ったファイルはもちろん削除してください。 VBA から CUI シェルをコンソールを開かずに使うというのは特殊な使用法なので、それなりの工夫は必要になると思います。 私なら VBA で完結させるか、exe を作るか、そもそも VBA を使わないというところから検討します。
kamikazelight

2018/08/11 03:16 編集

どのguidをどう取得すれば良いでしょうか? ファイル名を一意にするのは分かったのですが 処理の完了確認や中断はどうしたらいいでしょうか Excelが主体なのですがvbaを使わないというのは powershellからexcelを起動してイベントもpowershell側で管理するという事でしょうか
Zuishin

2018/08/11 03:31 編集

どの guid というのがよくわかりませんが、目的が果たせるもので。 処理の完了確認は戻り値で。 中断はプロセス停止で。 VBA を使わないということは Excel を主体にしません。Excel はあくまでデータの供給に使うということです。
kamikazelight

2018/08/11 04:06 編集

すみません GUIDが自動で割り当てられるものと勘違いしていました。 >処理の完了確認は戻り値で なんの戻り値でしょうか?使うのはrunですよね? 非同期 で実行すると戻り値は0にしかならないはずです >中断はプロセス停止で。 runですよね?プロセスID欲しかったら探さないといけないうえに非同期前提なので同じプロセスの名前が複数あるので特定できないですよね?
Zuishin

2018/08/11 04:30 編集

書いたとおり、私は中身を保証しません。目的を果たせないなら無理なのでしょう。 テキストファイルで戻り値もプロセス ID もやりとりできるとは思いますが、私は作りません。あなたに作れないのならこの方法はあきらめてください。 私なら VBA から PowerShell という連携を無理やりするよりは exe を作ります。 その方がはるかに簡単そうです。 PowerShell を呼び出す exe を作って仲介させる方法もありますが、exe 一本で行う方法に比べれば冗長です。 https://ufcpp.net/study/powershell/interop.html
kamikazelight

2018/08/11 05:41 編集

試した事に書いてある内容そのままいわれてもそれで出来ないので困っているのです。 >テキストファイルで戻り値もプロセス ID もやりとりできるとは思いますが、私は作りません。 といわれても、それが分からないので困っています。丸投げしているわけではないので私が作るのが当然ですが使う関数一つのヒントもなしではせっかくコメント頂いても意味ないです 例えばpowershell側が出力するファイルを2つにして名前の関連性を持たせておいて呼び出された段階で自身のプロセスIDを出力して結果はあとで別のテキストファイルに出力する様にすれば何とかなるとは思うのですが そういう事でしょうか? それとも 実はRunでもプロセスIDを取得できるのでしょうか?
Zuishin

2018/08/11 05:43

何がわからないのかこちらでは見当もつかないので、何とかなるならそうしてください。 少なくとも試したことそのままではないと思います。
kamikazelight

2018/08/11 06:30 編集

>処理の完了確認は戻り値で なんの戻り値でしょうか?使うのはrunですよね? 非同期 で実行すると戻り値は0にしかならないはずです >中断はプロセス停止で。 runですよね?プロセスID欲しかったら探さないといけないうえに非同期前提なので同じプロセスの名前が複数あるので特定できないですよね? と分からないことをちゃんと書いているのですが >テキストファイルで戻り値もプロセス ID もやりとりできるとは思いますが、私は作りません。 としか返事もらってないです。 どうしたらできますか? に対して 出来ると思うのですが と言われましても... 最初の投稿で >返値がテキストファイルに書き出して読み込みとかしないと受け取れなかったりとか >Exec ならステータスの確認が出来たり, stdin なるもの が何かに使えそうだと思ったので >今のところはそちらを使いたいと考えています。 っとRunで実行して結果はテキストファイルに出力すれば何とかなるかもしれないけど 出来れば使いたくない旨を示したつもりだったのですが >何とかなるならそうしてください。 といわれると
Zuishin

2018/08/11 06:29

いやそんなこと言われても。 作れということですか?
kamikazelight

2018/08/11 06:35

先ほど書きましたが 丸投げしているわけではないので私が作るのが当然ですが使う関数一つのヒントもなしではせっかくコメント頂いても意味ないです
Zuishin

2018/08/11 07:19

そのあたりの意味がわかりません。 ファイルの入出力の方法が知りたいということですか?
Zuishin

2018/08/11 07:22

それと、imihito さんが良い方法を示しておられるので、こちらの方法にこだわる必要はないと思うのですが。私に何かコードを書かせたいということではないのですね?
kamikazelight

2018/08/11 07:27 編集

>私に何かコードを書かせたいということではないのですね? そうです
Zuishin

2018/08/11 07:28

違いますではわかりません。 そこまでして何が知りたいのですか? 何がわからないのかが全くわからないと書きましたが。 ファイルに戻り値を出力して、それを読む。戻り値を得るのはそれだけのことではないですか?
Zuishin

2018/08/11 07:29

非同期に耐えるようにするには、ファイル名を個別に設定すればいい。 それには guid を使えばいい。 どこにわからない要素がありますか?
Zuishin

2018/08/11 07:34

そのうえで、私なら exe を作ると言いました。 つまり、私は PowerShell を VBA から呼び出すような方法は使いません。 開発効率も実行効率も悪いからです。
kamikazelight

2018/08/11 07:41 編集

ごめんなさい 最初コメントを読み間違えて 「私に何かコードを書かせたいということですね?」と勘違いしました。 何が分からないかですが 3回目になりますが一応載せます >処理の完了確認は戻り値で なんの戻り値でしょうか?使うのはrunですよね? 非同期 で実行すると戻り値は0にしかならないはずです >中断はプロセス停止で。 runですよね?プロセスID欲しかったら探さないといけないうえに非同期前提なので同じプロセスの名前が複数あるので特定できないですよね? です。 あと 連続でコメントする時には修正で付け足してもらってもいいでしょうか? 会話の流れがバラバラになります。
Zuishin

2018/08/11 09:01

戻り値はファイルでどうぞ。
Zuishin

2018/08/11 09:02

そこがわからなかったのですね? ファイルに書き込んでください。
Zuishin

2018/08/11 09:03

もう一回書きましょうか?
kamikazelight

2018/08/13 02:09

すみません。一度の投稿で複数の質問をした際に 番号を振ってなかった私が悪い のですが、 どの回答が どの質問への回答なのか 分かりにくかったです。 迅速な回答をありがとうございました。 ただ、短時間の連続投稿は会話の流れが乱れるためやめて頂きたいです。 私の質問の仕方が悪く 不快な思いをさせていたらすみません。 次回から気を付けますので またよろしくお願い致します。
Zuishin

2018/08/13 03:03

会話の流れを変えなければいいのでは?
kamikazelight

2018/08/13 03:28

A対してBを返そうとしている時にAをA’に変えられるとBが成り立たなくなります
Zuishin

2018/08/13 03:34

変えなければいいのでは?
kamikazelight

2018/08/13 04:29

>変えなければいいのでは? 変わらないようにするために連続での投稿をやめてほしいのです。 例えば 下記のような問題があります。 1.私:「Aはなんですか?」 2.あなた:「B+Cです。」 3.(入力中)私:「B+C=Fであってますか?」 4.あなた「あとDです。」 5.私:「B+C=Fであってますか?」 このように5の質問があなたの4によって成り立たなくなるのです。 「AがA'に変わる」というのはそういう意味で使いました。 テラテイルは誰かが入力途中であっても 表記されない為上記のような問題が発生します。 追加投稿ではなく編集した場合も同じ問題は起きますが、その場合は 1.私:「Aはなんですか?」 2.あなた:「B+Cです。」 3.(入力中)私:「B+C=Fであってますか?」 4.(修正)あなた:「B+C+Dです。」 5.私:「B+C=Fであってますか?」 6.(修正)私:「B+C+D=Fであってますか?」 っとすることが出来、その場合画面上には 1.私:「Aはなんですか?」 2.あなた:「B+C+Dです。」 3.私:「B+C+D=Fであってますか?」 っと正しい会話のみを残すことが出来ます。 投稿に 無意味となった文章が 散乱するほど全体の 理解が困難になります。 テラテイルは運営方針? として下記のことを謳っています。 https://teratail.com/tour >私達は、エンジニアが抱える問題の解決を全力でサポートします。 >質問・回答によって 生まれたコンテンツを、同じ問題を持った人に最適な形で届けます。 >プログラミングに関して、わからないことがあれば是非teratailで質問してください。 >あなたがわかることがあれば、是非解決方法をシェアして解決の手助けをしてください。 >あなたの全ての行動が、いつか多くの日本のエンジニアの為になります。 これらは後からその投稿を見た人が理解できなければ成り立ちません。 後から見る人の為にも必要以上の連続投稿は控えたほうがいいと思います。
Zuishin

2018/08/13 04:37

連続投稿の話に変えたのはあなたです。 変えなければいいのでは?
kamikazelight

2018/08/13 05:25

ちゃんと読みましたか? ありがとうございました。
Zuishin

2018/08/13 05:27

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

2018/08/13 05:28

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

2018/08/13 05:35

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.49%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問