DOBON.NET DOBON.NETプログラミング掲示板過去ログ

VB.NETでDOSコマンドの記述方法

環境/言語:[VB.NET 2005, Windows XP]
分類:[.NET]

以下より質問があります。

DOSコマンドを出力しデータを取得する
http://dobon.net/vb/dotnet/process/standardoutput.html

この場合、
psi.Arguments = "/c dir c:\ /w"
これでディレクトリ内のファイル情報が表示されるのですが、
この場合、表示されるファイルの内容が、
いわゆるプロジェクト内のディレクトリがルートになります。
たとえば、以下のようなディレクトリです。
c:\Documents and Setting\User\My Documents\Visual Studio 2005\Project\WindowsApplication\bin\Debug

もしも、c:\上のファイル情報を表示させたい場合、
どのように記述すればいいのでしょうか?

また、たとえば
dirコマンドを実行後、もう一度dirを実行し、Console.WriteLine(results)で
出力結果を書き出したい場合(すなわち、2つのコマンドを順番に実行したい場合)

psi.Arguments = "/c dir c:\ /w"
psi.Arguments = "/c dir c:\ /w"

と、ソース上に2度記述しても1度しか実行されません。
このようなことは出来ないのでしょうか?

よろしくお願いします。
2006/03/04(Sat) 09:21:17 編集(投稿者)

DJ Waveさん,こんにちは。

> psi.Arguments = "/c dir c:\ /w"
> これでディレクトリ内のファイル情報が表示されるのですが、
> この場合、表示されるファイルの内容が、
> いわゆるプロジェクト内のディレクトリがルートになります。

再現しません。私のマシンではc:\が出力されます。

> psi.Arguments = "/c dir c:\ /w"
> psi.Arguments = "/c dir c:\ /w"

これはあたりまえです。2回代入したって,psi.Argumentsの
内容は変わりません。
全体を関数にして2回呼べば良いのでは?

CommandLine("/c dir c:\ /w")
CommandLine("/c dir c:\ /w")

Sub CommandLine(ByVal Cmd as String)
...

...
psi.Arguments = Cmd
...

...
End Sub
>>psi.Arguments = "/c dir c:\ /w"
>>これでディレクトリ内のファイル情報が表示されるのですが、
>>この場合、表示されるファイルの内容が、
>>いわゆるプロジェクト内のディレクトリがルートになります。
>
> 再現しません。私のマシンではc:\が出力されます。

もしかすると私のプログラミングミスかもしれません。
今やるとc:\が表示されていました。

>>psi.Arguments = "/c dir c:\ /w"
>>psi.Arguments = "/c dir c:\ /w"
>
> これはあたりまえです。2回代入したって,psi.Argumentsの
> 内容は変わりません。
> 全体を関数にして2回呼べば良いのでは?
>
> CommandLine("/c dir c:\ /w")
> CommandLine("/c dir c:\ /w")
>
> Sub CommandLine(ByVal Cmd as String)
> ...
> 略
> ...
> psi.Arguments = Cmd
> ...
> 略
> ...
> End Sub
>

ありがとうございます。
関数にしたことで上記の事は実現できました。
ただ、たとえばカレントディレクトリをc:\へ移した後、コマンド作業をする場合、
たとえば上記の方法だと、

