Capturing the Output of a MS-DOS Program

Capturing the Output of a MS-DOS Program

Have you ever heard of redirected input or consoleapplications? Have you ever had the need to launch MS-DOS programs, wait for them toterminate, and then dump their output to screen? Let’s see how to accomplish all thisleveraging Win32 and Visual Basic.

Outside the exciting and pretty ideal world of the just-releasedsoftware products, many developers have still to cope with some legacy MS-DOS program. Inmost cases, you have a Windows application that launches a MS-DOS program, waits for it toterminate and then has the need to capture its output, possibly as a string. Is all thisdoable with Visual Basic? But of course.

In this article, I’ll describe a CShell classthat is meant to be an enhanced wrapper for the glorious Shell command.It exposes an Execute method that runs650 368 2939 a batch-mode, non-interactiveprogram and returns any character sent out to the standard output device during execution.

Redirecting Output
Working with MS-DOS, how many times you entered a commandlike this to get the list of all the executables in the root of your disk C:?

C:> dir *.exe >Programs.txt

The final effect of such a command was toopen an I/O channel on a file device and work with it the same way it would have done withthe screen. We need just to reproduce this behavior from within a Win32 application.

The first approach that comes to mind suggests to employ the VB’sshell function to execute the program’s command line using the MS-DOS redirection operator> to send everything to a file. Let’s see how this should work. The shellfunction has the following syntax:

processID = Shell(pathname[, windowstyle])

The first argument is the command line to execute,while the second argument denotes the style of the window in which the program must run.The returned value is the ID of the process created, if any. Notice that the returnedvalue is the process ID and not the process handle (HPROCESS). You can obtain a HPROCESSfrom the ID using the OpenProcess() function. Shell starts any programasynchronously. In other words, the calling program continues to run as soon as the newprocess is started and nothing knows about its termination.

Under the Hood of the Shell Function
The Shell function utilizes the CreateProcess()function to start a new process. CreateProcess is the standard Win32 way to create a newprocess. It’s a really powerful function even if its prototype is a bit baffling:

Declare Function CreateProcess Lib"kernel32" _
  Alias "CreateProcessA" ( _
    ByVal lpApplicationName As String, _
    ByVal lpCommandLine As String, _
    lpProcessAttributes As Any, _
    lpThreadAttributes As Any, _
    ByVal bInheritHandles As Long, _
    ByVal dwCreationFlags As Long, _
    lpEnvironment As Any, _
    ByVal lpCurrentDirectory As String, _
    lpStartupInfo As STARTUPINFO, _
    lpProcessInformation As PROCESS_INFORMATION _
) As Long

The program name you pass to Shellis in turn passed to CreateProcess as its lpApplicationName argument. Theredirection operator you might think to put in the command line is ignored byCreateProcess that simply has another way to achieve the same result. Thus you shouldforget a syntax like

Shell "MyProg.exe >File.txt"

and begin concentrating on the features CreateProcess()exposes.

Arranging a Call to CreateProcess
The following is a typical way to call into CreateProcess. Asyou can see, many arguments are just zeroes:

