4

I'm trying to process/filter input within a VBscript, but only if the input has been piped into the script. I don't want the script processing user/keyboard input. I'd like to code this as something like this:

stdin_is_tty = ...
if not stdin_is_tty then
   ...
   input = WScript.StdIn.ReadAll
end if

Otherwise, the script will hang, waiting on user input when it executes WScript.StdIn.ReadAll (or even earlier if I test the stream with WScript.StdIn.AtEndOfStream).

In C#, I'd use:

stdin_is_tty = not System.Console.IsInputRedirected // NET 4.5+

The accepted answer for Q: "How to detect if Console.In (stdin) has been redirected?" shows how to build that result using Win32 calls via P/Invoke, for versions of NET earlier than NET 4.5. But I don't know of any way to translate that method into VBscript.

I've constructed a clumsy, partial solution using SendKeys to send an end-of-stream sequence into the scripts' keyboard buffer. But the solution leaves keys in the buffer if STDIN is redirected, which I can't clean up unless I know that STDIN was redirected... so, same problem.

I'd prefer to keep the script in one packaged piece, so I'd rather avoid a separate wrapping script or anything not available on a generic Windows 7+ installation.

Any brilliant ideas or workarounds?

EDIT: added copy of initial solution

I've added a copy of my improved initial solution here (admittedly, a "hack"), which now cleans up after itself but still has several negatives:

input = ""
stdin_is_tty = False
test_string_length = 5 ' arbitrary N (coder determined to minimize collision with possible inputs)
sendkey_string = ""
test_string = ""
for i = 1 to test_string_size
   sendkey_string = sendkey_string & "{TAB}"
   test_string = test_string & CHR(9)
next
sendkey_string = sendkey_string & "{ENTER}"
wsh.sendkeys sendkey_string ' send keyboard string signal to self
set stdin = WScript.StdIn
do while not stdin.AtEndOfStream
   input = input & stdin.ReadLine
   if input = test_string then
       stdin_is_tty = True
   else
       input = input & stdin.ReadAll
   end if
   exit do
loop
stdin.Close
if not stdin_is_tty then
   set stdin = fso.OpenTextFile( "CON:", 1 )
   text = stdin.ReadLine
   stdin.Close
end if

This solution suffers from the three problems:

  1. leaving a visible trace at the command line (though now, just a single blank line which is low visibility)

  2. possible collision of the test string (a set series of N [coder determined] TABs followed by a NEWLINE) with the first line of any redirected input causing a false positive redirection determination. Since the number of TABs can be modified, this possibility can be made arbitrarily low by the coder.

  3. a race condition that if another window receives focus before the SendKeys portion is executed, the wrong window will receive the code string, leading to a false negative redirection determination. My estimate is that the possibility of this circumstance occurring is very low.

Community
  • 1
  • 1
rivy
  • 1,570
  • 1
  • 15
  • 30

1 Answers1

5

In short, no, but ...

I've tested everything i could think of and have not found a reasonable way to do it.

None of the properties/methods exposed by the TextStream wrappers retrieved with WScript.StdIn or fso.GetStdStream give enough information to determine if the input is redirected/piped.

Trying to obtain information from the behaviour/environment of a spawned process (how to create the executable is other story) is also unlikely to be useful because

  • WshShell.Execute always spawns the process with its input and output handles redirected

  • WshShell.Run creates a new process that does not inherit the handles of the current one

  • Shell.Application.ShellExecute has the same problem as WshShell.Run

So, none of these methods allow the spawned process to inherit the handles of the current process to check if they are redirected or not.

Using WMI to retrieve information from the running process does not return anything usable (well, HandleCount property for the process differs when there is a redirection, but it is not reliable)

So, not being able to determine from vbs code if there is a redirection, the remaining options are

  1. Don't detect it: If the piped input must be present, behave as the more command and in all cases try to retrieve it

  2. Indicate it: If the pipe input is not always required, use an argument to determine if the stdin stream needs to be read.

In my case, I usually use a single slash / as argument (for coherence with some of the findstr arguments that also use a slash to indicate stdin input). Then in the vbs code

If WScript.Arguments.Named.Exists("") Then 
    ' here the stdin read part
End If
  1. Check before: Determine if there is redirection before starting the script. A wrapper .cmd is needed, but with some tricks both files (.cmd and .vbs) can be combined into one

To be saved as .cmd

<?xml : version="1.0" encoding="UTF-8" ?> ^<!------------------------- cmd ----
@echo off
    setlocal enableextensions disabledelayedexpansion
    timeout 1 >nul 2>nul && set "arg=" || set "arg=/"
    endlocal & cscript //nologo "%~f0?.wsf" //job:mainJob %arg% %*
    exit /b
---------------------------------------------------------------------- wsf --->
<package>
  <job id="mainJob">
    <script language="VBScript"><![CDATA[
        If WScript.Arguments.Named.Exists("") Then
            Do Until WScript.StdIn.AtEndOfStream
                WScript.StdOut.WriteLine WScript.StdIn.ReadLine
            Loop
        Else
            WScript.StdOut.WriteLine "Input is not redirected"
        End If
    ]]></script>
  </job>
</package>

It is a .wsf file stored inside a .cmd. The batch part determines if the input is redirected (timeout command fails to get a console handle on redirected input) and pass the argument to the script part.

Then, the process can be invoked as

< inputfile.txt scriptwrapper.cmd             input redirected
type inputfile.txt | scriptwrapper.cmd        input piped
scriptwapper.cmd                              no redirection

While this is a convenient way to handle it, the invocation of the .wsf part from the .cmd, while being stable and working without problems, relies in an undocumented behaviour of the script host / cmd combination.

Of course you can do the same but with two separate files. Not as clean, but the behaviour is documented.

MC ND
  • 69,615
  • 8
  • 84
  • 126
  • Thanks for the investigative work. You described the situation very well and supplied a reasonable alternative. I've decided on a streamlined and improved version of my initial hack using `SendKeys`, so that it only leaves a single extra line on-screen as evidence of the test. I think the best course will be to add my solution to the question and mark your answer as correct. – rivy Nov 05 '14 at 16:56