[scripting] getting external program output

I plan to make a column script that displays a custom "tag" property of svn versioned files. I'd call svn commandline tool within my Opus scipt to query the files. My question is, how do I get the output of external program in my Opus script?

You should be able to do it using the WScript.Shell object's Exec method. See e.g. this example.

Using Exec() will always give you a command prompt window popping up, which is not desired in most cases.

I extracted these two functions from the TFS script add-in I use at work, which is also a version control thing o).
This uses the Run() method of the WScript shell object, and also cmd.exe which supports redirecting of streams.
It may not be the fastest solution, but the only one that..

  • does not show a window
  • provides returncode
  • provides all textual output
  • tests existence of exe-path if given

RunHiddenEx() returns an object with these props:
.returncode = the returncode of your external program
.stdout = the textual output to stdout
.stderr = the textual output to stderr

Example function call:

var result = RunHiddenEx("c:\mytool.exe", "/path c:\a\path");
///////////////////////////////////////////////////////////////////////////////
function RunHiddenEx ( exe, params, tmpFileBase, tmpFileExt, shell, fso){ //v1.2
		if (!fso)	fso		= new ActiveXObject("Scripting.FilesystemObject");
		if (!shell) shell	= new ActiveXObject("WScript.Shell");
		if (!tmpFileBase)	tmpFileBase = "DO.Temp.";
		if (!tmpFileExt)	tmpFileExt = ".txt";
		var tmpFileStdout = FM.GetTmpFileName(tmpFileBase, tmpFileExt, fso).fullname;
		var tmpFileStderr = tmpFileStdout+".err"+tmpFileExt;
		var cmd = '%comspec% /c ""'+exe+'" '+params+' >"'+tmpFileStdout+'" 2>"'+tmpFileStderr+'""';
		var result = {};
		result.cmd = cmd;
		if (exe.match(/^([A-z]:\\.+|\\\\(.*?)\\.+)$/)){ //test path to exe if given
			if (!fso.FileExists(exe)){
				var msg = "E   Executable not found ["+exe+"]";
				DOpus.Output(msg);
				throw msg;
			}
		}
		result.returncode = shell.Run( cmd, 0, true);
		result.stdout = ReadFile(tmpFileStdout, fso); fso.DeleteFile(tmpFileStdout);
		result.stderr = ReadFile(tmpFileStderr, fso); fso.DeleteFile(tmpFileStderr);
		return result;
	}
///////////////////////////////////////////////////////////////////////////////
function ReadFile ( path, fso ){
		fso = fso || new ActiveXObject("Scripting.FilesystemObject");
		var content = "";
		if (!fso.FileExists(path)){
			return content;
		}
		var file = fso.OpenTextFile( path, 1, -2); // Read, UseDefaultEncoding
		if (!file.AtEndOfStream)
			content = file.ReadAll();
		file.Close();
		return content;
	}	

Corrected version, function GetTmpFileName() was missing.
This is not tested, as I moved these functions out of their object context. Let me know if they fail. o)

///////////////////////////////////////////////////////////////////////////////
function RunHiddenEx ( exe, params, tmpFileBase, tmpFileExt, shell, fso){ //v1.2
      if (!fso)   fso      = new ActiveXObject("Scripting.FilesystemObject");
      if (!shell) shell   = new ActiveXObject("WScript.Shell");
      if (!tmpFileBase)   tmpFileBase = "DO.Temp.";
      if (!tmpFileExt)   tmpFileExt = ".txt";
      var tmpFileStdout = GetTmpFileName(tmpFileBase, tmpFileExt, fso).fullname;
      var tmpFileStderr = tmpFileStdout+".err"+tmpFileExt;
      var cmd = '%comspec% /c ""'+exe+'" '+params+' >"'+tmpFileStdout+'" 2>"'+tmpFileStderr+'""';
      var result = {};
      result.cmd = cmd;
      if (exe.match(/^([A-z]:\\.+|\\\\(.*?)\\.+)$/)){ //test path to exe if given
         if (!fso.FileExists(exe)){
            var msg = "E   Executable not found ["+exe+"]";
            DOpus.Output(msg);
            throw msg;
         }
      }
      result.returncode = shell.Run( cmd, 0, true);
      result.stdout = ReadFile(tmpFileStdout, fso); fso.DeleteFile(tmpFileStdout);
      result.stderr = ReadFile(tmpFileStderr, fso); fso.DeleteFile(tmpFileStderr);
      return result;
   }