'cd dir c:\ とやれば出来ることを、あえて下のようにしたい
CommandLine("/c cd c:\")
CommandLine("/c dir")
    …
    … 
    …

このように、一度カレントディレクトリをc:\へと移して数多くのコマンドを実行する場合、
上記の方法では出来ませんでした。
このようなことを実現するにはなにかありますでしょうか?
> 上記の方法では出来ませんでした。

p.WaitForExitで一度プロセスを終了しているので,前のコマンドの
続きを操作することはできません。

> このように、一度カレントディレクトリをc:\へと移して数多くのコマンドを実
> 行する場合、
> 上記の方法では出来ませんでした。

一番簡単なのはバッチファイルにしてしまうことだと思います。

C:\CMD.BAT -----
dir c:\
dir d:\
-------------

psi.Arguments = "/c C:\CMD.BAT"
> 一番簡単なのはバッチファイルにしてしまうことだと思います。
>
> C:\CMD.BAT -----
> dir c:\
> dir d:\
> -------------
>
> psi.Arguments = "/c C:\CMD.BAT"
>

YASさんありがとうございます。

一応、これらのArgumentsの値を今後は可変できるようにする予定なので、
仮にバッチファイルで行うと、
毎度ファイルを作成するという作業を行わないといけないため、
なんとかバッチファイルを作らない方法を考えていたのですが、
やはりどうもうまくいきません。
バッチファイルを作らずにダイレクトに行いたいのですが、
流れとかでもいいので、何か良い方法なないでしょうか?
> 一応、これらのArgumentsの値を今後は可変できるようにする予定なので、
> 仮にバッチファイルで行うと、
> 毎度ファイルを作成するという作業を行わないといけないため、
> なんとかバッチファイルを作らない方法を考えていたのですが、
> やはりどうもうまくいきません。
> バッチファイルを作らずにダイレクトに行いたいのですが、
> 流れとかでもいいので、何か良い方法なないでしょうか?

う〜ん。私もうまくできませんでした。
文字列から一時的なバッチファイルを自動的に作成して実行するのが
やっぱり一番簡単そうです。
WorkingDirectoryプロパティを、コマンドごとに設定させる方法は
だめでしょうか?

実際に動作させたいコマンドと、そのコマンドが動作すべきディレクトリが
対になるような2つの配列を作成し、配列がなくなるまで繰り返すといった
方法ですが、はずれていたらごめんなさい。

# 実装時はCmdsとDirsを動的に作り上げる・・
# ・
# ・
# Dim Cmds() As String = {"/c DIR /w", "/c DIR /w", "/c DIR /w"}
# Dim Dirs() As String = {"C:\", "D:\", "E:\"}
# ・
# CommandLine(Cmds,Dirs)
# ・
# ・
# Sub CommandLine(ByVal Cmds() as String,ByVal Dirs() as String)
# ...
# 略
# ...
# For I = Cmds.GetLowerBound(0) To Cmds.GetUpperBound(0)
# ...
# 略
# ...
# psi.WorkingDirectory= Dirs(I)
# psi.Arguments = Cmds(I)
# ...
# 略
# ...
# Next
# End Sub
■No15460に返信(にゃらんたさんの記事)
> WorkingDirectoryプロパティを、コマンドごとに設定させる方法は
> だめでしょうか?
>
> 実際に動作させたいコマンドと、そのコマンドが動作すべきディレクトリが
> 対になるような2つの配列を作成し、配列がなくなるまで繰り返すといった
> 方法ですが、はずれていたらごめんなさい。

やってみました。
これならいけそうですね。

ちょっと気になったのですけど、
これらを実行すると、実行結果は表示されますが、
いわゆるこちらから事前に仕込んだコマンドは表示されないのですね。
これも表示させる方法とかはありますでしょうか?
たとえば、

# Dim Cmds() As String = {"/c DIR /w", "/c DIR /w", "/c DIR /w"}
# Dim Dirs() As String = {"C:\", "D:\", "E:\"}

これでしたら、
これを実行後、

c:\>dir /w    ←これらも表示させたい
(実行結果)
d:\>dir /w    ←これらも表示させたい
(実行結果)
e:\>dir /w    ←これらも表示させたい
(実行結果)

こんな感じにしたいのですけど。
今だと、実行結果しか表示されていない状態です。
こういう感じでできなくはないですが、
・コマンドが正しく実行されたかどうか不明
・このままでは途中結果の取得ができない
など問題点は多いです。

Dim results As String
Dim psi As New System.Diagnostics.ProcessStartInfo

'ComSpecのパスを取得する
psi.FileName = System.Environment.GetEnvironmentVariable("ComSpec")

'入力を読み取れるようにする
psi.RedirectStandardInput = True
'出力を読み取れるようにする
psi.RedirectStandardOutput = True
psi.UseShellExecute = False
'ウィンドウを表示しないようにする
psi.CreateNoWindow = True
'起動
Dim p As Process = Process.Start(psi)

'コマンドラインを実行
p.StandardInput.WriteLine("c:")
p.StandardInput.WriteLine("cd \")
p.StandardInput.WriteLine("dir /w")
p.StandardInput.WriteLine("cd windows")
p.StandardInput.WriteLine("dir /w")
'exit
p.StandardInput.WriteLine("exit")


'出力を読み取る
results = p.StandardOutput.ReadToEnd
'出力された結果を表示
Console.WriteLine(results)

'WaitForExitはReadToEndの後である必要がある
'(親プロセス、子プロセスでブロック防止のため)
p.WaitForExit()
かなりベタなやり方になりますが、
以下の方法でやれます。

# ''==PROMPT=$P$Gの場合==
# Console.WriteLine(Dirs(I) & ">" & Cmds(I))
# Console.WriteLine(results)

問題点さえ気にならなければ、mc923さんの方法が
いいと思います。
一連のコマンドの中に、実行したコマンドのERRORLEVELで
処理内容を変えるとかの独自のフロー制御を
入れたい場合はmc923さんのやり方だと難しそうですね。
ずいぶん前のトピックをぶり返してしまってすみません。

その後、にゃらんたさんの方法を中心に組んでいたのですが、
ある程度組んでいるとちょっと気になる点に気づきました。

たとえば、コマンドプロンプトで処理に時間が掛かるものの場合、
(pingコマンド等で応答に時間が掛かるもの)
これれを実行すると、コマンドがすべて終わるまで一切他の処理を受け付けません。
と言うよりも、状態的には一時的に固まった状態になります。

そこで、例としてにゃらんたさんのを参考に、
TextBox1にコメントを追加しながら実行するものにしてみました。

# Dim Cmds() As String = {"/c ping xxx.xxx.xxx.xxx", "/c ping xxx.xxx.xxx.xxx", "/c ping xxx.xxx.xxx.xxx"}
# Dim Dirs() As String = {"C:\", "D:\", "E:\"}
# Dim Coms() As String = {"ping実行1", "ping実行2", "ping実行3"}
# CommandLine(Cmds,Dirs,Coms)
# ・
# ・
# Sub CommandLine(ByVal Cmds() as String,ByVal Dirs() as String, ByVal Coms() As String)
# ...
# 略
# ...
# For I = Cmds.GetLowerBound(0) To Cmds.GetUpperBound(0)
# ...
# 略
# ...
# psi.WorkingDirectory= Dirs(I)
# psi.Arguments = Cmds(I)
# ...
# TextBox1.Text = TextBox1.Text & Com(I) & vbCrLf
# ...
# Next
# End Sub


上記のようにやると、順番にTextBox1にComの内容が表示されてくれると思われるのですが、
実際はすべてが同時に表示されます。
実際これでも問題がないと言えばないのですが、
一瞬処理が止まったような感じになってしまうため、
なんとかしたいと思っています。
そもそもこれはどうしてこのような状態が怒ってしまうのでしょうか?
>これれを実行すると、コマンドがすべて終わるまで一切他の処理を受け付けません。
>と言うよりも、状態的には一時的に固まった状態になります。

同時に処理を進めるためにはマルチスレッドにする必要があると思います。

マルチスレッドにすればよほどのことがない限り,同時に処理が進みますが,
平行して進む処理の足並みをそろえるための処理を加える必要があります。
マルチスレッドにしたサンプルです。
一番初めのコマンドを連続で実行するという質問の回答でもあります。

Imports System.Threading
Public Class Form1

Dim Process As Process
Dim Commands As String()
Dim TextBox As New TextBox

Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Me.TextBox.Multiline = True
Me.TextBox.Dock = DockStyle.Fill
Me.Controls.Add(TextBox)
Commands = New String() {"ping 127.0.0.1", "dir d:\", "dir e:\"}
Dim CommandThread As New Thread(New ThreadStart(AddressOf Command))
CommandThread.Start()
End Sub

Private Sub Command()
Dim psi As New System.Diagnostics.ProcessStartInfo()
psi.FileName = System.Environment.GetEnvironmentVariable("ComSpec")
psi.RedirectStandardInput = True
psi.RedirectStandardOutput = True
psi.UseShellExecute = False
psi.CreateNoWindow = True
Me.Process = Diagnostics.Process.Start(psi)
Dim OutputThread As New Thread(New ThreadStart(AddressOf Output))
Dim InputThread As New Thread(New ThreadStart(AddressOf Input))
OutputThread.Start()
InputThread.Start()
InputThread.Join()
OutputThread.Join()
Process.WaitForExit()
End Sub

Private Sub Output()
Dim Result As String = Process.StandardOutput.ReadToEnd
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Result})
End Sub

Private Sub Input()
For Each Command As String In Commands
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Command & ControlChars.CrLf})
Process.StandardInput.WriteLine(Command)
Next
Process.StandardInput.WriteLine("exit")
End Sub

