Real time console output from WScript.Shell Exec - excel

I spent most of the day searching for a solution to this, I'm starting to think its maybe not possible for my requirements
My basic setup is to run a vbscript (.vbs) called from an excel vba code. The vba code has to continue on and leave the vbscript running, but will monitor it from time to time using Exec.Status
In the vbscript I'm using WScript.StdOut.WriteLine "whatever" to track/debug it's progress, but as it stands I can only read it's output after the excel vba code is finished what it needs to do.
What I want is to see a real time output to the console from the vbscript
Here's the vba code...
Dim WSH As IWshRuntimeLibrary.WshShell 'Windows Script Host Object Model
Dim Exec As WshExec
Set WSH = CreateObject("WScript.Shell")
Set Exec = WSH.Exec("%COMSPEC% /C CSCRIPT.EXE //nologo " _
& VbsFileDir _
& " " & Arg1 _
& " " & Arg2 _
& " " & Arg3 _
& " " & Arg4)
I have been able to get a real time output by converting from WSH.Exec to WSH.Run, but I do need the access to Exec.Status, which is not available under WSH.Run
UPDATE - 2015-02-06
To clarify further... Using the example '...B.vbs' code provided by #Ekkehard.Horner's answer... The following excel-vba code WILL display a real-time output to the console...
WSH.Run("cscript C:\28353522-B.vbs")
...but the following WILL NOT display anything to the console
WSH.Exec("cscript C:\28353522-B.vbs")
I can't use the .Run() because I use the .Status flag from .Exec()
Also I can't just move the vbscript into the VBA code because the VBA goes on to do other tasks in parallel with the vbscript.
P.s. If anyone can submit an answer explaining why it can't be done, then I will mark that as accepted.

Use .Stdout.ReadLine() until the process has finished and .Stdout.ReadAll() to slurp the rest of the output - as in
28353522-A.vbs
Option Explicit
Const WshFinished = 1
Dim oExc : Set oExc = CreateObject("WScript.Shell").Exec("cscript 28353522-B.vbs")
WScript.Echo "A", "start"
Do While True
If oExc.Status = WshFinished Then
WScript.Echo "A", "WshFinished"
Exit Do
End If
WScript.Sleep 500
If Not oExc.Stdout.AtEndOfStream Then WScript.Echo "A", oExc.Stdout.ReadLine()
Loop
If Not oExc.Stdout.AtEndOfStream Then WScript.Echo "A", oExc.Stdout.ReadAll()
28353522-B.vbs
Option Explicit
Dim i
For i = 1 To 10
WScript.Echo "B", i, "whatever"
WScript.Sleep 100
Next
output:
cscript 28353522-A.vbs
A start
A B 1 whatever
A B 2 whatever
A B 3 whatever
A WshFinished
A B 4 whatever
B 5 whatever
B 6 whatever
B 7 whatever
B 8 whatever
B 9 whatever
B 10 whatever
BTW - How did you get real-time output with .Run()?

Why are you running two files? There is no need.
VBA, being full basic, can write to it's own console.
So put your vbs into VBA (you can cut and paste VBS into VBA and it will work if you put sub/end sub around it). To have the VBS run in VBA put a timer that fires it.
Here's a class module for VBA to create/read/write consoles.
'User global var gconsole
Public Function WriteToConsoles(sOut As String)
If IsConsoleAvailable() = True Then
Dim Result As Long, cWritten As Long
Result = WriteConsole(hConsole, ByVal sOut, Len(sOut), cWritten, ByVal 0&)
End If
End Function
Public Sub ExecuteCommand(Cmd As String, ReturnToPrompt As Boolean)
If IsConsoleAvailable() = True Then
If Len(Cmd) <> 0 Then
If ReturnToPrompt = True Then
Shell Environ$("comspec") & " /k " & Cmd
Else
Shell Environ$("comspec") & " /c " & Cmd
End If
End If
End If
End Sub
Public Sub CreateConsole()
If IsConsoleAvailable() = False Then
If AllocConsole() Then
hConsole = GetStdHandle(STD_OUTPUT_HANDLE)
If hConsole = 0 Then MsgBox "Couldn't allocate STDOUT"
Else
MsgBox "Couldn't allocate console"
End If
End If
End Sub
Public Sub CloseConsole()
If IsConsoleAvailable() = True Then
CloseHandle hConsole
hConsole = 0
FreeConsole
End If
End Sub
Public Function IsConsoleAvailable() As Boolean
If hConsole <> 0 Then
IsConsoleAvailable = True
Else
IsConsoleAvailable = False
End If
End Function