///////////////////////////////////////////////////////////////////////////////
function ReadFile ( path, fso ){
      fso = fso || new ActiveXObject("Scripting.FilesystemObject");
      var content = "";
      if (!fso.FileExists(path)){
         return content;
      }
      var file = fso.OpenTextFile( path, 1, -2); // Read, UseDefaultEncoding
      if (!file.AtEndOfStream)
         content = file.ReadAll();
      file.Close();
      return content;
   }   
///////////////////////////////////////////////////////////////////////////
	function GetTmpFileName(prefix, extension, fso) {
		fso = fso || new ActiveXObject("Scripting.FilesystemObject");
		var tFolder = fso.GetSpecialFolder(2); //2 = temp folder
		var tFile = fso.GetTempName();
		if (prefix!=undefined) tFile=prefix+tFile;
		if (extension!=undefined) tFile+=extension;
		return {
			path	: tFolder.Path,
			name	: tFile,
			fullname: tFolder.Path+'\\'+tFile
		};
	}
1 Like

Isn't the command prompt because the example is running the comand prompt (cmd.exe)?

I don't think you have to run the program via cmd.exe.

Nope, cmd.exe shouldn't be the reason a window pops up with Exec().

Exec() will always give a window, but Run() does not if used with the appropriate parameter.
Exec() allows access to stdin/out/err, but Run() does not (and it does not allow redirection).
That's why cmd.exe is used to call the external tool with Run().
The stream redirection is hidden for Run(), but kicks in once cmd.exe runs the commandline passed.