Delegate Sub AddTextBoxDelegate(ByVal Text As String)

Private Sub AddTextBox(ByVal Text As String)
Me.TextBox.Text &= Text
End Sub

End Class
YAS様わざわざサンプルまで作っていただきありがとうございます。

マルチスレッドでというヒントを頂き、
私もどぼんさんが書かれた記事で勉強していました。

http://codezine.jp/a/article.aspx?aid=144

そこで、少し気になったのですが、
このマルチスレッドにする前までは、
psi.WorkingDirectory = "c:\directory"
のように、ディレクトリを変更していたのですが、

今回のように

'入力を読み取れるようにする
psi.RedirectStandardInput = True



StandardInput.WriteLine("????")

で実行させる場合、たとえばYASさんのサンプルで使うと、

Private Sub Input()
For Each Command As String In Commands
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Command & ControlChars.CrLf})
' ↓追加
Process.StandardInput.WriteLine("cd c:\directory")
Process.StandardInput.WriteLine(Command)
Next
Process.StandardInput.WriteLine("exit")
End Sub

のように
Process.StandardInput.WriteLine("cd c:\directory")
を追加すれば出来そうな気がしたのですが、
どうもこれを追加しても、うまいことディレクトリが変更してくれません。
おそらくですが、これもWriteLineでディレクトリを設定しても、
次のWriteLineで実行するときには、すでにカレントディレクトリに戻ってしまう?
ような気がしてならないのですが、ここに来てまたまたディレクトリでつまずいています。
どのような方法がベストでしょうか?
何度も何度もお手数ですがよろしくお願いします。
>おそらくですが、これもWriteLineでディレクトリを設定しても、
>次のWriteLineで実行するときには、すでにカレントディレクトリに戻ってしま
>う?