I've come up with an answer to my own question. Though this isn't a preferred solution (as I will explain below), so I'll not be marking as correct, but maybe someone can fix the issues with solution? (if so, post an answer and I'll mark as correct)
First off, +1 to #Ekkehard.Horner's answer for inspiring this solution.
Create the file 'B.vbs' representing my main vbscript to be run.
Option Explicit
Dim i
For i = 1 to 10
Wscript.Echo "B", i, "whatever"
Wscript.Sleep 100
Next
Create the file 'A.vbs' to act as a middle man between the main vbscript and my Excel VBA code
Option Explicit
Dim WSH
Set WSH = CreateObject("WScript.Shell")
WSH.Run "cscript C:\B.vbs", , True
Set WSH = Nothing
Now the excel VBA code...
Option Explicit
Sub Test()
Dim WSH As IWshRuntimeLibrary.WshShell
Dim Exec As WshExec
Set WSH = CreateObject("WScript.Shell")
Set Exec = WSH.Exec("cscript C:\A.vbs")
'Showing that I can still access the Exec.Status
While Exec.Status = WshRunning
Debug.Print "Running"
Wend
'But downside is nothing is avaiable from Stdout
Debug.Print Exec.StdOut.ReadAll
Set Exec = Nothing
Set WSH = Nothing
End Sub
So the Excel VBA calls the 'A.vbs' still using WSH.Exec(), then that will call the 'B.vbs' using WSH.Run(), which opens a second console window which will display the real-time output
Advantages
Excel VBA can still monitor the Exec.Status accurately
Progress of 'B.vbs' can be viewed from real-time console output
Disadvantages (reasons I'm not marking as correct)
The Exec.Terminate() will only terminate the 'A.vbs' (first console window), the 'B.vbs' will remain running
The Exec.StdOut. cannot read the output from 'B.vbs'

Related

VBA generates Error on Mac but not Windows

This VBA code tracks the activity of PowerPoint slides and store the record in an Excel worksheet, saved on my local drive (same folder as the slides):
Dim slideShowRunning As Boolean
Dim counter As Integer
Dim st As Dat
Dim i As Integer
Dim sttime As Date
Dim oxlapp As Object
Dim oxlwb As Object
Dim oxlws As Object
Dim edtime As Date
Sub SlideShowBegin(ByVal Wn As SlideShowWindow)
st = Date
sttime = Time
counter = 0
Debug.Print " works;1 "
Set oxlapp = CreateObject("Excel.Application")
Debug.Print " works; 2"
oxlapp.Visible = False
Debug.Print " works; 3"
Set oxlwb = oxlapp.Workbooks.Open(ActivePresentation.Path & "\" & "record.xlsx")
Debug.Print " works; 4"
Set oxlws = oxlwb.Sheets("TimeRecord")
Debug.Print " works; 5"
i = oxlws.Range("A99919").End(-4162).Row
oxlws.Range("A1").Offset(i, 0).Value = st
oxlws.Range("A1").Offset(i, 1).Value = sttime
Debug.Print " works; 6"
End Sub
Public Sub OnSlideShowPageChange(ByVal Wn As SlideShowWindow)
If TypeName(slideShowRunning) = "Empty" Or slideShowRunning = False Then
slideShowRunning = True
SlideShowBegin Wn
End If
End Sub
Public Sub OnSlideShowTerminate(ByVal Wn As SlideShowWindow)
Name = Application.ActivePresentation.Name
slideShowRunning = False
edtime = Time
Debug.Print " works; 7"
ivalue = DateDiff("s", sttime, edtime)
Debug.Print ivalue
oxlws.Range("A1").Offset(i, 2).Value = edtime
oxlws.Range("A1").Offset(i, 3).Value = ivalue
oxlws.Range("A1").Offset(i, 4).Value = Name
Debug.Print " works; 9"
oxlapp.DisplayAlerts = False
Debug.Print " works; 10"
oxlwb.Save
Debug.Print " works; 11"
oxlapp.Visible = True
Debug.Print " works; 12"
oxlapp.DisplayAlerts = True
Debug.Print " works; 13"
End Sub
Note:
The code stores the PowerPoint slide Name along with slide opening
time and slide closing time.
The details are stores in Excel Sheet.
I have many slides all with same code. The code is working fine in Windows.
The code won't work when I run it on MAC.
I know there are few changes that need to be done to make it work on Mac but can't figure out what. Any help would be much appreciated.
(Hijacking AlexG's explanation)
From Wikipedia:
A path is a string of characters used to uniquely identify a location in a directory structure. It is composed by following the directory tree hierarchy in which components, separated by a delimiting character, represent each directory. The delimiting character is most commonly the slash (/), the backslash character (\), or colon (:), though some operating systems may use a different delimiter.
For example,
Classic Mac OS used : as a directory separator (eg., Macintosh HD:Documents:Letter)
Current macOS uses / as a directory separator (eg., /home/user/docs/Letter.txt)
Windows can use either \ or / as a directory separator (eg., C:\user\docs\Letter.txt)
Rather than trying to remember all the different symbols, there's a VBA property called Application.PathSeparator, which returns the path separator for the current operating system.
So, try changing your code from:
Set oxlwb = oxlapp.Workbooks.Open(ActivePresentation.Path & "\" & "record.xlsx")
...to:
Set oxlwb = oxlapp.Workbooks.Open(ActivePresentation.Path & Application.PathSeparator & "record.xlsx")
...and maybe that will solve your problem.
If not, you'll need to provide more specific information about what error you're getting and where.
I can't test it (and you'll find very little support for Excel on Mac) since not very many people use Excel on Mac — especially VBA. (Personally, the last time I touched a Macintosh was ~1986.)
There are several differences between Excel for Mac and Excel for Windows. You can read more about them here, perhaps starting with this explanation.

How to loop through files sorted by modified date with VBA?

I have gazillion files in a folder, they have been uploaded there every 30 minutes for a few years now.
I have a macro to open and modify files with or after specified last modified date, however it checks every file in the folder to determine if it satisfies the condition or not which takes forever.
Is there a way to first sort files in descending order by modified date and set macro to go from the top and just break once a specified date is reached?
Many thanks in advance!
If you need to keep all the files in one folder then something like this might be faster: you only need to check the modified date until you reach the first "too old" file.
Sub tester()
Const FOLDER As String = "C:\Tester\"
Dim arr, f
arr = AllFilesNewestFirst(FOLDER & "*")
For Each f In arr
If Len(f) > 0 Then
'process files until last modified is >10 days ago
If FileDateTime(FOLDER & f) < Date - 10 Then Exit For
'process this file
Debug.Print "Processing: " & f
End If
Next f
End Sub
'return an array of all files matching `pattern`, using dir
Function AllFilesNewestFirst(pattern)
Dim s As String
Dim oShell As Object
Dim oExec As Object, cmd
Set oShell = CreateObject("WScript.Shell")
cmd = "cmd /c dir """ & pattern & """ /A-D-H-S /b /o-d"
s = oShell.Exec(cmd).StdOut.readall()
AllFilesNewestFirst = Split(s, vbCrLf)
End Function
Perhaps use DateLastModified. If it's before/after a date, then you can run the rest of the macro:
Sub ShowFileAccessInfo(filespec)
Dim fs, f, s
Set fs = CreateObject("Scripting.FileSystemObject")
Set f = fs.GetFile(filespec)
s = UCase(filespec) & vbCrLf
s = s & "Created: " & f.DateCreated & vbCrLf
s = s & "Last Accessed: " & f.DateLastAccessed & vbCrLf
s = s & "Last Modified: " & f.DateLastModified
MsgBox s, 0, "File Access Info"
End Sub
Admittedly, it requires each file to be opened, but it would work.
Edit: Using the above, you could even create a quick function:
Function modified_date(filespec) As Date
Dim fs, f, s
Set fs = CreateObject("Scripting.FileSystemObject")
Set f = fs.GetFile(filespec)
modified_date = Format(f.datelastmodified, "MM/DD/YYYY")
End Function
Sub t()
Dim myFile As String
myFile = "C:\User\Documents\SomeFile.pdf"
Dim oldestDate As Date
oldestDate = Format("10/1/2010", "MM/DD/YYYY")
Dim fileModifiedDate As Date
fileModifiedDate = modified_date(myFile)
Debug.Print "File was last modified on: " & filemodified; Date
Debug.Print "You want to run this on files modified after: " & oldestDate
If oldestDate <= fileModifiedDate Then
Debug.Print "This file is modified AFTER your chosen date"
' Do stuff
End If
End Sub
OK, so this is going to get painful, but it should be faster once you get it working. We'll use the DIR command of the CMD shell and send the output to a file.
Use the Shell VBA command to execute CMD with a "dir /o:d /t:w *. > mylisting.txt". This will create a date-sorted listing of your files (oldest first) and put it in a file called mylisting.txt.
Wait around until the shell command finishes. Maybe loop 10 ten times, sleeping for a few seconds and then looking for your file to be created. Handle the error if the file doesn't show up.
Read the file into a new worksheet as text file.
Starting at the bottom of that new sheet, parse out the file name and the write date. Regular expression functions might be useful for this.
Execute your file-by-file logic.
Loop up to the top entry until the write date < your trigger date.
Clean up.

open .exe in VBA excel and write in the input window

I have to run an .exe in VBA Excel and write in the input window "in.txt" "out.txt" in order to make the process automatic inside a macro. I tried to use shell but it works asynchrounous and I also don't know how to tell her to write inside the .exe.
I've also tried with SendKeys but apperently it doesen't work.
How could I make the VBA calling my .exe, open it, write inside the command window of the .exe, wait for the output and use it to go on?
thank you in advance
here are two attempts (both failed):
Sub write()
prog = Shell("C:\Users\arancia\Pictures\Camera Roll\axtur\axtur\AXTUR_64.exe", 1)
Application.Run "'AXTUR&EXCEL.xlsm'!inserisci_dati_input"
SendKeys.send "in.txt~", True
SendKeys.send "out.txt~", True
SendKeys "%{F4}", True
End Sub
Sub StartExeWithArgument()
Dim strProgramName As String
Dim strArgument As String
strProgramName = "C:\Users\arancia\Pictures\Camera Roll\axtur\axtur\AXTUR_64.exe"
strArgument = "in.txt~out.txt~"
Call Shell("""" & strProgramName & """ """ & strArgument & """", vbNormalFocus)
End Sub
One solution would be to write a batch file that includes all the parameters and then run the batch.
I have used WshShell (Windows scripting host) to run batch files to do what you want in the past but WshShell does not work on our computers since the Nov 2020 updates. WshShell allows you to wait for the outcome of the external program.
One way I found to go around it is to write a simple text file at the end of the batch and wait for it to show up. This is crude but it works.
In the code below, I write a simple batch file in the folder of the Excel sheet. The last line of the batch writes the content of the folder in a text file. The Do Until loop waits for the text file to show up in 1 second increments. When the code resumes after the loop, the text file is deleted. If you write the command line you would type in cmd instead of "echo Hello World" this should work.
You need to reference the Microsoft Scripting Runtime (scrrun) to use the file system object.
Good Luck!
Public Sub RunBatch()
Dim i As Integer
Dim k As Integer
Dim xlWB As Workbook
Dim fso1 As New FileSystemObject
Dim BatFile As Object
Dim IsDone As Boolean
Dim OutFileName As String
Set xlWB = ThisWorkbook
OutFileName = xlWB.Path & "\" & "HW.bat"
Set BatFile = fso1.CreateTextFile(OutFileName)
BatFile.WriteLine "cd /d " & xlWB.Path
BatFile.WriteLine "echo Hello World"
BatFile.WriteLine "dir > Done.txt"
BatFile.Close
IsDone = False
Call Shell(OutFileName, vbNormalFocus)
Do Until IsDone
If fso1.FileExists(xlWB.Path & "\Done.txt") Then IsDone = True
Application.Wait (Now + TimeValue("00:00:01"))
Loop
fso1.DeleteFile (OutFileName)
End Sub

Late bound MSForms.DataObject works in VBA but only partially in VBScript

I sometimes use the MSForms.DataObject object from the Microsoft Forms 2.0 Object Library in Excel VBA. It is absolutely wonderful for reading / writing text data from / to the clipboard. Recently, I stumbled across this article which shows how to instantiate the object using late binding and found that it works beautifully in VBA. Now, I don't have to worry about adding the reference library each time I port my code to new projects.
That discovery made me wonder if it were possible to use the same object in VBScript. There have been several instances in the past when I wanted to manipulate the clipboard with VBScript but all my research at the time indicated that it wasn't possible (aside from using internet explorer, mshta, clip, etc). To my surprise, the DataObject worked exactly as expected when I tried to read the clibboard. However, it would not put data back into the clipboard and threw an error which makes no sense to me. Below are the details.
Error Number: -2147221008 (800401F0)
Error Description: DataObject:PutInClipboard CoInitialize has not been called.
So, is there a workaround for the error I'm getting or is it simply part of the same VBScript limitation described on MSDN and this answer?
Here is the VBScript code I used for testing on my 64 bit Windows 7 PC:
Option Explicit
Dim DObj
Sub TestClipboard()
Dim ClipData
VerifyArchitecture
If Not InitClipboardObject Then
Terminate "Unable to initialize the clipboard object"
ElseIf Not ClipboardPaste(ClipData) Then
Terminate "Unable to retrieve the clipboard data"
End If
' The message box will have the current clipboard text (if any exist)
MsgBox "The clipboard contains the following text:" & _
vbCrLf & vbCrLf & ClipData
ClipData = "Text we put in the clipboard"
' However, this function will not succeed.
If Not ClipboardCopy(ClipData) Then Terminate "Unable to put data into the clipboard"
End Sub
Function InitClipboardObject()
On Error Resume Next
' If the code is run in VBA, the following reference library
' can be used as an alternative to late binding:
' Microsoft Forms 2.0 Object Library
' Note: The reference library will not show up on the
' list unless a userform has already been added in Excel.
' If not, browse for the FM20.DLL file
Set DObj = GetObject("new:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}")
InitClipboardObject = Err = 0
End Function
' Put data in the clipboard similar to pressing Ctrl + C
Function ClipboardCopy(ByVal Data)
On Error Resume Next
DObj.SetText Data
' This line of code will throw the following error
' Error Number: -2147221008 (800401F0)
' Description: DataObject:PutInClipboard CoInitialize has not been called.
' However, it works perfectly in VBA
DObj.PutInClipboard
ClipboardCopy = Err = 0
End Function
' Retrieve data from the clipboard similar to pressing Ctrl + V
Function ClipboardPaste(ByRef Data)
On Error Resume Next
DObj.GetFromClipboard
Data = DObj.GetText(1)
ClipboardPaste = Err = 0
End Function
' This sub will re-load the script using the 32 bit host
' if it is loaded on the 64 bit version. This is necessary
' since the clipboard object is 32 bit.
Sub VerifyArchitecture()
' The code in this sub is a modified version of Vozzie's answer
' and I do not take credit for the idea:
' https://stackoverflow.com/a/15320768/2734431
Dim Arch, Arg, Args, Cmd, ExeFullName, ExeShortName
Dim Path32, Path64, ProcEnv, q, Reload, ScriptName
Dim WinDir, WShell
q = Chr(34)
Reload = False
ExeFullName = WScript.FullName
ScriptName = WScript.ScriptFullName
ExeShortName = Mid(ExeFullName, InStrRev(ExeFullName, "\") + 1)
Set WShell = CreateObject("WScript.Shell")
Set ProcEnv = WShell.Environment("Process")
WinDir = ProcEnv("windir") & "\"
Path32 = WinDir & "SysWOW64\"
Path64 = WinDir & "System32\"
Arch = ProcEnv("PROCESSOR_ARCHITECTURE")
For Each Arg In WScript.Arguments
Args = " " & q & Arg & q
Next
Cmd = q & Path32 & ExeShortName & q & " " & q & ScriptName & q & Args
If InStr(LCase(ExeFullName), LCase(Path64)) <> 0 And Arch = "AMD64" Then
Reload = True
WShell.Run Cmd
End If
Set WShell = Nothing
Set ProcEnv = Nothing
If Reload Then Terminate ""
End Sub
' This sub is designed to clear any global variables, optionally
' display an error message, and stop the script
Sub Terminate(ByVal ErrMsg)
Dim ErrNbr
Set DObj = Nothing
If ErrMsg <> "" Then
ErrNbr = "Error"
If Err <> 0 Then
ErrNbr = ErrNbr & " " & Err & " (" & Hex(Err) & ")"
ErrMsg = ErrMsg & vbCrLf & vbCrLf
ErrMsg = ErrMsg & "Code Error: " & Err.Description
End If
' &H10 = vbCritical
MsgBox ErrMsg, &H10, ErrNbr
End If
WScript.Quit
End Sub

Extracting a row from a CSV file quickly in Excel VBA

I have about 5000 .csv files and I want to search for one row in each file and extract it. I have pasted the key part of code below, which works, but as I have to open and close each .csv file, the process is slow for 5000 files. Is there any way to read a csv file without opening it? I had considered writing a small script to convert each csv file to Excel first? Thx.
SP_File_Name = Dir(DN_Path & "*.*")
Count = 1
Set START_CELL_RANGE = TARGET_SP_SHEET.Range("B3")
Set TICKER_CODE_RANGE = TARGET_SP_SHEET.Range("B1")
While (SP_File_Name <> "")
SP_Full_Path = DN_Path & SP_File_Name
Workbooks.OpenText Filename:=SP_Full_Path, DataType:=xlDelimited, comma:=True, Local:=True
Set INPUT_WORKBOOK = ActiveWorkbook
Set INPUT_SHEET = INPUT_WORKBOOK.Worksheets(1)
INPUT_SHEET.Range("$A$1").Select
Set INPUT_RANGE = ActiveCell.CurrentRegion
Set INPUT_FIRST_MATCH_RANGE = INPUT_RANGE.Find(TICKER_CODE_RANGE)
If INPUT_FIRST_MATCH_RANGE Is Nothing Then
GoTo NOT_FOUND
End If
START_CELL = START_CELL_RANGE.Address
TARGET_SP_SHEET.Range(START_CELL_RANGE.Address, START_CELL_RANGE.Offset(0, 6).Address).Value = INPUT_SHEET.Range(INPUT_FIRST_MATCH_RANGE.Address, INPUT_FIRST_MATCH_RANGE.Offset(0, 7).Address).Value
' write diagnostics
Sheet5.Range("K" & Count + 4).Value = START_CELL
Sheet5.Range("L" & Count + 4).Value = "$A$1"
Sheet5.Range("M" & Count + 4).Value = INPUT_FIRST_MATCH_RANGE.Address
Sheet5.Range("N" & Count + 4).Value = INPUT_FIRST_MATCH_RANGE.Offset(0, 7).Address
NOT_FOUND:
Set START_CELL_RANGE = START_CELL_RANGE.Offset(1, 0)
Workbooks(SP_File_Name).Close SaveChanges:=False
SP_File_Name = Dir
Count = Count + 1
Wend
To call a cmd command from VBA, I have used WshShell. For early binding I set a reference to the Windows Script Host Object Model
One problem with the Shell function is that it runs asynchronously. By using the WshShell Run method, you can have it wait until finished before executing subsequent commands.
Sample code might look as follows:
Option Explicit
Sub foo()
Dim WSH As WshShell
Dim lErrCode As Long
Set WSH = New WshShell
lErrCode = WSH.Run("cmd /c findstr /C:""Power"" ""C:\Users\Ron\filelist.txt"" > ""C:\Users\Ron\Results2.txt""", 1, True)
If lErrCode <> 0 Then
MsgBox "Error Code: " & lErrCode
Stop
End If
Set WSH = Nothing
Call Shell
End Sub
With regard to your command that you showed in your comment, I would ensure that VBA is interpreting the string correctly for the cmd prompt. Looking at your code line, I would wonder whether you are missing a space between the search string and the file path.
I don't think you can read the contents of a file without opening it. Why not just merge all 5000 files into 1 single file and read that into Excel. Certainly that will be much faster. Use the Command Window, point it to the folder that contains all 5000 files, and enter this:
copy *.csv merge.csv
See the link below for an example.
http://analystcave.com/merge-csv-files-or-txt-files-in-a-folder/

Resources