This requires temporary files, but is better than a lot of black windows popping in and out.
I've been tinkering with run() and exec() for 15 years and it's always been that unsatisfying. o(

For Exec(), rumors tell, that if the command to run is a commandline tool, then it will not show a window.
Or if you wrap your call to your external tool in a call to a temporary script/batch whatever.
Maybe that works for someone, it never did for me on 2k, 2k3, XP, 7 etc.

Exec doesn't always open a command prompt window.

e.g. Run Notepad with it, and Notepad is the only window you'll see open:

Set objShell = CreateObject("WScript.Shell")
Set objWshScriptExec = objShell.Exec("notepad.exe")
Set objStdOut = objWshScriptExec.StdOut

While Not objStdOut.AtEndOfStream
   strLine = objStdOut.ReadLine
Wend

But if you're reading stdout, you're probably running a command-line program (although GUI programs can also write to it so it's not an absolute). Command-line programs on Windows will open a command prompt themselves unless it is explicitly suppressed.

So it's not Exec itself that's causing the command prompt to open, it's the program being run.

The same as you'll see a command prompt window appear if you double-click a command-line application.

I think I have a little GUI program I wrote somewhere which does nothing but run another program with its window hidden and redirect the output, to let you run command-line programs from any tool without their windows appearing. I could dig that out if it's useful. While Run lets you hide the window, it doesn't let you read the program's output until the program has completed, so there may be some cases where Exec is still desirable. (Although you usually want the reverse, and having to wait for completion with Exec is often just a pain in the behind. :slight_smile: Less than having to manage temp-files and extra code for dealing with them, OTOH.)

Wow thanks for all the detailed replies!
I'd like to avoid the temp files if possible, so leo's program seems interesting, can you try to dig it out? :slight_smile:

But another question also comes to mind: is there a chance to add such support to Opus scripting natively?

Also, does Column script initData property initData.default_enable have any meaning for Column scripts? Shouldn't it always be automatically enabled when user adds the column via Columns menu and auto-disabled when column is removed?

@tbone: I've just tried to use your solution and it works perfectly "out of the box" and it's fast even though it uses tmp file. Thanks! :thumbsup:

(@leo: I'm still interested in your program, unless such support can be added to Opus scripting natively.)

Ok, so Exec() will "always" open a command prompt, for when you're interested in the textual output of a command, can you agree on that Leo? o) I maybe wasn't 100% correct on this, but I guess Exec() remains an undesired way to call external "non-notepad" like tools.

For the sake of completeness, a function returning the same result object, realized with Exec().
It will probably popup a window, but does not rely on temporary files to capture output from stdout/stderr.

///////////////////////////////////////////////////////////////////////////////
function RunEx(exe, params){
	params = (params?" "+params:"");
	var shell = new ActiveXObject("WScript.Shell");
	var exec = shell.Exec(exe + params);
	var stdOut = "", stdErr = "";

	while(exec.Status == 0){
	   stdOut += exec.StdOut.ReadAll();
	   stdErr += exec.StdErr.ReadAll();
	}

	return {
		returncode : exec.ExitCode,
		stdout : stdOut,
		stderr : stdErr
	};
}

initData.default_enable just tells DO to enable that script, whenever it is added to the script addins folder.
This property has nothing to do with columns visibility or wether they're currently in use.

Peace! o)

Yep, it flashes console window for each file in dopus lister. Checked around the 'net and it seems there's absolutely no way to make Exec suppress the console window. There is a parameter for Run to suppress it but then it's impossible to get stdout.

By the way it seems the check in your code for non-existent exe doesn't work (never evaluates to "true"), specifically this line:

if (exe.match(/^([A-z]:\\.+|\\\\(.*?)\\.+)$/)){ //test path to exe if given

Yep, it flashes console window for each file in dopus lister.

Too bad I can't edit my post, I've just checked with launching notepad.exe and it does not open console window at all. It seems it doesn't open any console windows for GUI applications. At least on my Windows 8.1.

That's a reasonable approximation.

My main concern was people might think Exec itself always creates a command prompt, and avoid using it to run GUI apps, when in fact Exec never creates a command prompt.

(Exec also never suppresses the command prompt which is created by Windows console applications. Both console and GUI applications can output text for Exec to capture.)

[quote="jsys"]By the way it seems the check in your code for non-existent exe doesn't work (never evaluates to "true"), specifically this line:

if (exe.match(/^([A-z]:\\.+|\\\\(.*?)\\.+)$/)){ //test path to exe if given

Oops, nevermind, it actually works, I failed to notice that this expression works if exe path was given :blush:

@Leo
Yes, thanks. I really think I learned about the exact difference now. o)

@Jsys
Right, it just matches if a path is given. For commands to run, where you did not pass a path to some specific executable, it's hard to test if it exists, that's why it skips the test for those. One could still test with "where.exe" if that file is in the path somehwere, but.. these scripts don't necessarily need to be bloat-ware. o)

Is it possible to tell Opus to call my column method only once and give me a list of all visible files (for column) immediately, so that I can call my external program to process those in one pass, rather than it being invoked for each file separately (opening external program which processes file, writing redirected stdout and reading that stdout back would be a lot faster in one go)?

No, not currently. [But it was added later, if you find this old thread while searching. See ScriptColumnData.columns.]

1 Like

I'd appreciate something like that too! o) So: +1
Not only a column map to dump values for all columns at once, but also a collection of items, so these can all be processed in one go.

For now, the only thing you can do is to cache the output/data from your external tool in a tab-scoped variable and reuse that content, as long as it may be valid - if expired renew that, which needs some extra logic of course. For more static data and a lot of items, I did a good working column cache, which speeds up things tremendously, but for VCS things, items and their column values change to quickly I guess to be stored in that.

My TFS plugin remembers the temp-file for some seconds to skip subsequent calls to the external tool, which always fetches data for all items. This also speeds up things a lot, but there will be situations, where you need an extra press of "F5", because you'll recognize items which data is just not recent "enough".