上の現象は私のところでは再現しませんでした。
No.14855のサンプルの12行目に

Commands = New String() {"cd c:\windows", "dir"}

と記述すればwindowsフォルダが表示されました。
YAS様ご回答ありがとうございます。

YAS様の方では動くと言うことで、違う方面からアプローチしていたところ、
コマンド内の記述が間違っていたために、うまいことディレクトリも変更出来ない、
正しい実行結果が得られないという状況でした。
(標準エラー出力がされていないため、コマンドの間違いに気づきませんでした)

そこで、そのような事をふまえて、標準エラー出力も実装しようとしたのですが、

まず、

Private Sub Command()
Dim psi As New System.Diagnostics.ProcessStartInfo()
psi.FileName = System.Environment.GetEnvironmentVariable("ComSpec")
psi.RedirectStandardInput = True
psi.RedirectStandardOutput = True
psi.RedirectStandardError = True ' ←追加
psi.UseShellExecute = False
psi.CreateNoWindow = True
Me.Process = Diagnostics.Process.Start(psi)
Dim OutputThread As New Thread(New ThreadStart(AddressOf Output))
Dim InputThread As New Thread(New ThreadStart(AddressOf Input))
Dim FailThread As New Thread(New ThreadStart(AddressOf Fail))
OutputThread.Start()
InputThread.Start()
FailThread.Start() ' ←追加
InputThread.Join()
OutputThread.Join()
Process.WaitForExit()
End Sub