bResult = CreateProcess( _
    szProgram, _
    vbNullString, _
    ByVal 0&, _
    ByVal 0&, _
    True, _
    ByVal 0&, _
    vbNullString, _
    si, _

The function returns a boolean value denoting the success of theoperation. The first two arguments indicate the program name and its command line. Youaren?t strictly required to split program name and command line but you could passall through the single lpApplicationName parameter. In most cases youdon?t need to take care of all the remaining arguments until the last two. (You canleave them to take the default values, as shown above.)

The core of CreateProcess ? at least for the purposes of thisarticle ? are the arguments of type STARTUPINFO and PROCESS_INFORMATION.


   hProcess As Long
   hThread As Long
   dwProcessId As Long
   dwThreadId As Long
End Type

   cb As Long
   lpReserved As String
   lpDesktop As String
   lpTitle As String
   dwX As Long
   dwY As Long
   dwXSize As Long
   dwYSize As Long
   dwXCountChars As Long
   dwYCountChars As Long
   dwFillAttribute As Long
   dwFlags As Long
   wShowWindow As Integer
   cbReserved2 As Integer
   lpReserved2 As Byte
   hStdInput As Long
   hStdOutput As Long
   hStdError As Long
End Type

Any process has its own handle and its own ID. When a processstarts it generates at least one thread. Alos threads are identified via handles and IDs.All this information is returned through the parameter of PROCESS_INFORMATION.

The Standard Output Device
The program you run will create a window. To establish a channelwith this window you could utilize the STARTUPINFO argument. Such a data structure letsyou decide the style of the window, its size, the text for the title bar and even thedesktop (under Windows NT only). It also makes available three handles to manage the threestandard I/O devices: input, output and error. If you want the output of your MS-DOSprogram to go to a file just pass a handle to that file as the hStdOutfield.

Notice that this technique will work only with those program that send their output to the standard output device (aka stdout). This is quite usual with many MS-DOS and Win32 console programs, but is not a rule.

To make use of the hStdOut field you need to addthe STARTF_USESTDHANDLES to the dwFlags field:

si.cb = Len(si)

I’ve said that you need to pass a file handle to hStdOut.That’s true but not all the handles will work fine. The only valid handle is any returnedby CreateFile(). There’s a great difference between, say, a window handleand what’s needed here.

Dim hFile As Long
hFile = CreateFile(
    sTempFile, _
    0, _
    ByVal 0&, _
    0, _
    ByVal 0&)

If hFile Then
    si.hStdOutput = hFile
End If

Once you created your output file and passed its handle to hStdOuteverything that the running program sends out to its stdout goes to the file. At the endof the program you have just to close the file with CloseHandle() andread it back in case you want to return a string.

Designing the CShell Class
The CShell class I mentioned earlier will havejust an Execute method with a prototype like this:

Public Function Execute( _
   ByVal szProgram As String, _
   Optional ByVal fRedirectOutput As Boolean_
) As String

It takes the name of the program to run, including any neededparameters, and a boolean flag that can explicitly state whether output redirection isneeded or not.

Public Function Execute(ByVal szProgram As String, _
     Optional ByVal fRedirectOutput) As String
Dim hProcess As Long
Dim hFile As Long

Execute = ""
If IsMissing(fRedirectOutput) Then
   fRedirectOutput = False
End If

' If redirection needed executes program
sTempFile = GetTempFile
If fRedirectOutput Then
     si.cb = Len(si)
     si.wShowWindow = SW_HIDE

     ' Creates a temp file
     hFile = CreateFile(sTempFile, GENERIC_READ Or GENERIC_WRITE, _
                0, ByVal 0&, CREATE_ALWAYS, 0, ByVal 0&)
     If hFile Then
          si.hStdOutput = hFile
     End If
End If

' Creates the process
bResult = CreateProcess(szProgram, vbNullString, _
         ByVal 0&, ByVal 0&, True,NORMAL_PRIORITY_CLASS, _
         ByVal 0&, vbNullString, si, pi)
If bResult Then
     WaitForSingleObject pi.hProcess, INFINITE
End If

' Closes handles
If hFile And fRedirectOutput Then
    Dim numOfBytes As Long
    Dim buf As String
    numOfBytes = GetFileSize(hFile, ByVal 0&)
    buf = Space(numOfBytes)
    CloseHandle (hFile)

    ' Reads back the file content
    hFile = lopen(sTempFile, 0)
    lread hFile, buf, numOfBytes
    lclose (hFile)

    ' Returns
    Execute = buf
    DeleteFile (sTempFile)
End If
End Function

Private Function GetTempFile() As String
  Dim s As String

  dwSize = GetTempPath(0, s)
  s = Space(dwSize - 1)
  GetTempPath dwSize - 1, s
  If Right$(s, 1) "" Then
     s = s + ""
  End If
  s = s + "TEMP.tmp"
  GetTempFile = s
End Function

Often you have also the problem of stopping the calling processuntil the spawned process terminates. This can be easily accomplished via WaitForSingleObject()as shown in the listing above.

This article demonstrated how to capture the output of MS-DOSprogram that writes to the standard output device. By making a specialized use ofCreateProcess you can redirect the output to a temporary file and then read it back into astring. The source code available includes a cshell.cls file plus a cshell.baswith all the declarations needed.


Share the Post: