vbScript - Run an External Program and Capture the Output

Please see my post vbScript - The Basics for more details on vbScript.

When you want to execute an external program for a particular result (such as resizing an image, as shown in a previous snippet) you can simply use the Run method of the Wscript.Shell object. However, if you want something a little more complex there is the Exec method of the same object. This method gives you access to the standard input, output, and error streams. As such, you can take advantage, for example, of the increased flexibility of all of the built in commands available in a command shell.

Let's say you want to generate a list of all of the files of a particular type in a given folder, and all subfolders. To do that using the Scripting.FileSystemObject you would have to write a recursive routine that would enumerate all of the files in the given folder of that type, then call itself for each subfolder in the given folder. While not that complex it is still not trivial. It would be far easier to just be able to do

dir /s .b somefolder\*.jpg

and capture the resulting output. As it turns out this is easily done by

set wso = CreateObject("Wscript.Shell")
set exe = wso.Exec("cmd /c dir /s /b d:\temp\*.jpg")

That's it. Except for getting the output. For that you read the text from the standard output stream. You can read it line by line in a loop like

Do Until exe.StdOut.AtEndOfStream
    Wscript.Echo exe.StdOut.ReadLine
Loop

I find it more convenient to real all of the output at once like

set wso = CreateObject("Wscript.Shell")
set exe = wso.Exec("cmd /c dir /s /b d:\temp\*.jpg")
sout = exe.StdOut.ReadAll

Most StdOut text will have lines terminated by vbCrLf, however, some programs ported from Unix/Linux, or output resulting from an operation of an odd text file, may return lines delimited only by vbLf. The following code will split output in either format into an array of lines.

sout = Replace(sout,vbCr,"")
If Len(sout) > 0 Then sout = Split(sout,vbLf): Else: sout = Split(""): End If

The Split("") hand,es the case when StdOut returns no text. This allows the calling routine to handle both cases with a For Each loop.

At the end of the listing you will see a block that looks like

''Test code                                                                             '
If StrComp(Wscript.ScriptName,"execcmd.vbs",vbTextCompare) = 0 Then
    For Each line In ExecCmd("cmd /c dir")
        WScript.Echo line
    Next
End If

This is something I borrowed from Python. Wscript.ScriptName returns the unqualified (no path) name of the currently executing script. If you execute the file ExecCmd.vbs by itself then the test code will run. As long as you ensure that any file you Include this code into has a different name then the test code will not be executed. Feel free to just delete it or comment it out.

One final note. When executing dos shell commands (cmd) you should always specify /c as an option. This will terminate the shell on completion. If you don't do this you may end up with orphan cmd.exe processes littering up memory.

''#region Header                                                                        '
''                                                                                      '
''  Name:                                                                               '
''                                                                                      '
''      ExecCmd.vbs                                                                     '
''                                                                                      '
''  Description:                                                                        '
''                                                                                      '
''      Executes an external program and returns all output written to stdout as an     '
''      array of lines of text.                                                         '
''                                                                                      '
''  Usage:                                                                              '
''                                                                                      '
''      results = ExecCmd(command)                                                      '
''                                                                                      '
''  Example:                                                                            '
''                                                                                      '
''      To return a list of all jpg files in a folder and all subfolders. Note that     '
''      is much simpler than writing a recursive function to do this with the file      '
''      system object.                                                                  '
''                                                                                      '
''      dirlist = ExecCmd("cmd /c dir /s /b d:\temp\*.jpg")                             '
''                                                                                      '
''  Audit:                                                                              '
''                                                                                      '
''      2016-02-15  rj  Original code                                                   '
''                                                                                      '
''#endregion                                                                            '

Function ExecCmd ( cmd )

    Dim wso: Set wso = CreateObject("Wscript.Shell")
    Dim exe: Set exe = wso.Exec(cmd)

    'Eliminating vbCr and then splitting on vbLf instead of just splitting on vbCrLf    '
    'handles output in both the Windows (vbCrLf) and Unix/Linux (vbLf only) formats.    '

    ExecCmd = exe.StdOut.ReadAll
    ExecCmd = Replace(ExecCmd,vbCr,"")

    If Len(ExecCmd) > 0 Then
        ExecCmd = Split(ExecCmd,vbLf)
    Else
        ExecCmd = Split("")
    End If

End Function

''Test code                                                                             '
If StrComp(Wscript.ScriptName,"ExecCmd.vbs",vbTextCompare) = 0 Then
    For Each line In ExecCmd("cmd /c dir")
        WScript.Echo line
    Next
End If

I have found a bug in vbScript. So far it has only shown up in one specific case. I have been converting a pile of old avi videos to mp4 (so that I could use them with my Chromecast device). Naturally, I threw together a script to act as a front end to divxengine.exe. The script is called tomp4.vbs

ToMP4 file [file...]

    Convert the given file(s) to mp4 format. The conversion is done by
    divxengine.exe which may not be able to decode all input formats.

    Options:

        /bitrate:same (/b) - use bitrate from source
        /bitrate:###  (/b) - use ###kbps as bitrate
        /crop         (/c) - remove black borders
        /show         (/s) - show divx window
        /ignore       (/i) - ignore files already converted
        /recode:dir        - re-encode and write to folder 'dir'
        /help         (/?) - show this help

It was working just fine. I could go into a folder and type tomp4 *.avi /b:950 and it would happily convert each file in turn. Until it came to a file with an unusual, but perfectly valid, character in the name. The offending character was ë. I was using ExecCmd to get a list of files (optional wildcards) and it seems that the text returned from StdOut had translated ë to . You can imagine that this causes all sorts of problems. I've put a notice into the Microsoft Feedback site.

In the meantime, if you want to get a list of files in a folder (with wildcards) you can use the following function.

''#region Header                                                                        '
''                                                                                      '
''  Name:                                                                               '
''                                                                                      '
''      GetFiles.vbs                                                                    '
''                                                                                      '
''  Description:                                                                        '
''                                                                                      '
''      Return a dictionary of all files in a folder matching the given dir command     '
''      style wildcards.                                                                '
''                                                                                      '
''  Audit:                                                                              '
''                                                                                      '
''      2018-07-08  rj  Original Code                                                   '
''                                                                                      '
''#endregion                                                                            '

Function GetFiles (folder, ByVal pattern)

    Set GetFiles = CreateObject("Scripting.Dictionary")
    Dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
    Dim rex: Set rex = New RegExp

    'Set up a pattern to replicate dic command wildcard matching

    pattern = Replace(pattern,".","\.")
    pattern = Replace(pattern,"[","\[")
    pattern = Replace(pattern,"]","\]")
    pattern = Replace(pattern,"(","\(")
    pattern = Replace(pattern,")","\)")
    pattern = Replace(pattern,"*",".*")
    pattern = Replace(pattern,"?",".")

    rex.Pattern = "^" & pattern & "$"
    rex.IgnoreCase = True

    Dim file: For Each file In fso.GetFolder(folder).Files
        If rex.Test(file.Name) Then GetFiles.Add file.Name, file
    Next

End Function

''Test code                                                                             '
If StrComp(Wscript.ScriptName,"GetFiles.vbs",vbTextCompare) = 0 Then
    Set files = GetFiles(".","*")
    For Each file In files.keys
        WScript.Echo file, files(file).Size
    Next
End If