Saturday, June 14, 2008

Pipe Dreams (or: VBScript, Spawned Processes and StdOut/StdErr Capture)

I mentioned before that I'm writing a small console utility for Window$ that reads and writes a lot of files. It works nicely on my Debian machine, both when compiled natively and when cross compiled for Window$ and run with Wine. It even works when run in a console window on my wife's PC.

So far so good, but I intend to spawn the program from within a VB script, run by the Windows Script Host. So I wrote a little script that (I thought) does exactly that. Here's a script that runs a command and captures its output:

' do.vbs - run a command and echo its output
' usage:
' cscript do.vbs "command arguments ..."
Set WshShell = CreateObject("WScript.Shell")
If Wscript.Arguments.Count = 1 Then
runCommand Wscript.Arguments.Item(0)
Else
Wscript.Echo "Please supply command to run, enclosed in double quotes."
End If
Set WshShell = Nothing

Sub runCommand(strCommand)
Set objScriptExec = WshShell.Exec(strCommand)
strStdOut = objScriptExec.StdOut.ReadAll
WScript.Echo strStdOut
Set objScriptExec = Nothing
End Sub

This works nicely with commands like "dir C:" or "ipconfig /all", or any other program that only outputs text to the standard output stream (StdOut). Trouble starts when the program in question also outputs text to the standard error stream (StdErr) - a common practice among console utilities, mine included.

Such programs simply hang.

How lame.

Yes, even if you try to capture StdErr with StdErr.ReadAll.

Well, it seems that only one stream can be captured like this. It's some kind of a race condition, since you can get it to work for some programs (as in this Micro$oft knowledge base article). But in general it's hopeless.

Here's the best workaround I could come up with for this (tested on WinXP Home edition, YMMV):

Sub runCommand(strCommand)
Set objScriptExec = WshShell.Exec("cmd /c " & strCommand & " 2>NUL")
strStdOut = objScriptExec.StdOut.ReadAll
WScript.Echo strStdOut
Set objScriptExec = Nothing
End Sub

This completely discards the contents of StdErr. Alternatively, you may want to replace NUL with a path to a file, so that StdErr will be redirected to that file.

So very lame.

[29 Oct 2008] UPDATE: a kind anonymous soul posted a comment, providing a better workaround:

Sub runCommand(strCommand)
Set objScriptExec = WshShell.Exec("cmd /c " & strCommand & " 2>&1")
strStdOut = objScriptExec.StdOut.ReadAll
WScript.Echo strStdOut
Set objScriptExec = Nothing
End Sub

which not only prevents the script from hanging, but also allows it to collect messages from both StdOut and StdErr. Thanks!

2 comments:

  1. Did you try redirecting stderr to stdout ? ie 2>&1

    ReplyDelete
  2. Thanks for the tip!

    I replaced the string " 2>NUL" with " 2>&1", and indeed, the script does not hang anymore, and now it can capture messages on both the output and error streams.

    Nice. LazyWeb saves the day.

    But (hey, I'm never satisfied...), what I'm really after is capturing both streams into separate strings, without capturing one of them to a file first.

    Any idea?

    ReplyDelete