Private Sub Fail()
Dim Result As String = Process.StandardError.ReadToEnd
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Result})
End Sub

これですと、複数のコマンドラインを実行したときに、
エラーの箇所でエラーを出力してくれません。
順序的になんとなく違うって言うのは分かるのですけど、
実際の所、同時に実行しているスレッドが、
エラーの時に出力されないのがちょっと理解できません。
やはりどこかおかしいでしょうか?
> 順序的になんとなく違うって言うのは分かるのですけど、
> 実際の所、同時に実行しているスレッドが、
> エラーの時に出力されないのがちょっと理解できません。
> やはりどこかおかしいでしょうか?

>Dim Result As String = Process.StandardError.ReadToEnd

ReadToEndは末尾まで読み込んでから値を返すので,Outputスレッドの
出力の途中にFailスレッドの出力が挟まることはあり得ません。
では,下の様にすればよいのか,というと実はこれでもダメです。

Private Sub Output()
Do While Not Process.StandardOutput.EndOfStream
Dim Result As String = Process.StandardOutput.ReadLine & ControlChars.CrLf
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Result})
Loop
End Sub

Private Sub Fail()
Do While Not Process.StandardOutput.EndOfStream
Dim Result As String = Process.StandardError.ReadLine & ControlChars.CrLf
Me.Invoke(New AddTextBoxDelegate(AddressOf AddTextBox), New Object() {Result})
Loop
End Sub

Inputスレッドの入力はOutputを待たずに行われ,それに対するエラー出力も
標準出力への出力を待たずに行われるので,一番最初にエラーがまとめて
表示されてしまいます。

実際のコマンドプロンプトに手でタイプしたような出力を得ることは面倒だと
思います。

> 上記のようにやると、順番にTextBox1にComの内容が表示されてくれると思われるのですが、
> 実際はすべてが同時に表示されます。
> 実際これでも問題がないと言えばないのですが、
> 一瞬処理が止まったような感じになってしまうため、
> なんとかしたいと思っています。
> そもそもこれはどうしてこのような状態が怒ってしまうのでしょうか?

CommandLineプロシージャを実行した段階で、このプログラムはCommandLineの内容のみを一生懸命処理します。
このとき、CommandLineの中でForm上の絵(ボタンやテキストボックスの見た目)を書き換える
処理(=TextBox1.Text = TextBox1.Text & Com(I) & vbCrLf の部分)を行った場合、TextBox1の見た目を
変更する処理(=Formの見た目を書き換える処理)は後回しにされ、CommandLineが完了した時点で
処理されます。

# 'テキストの追加(これを使うと、強制的に再描画され、自動的にスクロールする)
# TextBox1.AppendText(results)
と記述するか、
# TextBox1.Text & Com(I) & vbCrLf
# Me.Refresh 'フォームの表示内容を強制的に再描画する
と記述すると、おっしゃられているような現象には
ならないと思います。

※Ping等、一回の処理に時間がかかるものは、その処理が
 完了するまでは何も表示されません。

後、賛否があるとは思いますが、
Application.DoEvents()をMe.Refreshの位置に差し込んでも、
<見た目>は同様の処理となります。

DOBON.NET | プログラミング道 | プログラミング掲示板