Running 7Z in a dos command from VBA Excel causes warning alert - vba

I am currently using the following code to run a dos command as follows from VBA.
Set objShell = CreateObject("WScript.Shell")
dos_command="\\\10.xx.xx.xx\test\7z.exe a -r " etc etc etc
result = objShell.Run(dos_command, 0, True)
Set objShell =nothing
All runs well, the only problem is that I get an annoying Warning Windows Box advising a program is trying to run in my computer, press OK or Cancel
I must use "objshell" because I need VBA to wait until DOS command is completed.
is there a way to avoid the warning box from coming up from within VBA or adding some additional parameters to the DOS command ?
The 7z.exe file is running in a server (not local PC) so I assume that's the problem.
I cannot use or install 7z.exe in each machine.

Here are three options, presented in order from quickest/dirtiest to most robust:
Create a text file as part of command line and wait for its existence: modify your command line to something like this and run it using Shell (not your objShell):
dos_command = "\\\10.xx.xx.xx\test\7z.exe a -r " etc etc etc
dos_command = dos_command & " && echo > " & TempFileName
This will create a text file named TempFileName after your 7-zip code completes. You just need to make sure TempFileName does not exist before you run your shell command, then run the command and wait for the TempFileName file to exist.
Use OpenProcess and GetExitCodeProcess APIs: launch your command line using the OpenProcess API call which provides access to your new process (note that the Shell function returns the ProcessID of the launched process). Then use the ProcessID to sit in a loop and poll the process via GetExitCodeProcess. Relevant declarations:
Private Declare Function OpenProcess Lib "kernel32" _
(ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function GetExitCodeProcess Lib "kernel32" _
(ByVal hProcess As Long, _
lpExitCode As Long) As Long
Private Const STILL_ACTIVE = &H103
Private Const PROCESS_QUERY_INFORMATION = &H400
'---------------------------------------------------------------------------------------vv
' Procedure : ShellWait
' DateTime : 2/15/2008 10:59
' Author : Mike
' Purpose : Executes a shell command and waits for it to complete.
' Notes : Runs the shell as a batch file, allowing the user to pass a string with
' line breaks to execute a multi-line command.
'
' : Provides two means to break out of the loop.
' 1) Provide a timeout in seconds.
' The code breaks out once it reaches the timeout.
' 2) Provide a flag to tell the procedure to stop running.
' To use this option, you would need to pass the procedure a global flag
' that the user has the ability to change through the interface.
' Update (5/23/2008):
' - Uses a progressive sleep timer to allow fast processes to run quickly
' and long processes to get increasing clock cycles to work with.
' - Changed default window mode to hidden.
'---------------------------------------------------------------------------------------
'^^
Public Function ShellWait(DosCmd As String, _
Optional StartIn As String = "WINDOWS TEMP FOLDER", _
Optional WindowStyle As VbAppWinStyle = vbHide, _
Optional TimeOutSeconds As Long = -1, _
Optional ByRef StopWaiting As Boolean = False) 'vv
On Error GoTo Err_ShellWait
Dim hProcess As Long, RetVal As Long, StartTime As Long
Dim BatName As String, FileNum As Integer, SleepTime As Long
StartTime = Timer
BatName = TempFileName(StartIn, "bat")
FileNum = FreeFile()
Open BatName For Output As #FileNum
ChDrive Left(BatName, 1)
ChDir Left(BatName, InStrRev(BatName, "\"))
Print #FileNum, DosCmd
Close #FileNum
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, False, Shell(BatName, WindowStyle))
SleepTime = 10
Do
'Get the status of the process
GetExitCodeProcess hProcess, RetVal
DoEvents: Sleep SleepTime
If TimeOutSeconds <> -1 Then
If Timer - StartTime > TimeOutSeconds Then Exit Do
End If
If StopWaiting Then Exit Do
'Progressively increase the SleepTime by 10%
' This allows a quick process to finish quickly, while providing
' a long process with increasingly greater clock cycles to work with
SleepTime = SleepTime * 1.1
Loop While RetVal = STILL_ACTIVE
Kill BatName
Exit_ShellWait:
Exit Function
Err_ShellWait:
MsgBox Err.Description
Resume Exit_ShellWait
End Function
'---------------------------------------------------------------------------------------vv
' Procedure : TempFileName
' DateTime : 12/9/08
' Author : Mike
' Purpose : Returns an unused file name but does not create the file. Path can be
' passed with or without the trailing '\'.
' Requires : TempPath() function
'---------------------------------------------------------------------------------------
'^^
Function TempFileName(Optional ByVal Path As String = "WINDOWS TEMP FOLDER", _
Optional Ext As String = "txt", _
Optional Prefix As String = "temp") As String 'vv
Dim TempFName As String, i As Integer
If Path = "WINDOWS TEMP FOLDER" Then Path = TempPath
If Right(Path, 1) <> "\" Then Path = Path & "\"
If Not (Path Like "?:\*" Or Path Like "\\*") Then
Err.Raise 52 '"Bad file name or number."
ElseIf Dir(Path, vbDirectory) = "" Then
Err.Raise 76 '"Path not found."
End If
TempFName = Path & Prefix & "." & Ext
For i = 1 To 500
If Dir(TempFName) = "" Then
TempFileName = TempFName
GoTo Exit_TempFileName
End If
TempFName = Path & Prefix & "_" & Format(i, "000") & "." & Ext
Next i
TempFileName = ""
End Function
'---------------------------------------------------------------------------------------
' Procedure : TempPath
' Author : Mike
' Date : 8/12/2008
' Purpose : Returns something like:
' C:\DOCUME~1\BGRAND~1\LOCALS~1\Temp\
'---------------------------------------------------------------------------------------
'^^
Function TempPath() As String 'vv
Const TemporaryFolder = 2
Static TempFolderPath As String
Dim fs As Object
If Len(TempFolderPath) = 0 Then
Set fs = CreateObject("Scripting.FileSystemObject")
TempFolderPath = fs.GetSpecialFolder(TemporaryFolder) & "\"
End If
TempPath = TempFolderPath
End Function
Use CreateProcess and WaitForSingleObject APIs: refer to the "Super Shell" example at this help page for CreateProcess

Calling Microsoft® Windows® Script Host causes windows to display the message. Instead try this
Public Sub test()
Dim dos_command$, lRet&
dos_command = """\\xxx.xxx.xxx.xxx\xxx\xxx\7z.exe"" a test.zip ""\\xxx.xxx.xxx.xxx\xxx\xxx\*.log"" -r"
lRet = Shell(dos_command, vbMaximizedFocus)
MsgBox lRet
End Sub
UPDATE
You may do the following and use your code:
Open Start | Run and type gpedit.msc. Click OK
User Configuration >> Administrative Templates >> Windows Components >> Attachment Manager
Add 7z.exe to the Inclusion list for moderate risk file types setting.
Hpe this helps

Related

Use Console as debug window in VBA

So I have some macros run inside an Excel document and wanted to know if there is a way to frequently output text to a Console window (Basicly using it like the immediate window).
I know there are multiple ways of writing text to files, I just want to display some info on the running process without using the immediate window or other windows inside Excel itself.
Using this helps me to display a single line, but I dont want to open a new window for every line:
Call Shell("cmd.exe /K echo testInfo", vbNormalFocus)
I do NOT want to run a command (except echo maybe?) to execute a task, its just supposed to display text.
Thank you in advance for any advice.
EDIT:
As an addition to #JohnRC 's post I found a solution without external applications:
Call Shell("PowerShell.exe -noexit -command get-content " + strPath + " -wait")
Logging information to a textfile at the location after running the command above does the trick.
OK, as I got a couple of downvotes on my earlier answer, I thought I should attempt to provide an actual answer to the request, namely to provide a way of sending log messages to a command prompt window. Here goes...
This solution is implemented as a VBA class that will send messages as comment lines to a separately-running command prompt window that has the text "ExcelLog" in the title. This command prompt must be started separately. The easiest way to do this is to create a shortcut named "ExcelLog" to just run CMD, then when this shortcut is opened the command prompt window will have "ExcelLog" in the title.
Inside the spreadsheet add the code for the class cConsole (below), and then in your VBA code create a global instance of the class and use the method .W "message" to send a text message to the console as a comment line (in this case using the prefix :: to the line to identify it as a comment).
The cConsole class looks for any command prompt window with the requisite title, then sends the comment message to that window. If the window cannot be found, it simply skips the action so the Excel VBA code continues to execute without reporting an error. Also if you open the command prompt window after Excel VBA has started running, cConsole will automatically connect to the window and start/resume sending messages. This means you can close and reopen the command prompt ExcelLog window any time without interrupting the execution of the VBA code.
This seems to work on my setup OK. I think it is a bit more trouble than simply tailing a text file, but - hey, you pays your money and takes your choice.
Here is the code of the cConsole class.
Option Explicit
'// cConsole class
'// This class wraps an interface to a separately-started command prompt
'// window to which messages are sent as comments, so that the command prompt
'// window can be used as a real-time scrolling log from Excel.
'// Each instance of this class creates its own connection to the
'// command prompt window which must have a title containing the text
'// "ExcelLog". If such a window is not open then messages are not
'// logged. The command prompt window can be opened after messages
'// have started, and it will be connected when the next message is
'// sent.
'// The simplest way to set up the necessary command prompt window is to
'// create a shortcut on the desktop the name "ExcelLog" which runs CMD
'// Usage - - - - - - - - - - - -
'//
'// Dim oConsole As New cConsole
'// :
'// oConsole.W "Message to be written to the console"
'//
'// Windows functions to get window handles etc
Private Declare Function FindWindowEx Lib "user32" Alias "FindWindowExA" _
(ByVal hWnd1 As Long, ByVal hWnd2 As Long, ByVal lpsz1 As String, ByVal lpsz2 As String) As Long
Private Declare Function GetClassName Lib "user32" Alias "GetClassNameA" _
(ByVal hWnd As Long, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long
Private Declare Function GetWindowText Lib "user32" Alias "GetWindowTextA" _
(ByVal hWnd As Long, ByVal lpString As String, ByVal cch As Long) As Long
Private Declare Function SetForegroundWindow Lib "user32" _
(ByVal hWnd As Long) As Long
'// Handle of the excel log window
Private hLogWindow As Long
Private Sub Class_Initialize()
'// On instantiation, attempts to find the ExcelLog window
findExcelLogWindow
End Sub
Public Sub W(sMsg As String)
'// Public function used to send the given message
'// as a comment line to the linked window
SendToConsole ":: " & sMsg
End Sub
Private Sub SendToConsole(Command As String)
'// Connects to and sends a command line to the command prompt
'// window that is being used as the log
Dim res As Boolean
'// Check that a connection has been made and
'// attempt to connect if not
If hLogWindow = 0 Then
findExcelLogWindow
If hLogWindow = 0 Then Exit Sub
End If
On Error Resume Next
Do
'// Attempt to bring the logging window to the foreground
res = SetForegroundWindow(hLogWindow)
'// Check if successful, and send the command if so
If res Then
SendKeys Command & vbCrLf
Exit Do
Else
'// Not successful, so try reconnecting to the logging window
findExcelLogWindow
'// If we cannot connect, just exit without sending anything
If hLogWindow = 0 Then Exit Sub
End If
Loop
'// Check if there has been any error
If Err.Number <> 0 Then
hLogWindow = 0
MsgBox "Error: " & Err.Number & vbCrLf & Err.Description
End If
On Error GoTo 0
End Sub
Private Function findExcelLogWindow() As Long
'// This function looks for a command prompt window that has the text
'// ExcelLog in the title
Dim nLen As Long
Dim sData As String
Dim Class As String
Dim Title As String
'// Get handle to the first window
hLogWindow = 0
'// Check each window in turn
Do
hLogWindow = FindWindowEx(0&, hLogWindow, vbNullString, vbNullString)
'// Check that a window was found
If hLogWindow = 0 Then Exit Do
'// Get the class name of the window
sData = String$(100, Chr$(0))
nLen = GetClassName(hLogWindow, sData, 100)
Class = Left$(sData, nLen)
'// Get the title of the window
sData = String$(100, Chr$(0))
nLen = GetWindowText(hLogWindow, sData, 100)
Title = Left$(sData, nLen)
'// Check if the required window has been found
If Class = "ConsoleWindowClass" And InStr(Title, "ExcelLog") > 0 Then
'// Initialise the window to remove any prompt text
SendToConsole "PROMPT $S"
'// Write some initial messages
Me.W "*******************"
Me.W "[" & ThisWorkbook.Name & "] connected to console at " & Now
Me.W ""
'// Return the handle to the log window
findExcelLogWindow = hLogWindow
Exit Function
End If
Loop
'// The log window was not found, so return zero
findExcelLogWindow = 0
End Function
I tested this out by handling MouseMove events on an image control in a worksheet:
Option Explicit
Private oCons As New cConsole
Private Sub Image1_MouseMove(ByVal Button As Integer, ByVal Shift As Integer, ByVal X As Single, ByVal Y As Single)
oCons.W "MouseMove " & X & ", " & Y
End Sub
And here is the result
Instead of using a shell to act as a console to log messages, I have used a text file to hold the log, and monitored the output to the file with a tail utility (I have used WinTail from http://www.baremetalsoft.com/wintail/ but I am sure there are others). This is the code, which I put in a separate vba module named Log. Then call Log.W "Message" to log a message.
Option Explicit
'// You need a reference to "Microsoft Scripting Runtime" library in VBA
Private oLog As Scripting.TextStream
Private bErr As Boolean
Private Sub INIT()
'// Initialise the output log file
'// Check if log file is already open, or there has been an error
If bErr Then Exit Sub
If Not oLog Is Nothing Then Exit Sub
'// Open the log file for appending
Dim ofso As New Scripting.FileSystemObject
On Error Resume Next
Set oLog = ofso.OpenTextFile("excel.log", ForAppending, True)
'// Check that open was successful
If Err.Number <> 0 Then
MsgBox "Log file error: " & Err.Number & ": " & Err.Description
bErr = True
Exit Sub
End If
On Error GoTo 0
'// Write a starting block to the log
oLog.WriteLine "*"
W "********************************** START"
W "* Start of log " & Format(Date, "YYYY-MM-dd")
W ""
End Sub
Public Sub W(sMsg)
'// Writes a single line message to the log
'// Initialize if required
INIT
'// Check for log file error
If bErr Then Exit Sub
'// Create the log line and write to log file
Dim st As String
st = Format(Now, "hh:mm:ss ")
oLog.WriteLine st & sMsg
End Sub
Public Function ReportErr(Optional Loc As Variant = "") As Boolean
'// Reports information from the Err object, if an error has occured
'// Check if error has occurred, exit if not
If Err.Number = 0 Then ReportErr = False: Exit Function
'// Set return value
ReportErr = True
'// Initialize if required
INIT
'// Check for log file error
If bErr Then Exit Function
'// Write the error block to the log
W "*********** ERROR ******* " & IIf(Len(Loc) > 0, "[" & Loc & "]", "")
W "* Error #" & Err.Number
If Len(Err.Description) > 0 Then
W "* : " & Err.Description
W "*************************"
End If
End Function
Tailing the log file with WinTail means that output to the log appears immediately it is written, so you can monitor the log as the program is running.

Pass output from powershell script to variable in VBA [duplicate]

Found this function on http://www.cpearson.com/excel/ShellAndWait.aspx
But I would also need to capture the output from the shell. Any code suggestion?
Option Explicit
Option Compare Text
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' modShellAndWait
' By Chip Pearson, chip#cpearson.com, www.cpearson.com
' This page on the web site: www.cpearson.com/Excel/ShellAndWait.aspx
' 9-September-2008
'
' This module contains code for the ShellAndWait function that will Shell to a process
' and wait for that process to end before returning to the caller.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Private Declare Function WaitForSingleObject Lib "kernel32" ( _
ByVal hHandle As Long, _
ByVal dwMilliseconds As Long) As Long
Private Declare Function OpenProcess Lib "kernel32.dll" ( _
ByVal dwDesiredAccess As Long, _
ByVal bInheritHandle As Long, _
ByVal dwProcessId As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" ( _
ByVal hObject As Long) As Long
Private Const SYNCHRONIZE = &H100000
Public Enum ShellAndWaitResult
Success = 0
Failure = 1
TimeOut = 2
InvalidParameter = 3
SysWaitAbandoned = 4
UserWaitAbandoned = 5
UserBreak = 6
End Enum
Public Enum ActionOnBreak
IgnoreBreak = 0
AbandonWait = 1
PromptUser = 2
End Enum
Private Const STATUS_ABANDONED_WAIT_0 As Long = &H80
Private Const STATUS_WAIT_0 As Long = &H0
Private Const WAIT_ABANDONED As Long = (STATUS_ABANDONED_WAIT_0 + 0)
Private Const WAIT_OBJECT_0 As Long = (STATUS_WAIT_0 + 0)
Private Const WAIT_TIMEOUT As Long = 258&
Private Const WAIT_FAILED As Long = &HFFFFFFFF
Private Const WAIT_INFINITE = -1&
Public Function ShellAndWait(ShellCommand As String, _
TimeOutMs As Long, _
ShellWindowState As VbAppWinStyle, _
BreakKey As ActionOnBreak) As ShellAndWaitResult
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' ShellAndWait
'
' This function calls Shell and passes to it the command text in ShellCommand. The function
' then waits for TimeOutMs (in milliseconds) to expire.
'
' Parameters:
' ShellCommand
' is the command text to pass to the Shell function.
'
' TimeOutMs
' is the number of milliseconds to wait for the shell'd program to wait. If the
' shell'd program terminates before TimeOutMs has expired, the function returns
' ShellAndWaitResult.Success = 0. If TimeOutMs expires before the shell'd program
' terminates, the return value is ShellAndWaitResult.TimeOut = 2.
'
' ShellWindowState
' is an item in VbAppWinStyle specifying the window state for the shell'd program.
'
' BreakKey
' is an item in ActionOnBreak indicating how to handle the application's cancel key
' (Ctrl Break). If BreakKey is ActionOnBreak.AbandonWait and the user cancels, the
' wait is abandoned and the result is ShellAndWaitResult.UserWaitAbandoned = 5.
' If BreakKey is ActionOnBreak.IgnoreBreak, the cancel key is ignored. If
' BreakKey is ActionOnBreak.PromptUser, the user is given a ?Continue? message. If the
' user selects "do not continue", the function returns ShellAndWaitResult.UserBreak = 6.
' If the user selects "continue", the wait is continued.
'
' Return values:
' ShellAndWaitResult.Success = 0
' indicates the the process completed successfully.
' ShellAndWaitResult.Failure = 1
' indicates that the Wait operation failed due to a Windows error.
' ShellAndWaitResult.TimeOut = 2
' indicates that the TimeOutMs interval timed out the Wait.
' ShellAndWaitResult.InvalidParameter = 3
' indicates that an invalid value was passed to the procedure.
' ShellAndWaitResult.SysWaitAbandoned = 4
' indicates that the system abandoned the wait.
' ShellAndWaitResult.UserWaitAbandoned = 5
' indicates that the user abandoned the wait via the cancel key (Ctrl+Break).
' This happens only if BreakKey is set to ActionOnBreak.AbandonWait.
' ShellAndWaitResult.UserBreak = 6
' indicates that the user broke out of the wait after being prompted with
' a ?Continue message. This happens only if BreakKey is set to
' ActionOnBreak.PromptUser.
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Dim TaskID As Long
Dim ProcHandle As Long
Dim WaitRes As Long
Dim Ms As Long
Dim MsgRes As VbMsgBoxResult
Dim SaveCancelKey As XlEnableCancelKey
Dim ElapsedTime As Long
Dim Quit As Boolean
Const ERR_BREAK_KEY = 18
Const DEFAULT_POLL_INTERVAL = 500
If Trim(ShellCommand) = vbNullString Then
ShellAndWait = ShellAndWaitResult.InvalidParameter
Exit Function
End If
If TimeOutMs < 0 Then
ShellAndWait = ShellAndWaitResult.InvalidParameter
Exit Function
ElseIf TimeOutMs = 0 Then
Ms = WAIT_INFINITE
Else
Ms = TimeOutMs
End If
Select Case BreakKey
Case AbandonWait, IgnoreBreak, PromptUser
' valid
Case Else
ShellAndWait = ShellAndWaitResult.InvalidParameter
Exit Function
End Select
Select Case ShellWindowState
Case vbHide, vbMaximizedFocus, vbMinimizedFocus, vbMinimizedNoFocus, vbNormalFocus, vbNormalNoFocus
' valid
Case Else
ShellAndWait = ShellAndWaitResult.InvalidParameter
Exit Function
End Select
On Error Resume Next
Err.Clear
TaskID = Shell(ShellCommand, ShellWindowState)
If (Err.Number <> 0) Or (TaskID = 0) Then
ShellAndWait = ShellAndWaitResult.Failure
Exit Function
End If
ProcHandle = OpenProcess(SYNCHRONIZE, False, TaskID)
If ProcHandle = 0 Then
ShellAndWait = ShellAndWaitResult.Failure
Exit Function
End If
On Error GoTo ErrH:
SaveCancelKey = Application.EnableCancelKey
Application.EnableCancelKey = xlErrorHandler
WaitRes = WaitForSingleObject(ProcHandle, DEFAULT_POLL_INTERVAL)
Do Until WaitRes = WAIT_OBJECT_0
DoEvents
Select Case WaitRes
Case WAIT_ABANDONED
' Windows abandoned the wait
ShellAndWait = ShellAndWaitResult.SysWaitAbandoned
Exit Do
Case WAIT_OBJECT_0
' Successful completion
ShellAndWait = ShellAndWaitResult.Success
Exit Do
Case WAIT_FAILED
' attach failed
ShellAndWait = ShellAndWaitResult.Failure
Exit Do
Case WAIT_TIMEOUT
' Wait timed out. Here, this time out is on DEFAULT_POLL_INTERVAL.
' See if ElapsedTime is greater than the user specified wait
' time out. If we have exceed that, get out with a TimeOut status.
' Otherwise, reissue as wait and continue.
ElapsedTime = ElapsedTime + DEFAULT_POLL_INTERVAL
If Ms > 0 Then
' user specified timeout
If ElapsedTime > Ms Then
ShellAndWait = ShellAndWaitResult.TimeOut
Exit Do
Else
' user defined timeout has not expired.
End If
Else
' infinite wait -- do nothing
End If
' reissue the Wait on ProcHandle
WaitRes = WaitForSingleObject(ProcHandle, DEFAULT_POLL_INTERVAL)
Case Else
' unknown result, assume failure
ShellAndWait = ShellAndWaitResult.Failure
Exit Do
Quit = True
End Select
Loop
CloseHandle ProcHandle
Application.EnableCancelKey = SaveCancelKey
Exit Function
ErrH:
Debug.Print "ErrH: Cancel: " & Application.EnableCancelKey
If Err.Number = ERR_BREAK_KEY Then
If BreakKey = ActionOnBreak.AbandonWait Then
CloseHandle ProcHandle
ShellAndWait = ShellAndWaitResult.UserWaitAbandoned
Application.EnableCancelKey = SaveCancelKey
Exit Function
ElseIf BreakKey = ActionOnBreak.IgnoreBreak Then
Err.Clear
Resume
ElseIf BreakKey = ActionOnBreak.PromptUser Then
MsgRes = MsgBox("User Process Break." & vbCrLf & _
"Continue to wait?", vbYesNo)
If MsgRes = vbNo Then
CloseHandle ProcHandle
ShellAndWait = ShellAndWaitResult.UserBreak
Application.EnableCancelKey = SaveCancelKey
Else
Err.Clear
Resume Next
End If
Else
CloseHandle ProcHandle
Application.EnableCancelKey = SaveCancelKey
ShellAndWait = ShellAndWaitResult.Failure
End If
Else
' some other error. assume failure
CloseHandle ProcHandle
ShellAndWait = ShellAndWaitResult.Failure
End If
Application.EnableCancelKey = SaveCancelKey
End Function
Based on Andrew Lessard's answer, here's a function to run a command and return the output as a string -
Public Function ShellRun(sCmd As String) As String
'Run a shell command, returning the output as a string
Dim oShell As Object
Set oShell = CreateObject("WScript.Shell")
'run command
Dim oExec As Object
Dim oOutput As Object
Set oExec = oShell.Exec(sCmd)
Set oOutput = oExec.StdOut
'handle the results as they are written to and read from the StdOut object
Dim s As String
Dim sLine As String
While Not oOutput.AtEndOfStream
sLine = oOutput.ReadLine
If sLine <> "" Then s = s & sLine & vbCrLf
Wend
ShellRun = s
End Function
Usage:
MsgBox ShellRun("dir c:\")
You can CreateProcess the application redirecting its StdOut to a pipe, then read that pipe directly; http://pastebin.com/CszKUpNS
dim resp as string
resp = redirect("cmd","/c dir")
resp = redirect("ipconfig","")
Based on Brian Burns' answer, I added passing input (using StdInput) to the executable during the call. Just in case somebody stumbles upon this and has the same need.
''' <summary>
''' Executes the given executable in a shell instance and returns the output produced
''' by it. If iStdInput is given, it is passed to the executable during execution.
''' Note: You must make sure to correctly enclose the executable path or any given
''' arguments in quotes (") if they contain spaces.
''' </summary>
''' <param name="iExecutablePath">
''' The full path to the executable (and its parameters). This string is passed to the
''' shell unaltered, so be sure to enclose it in quotes if it contains spaces.
''' </param>
''' <param name="iStdInput">
''' The (optional) input to pass to the executable. Default: Null
''' </param>
Public Function ExecuteAndReturnStdOutput(ByVal iExecutablePath As String, _
Optional ByVal iStdInput As String = vbNullString) _
As String
Dim strResult As String
Dim oShell As WshShell
Set oShell = New WshShell
Dim oExec As WshExec
Set oExec = oShell.Exec(iExecutablePath)
If iStdInput <> vbNullString Then
oExec.StdIn.Write iStdInput
oExec.StdIn.Close ' Close input stream to prevent deadlock
End If
strResult = oExec.StdOut.ReadAll
oExec.Terminate
ExecuteAndReturnStdOutput = strResult
End Function
Note: You will need to add a reference to Windows Script Host Object Model so the types WshShell and WshExec are known.
(To do this go to Tools -> References in the VBA IDE's menu bar.)
You can use the following small C# program to test your call from VBA. (If you don't have Visual Studio (Express) handy, you can follow these instructions to quickly compile it from a simple source file.):
using System;
class Program
{
static void Main(string[] args)
{
// Read StdIn
string inputText = Console.In.ReadToEnd();
// Convert input to upper case and write to StdOut
Console.Out.Write(inputText.ToUpper());
}
}
In VBA you could then run the following method that should show you a message box containing "ABCDEF":
Public Sub TestStdIn()
MsgBox ExecuteAndReturnStdOutput("C:\ConvertStdInToUpper.exe", "abcdef")
End Sub
Based on the various answers mostly the one from Brian Burns, here is a shorten version, tested and functional :
Function F_shellExec(sCmd As String) As String
Dim oShell As New WshShell 'requires ref to Windows Script Host Object Model
F_shellExec = oShell.Exec(sCmd).StdOut.ReadAll
End Function
it works pretty fine and it's quite fast. BUT, if the output is too large (for example scanning the whole C: drive sCmd = "DIR /S C:\"), ReadAll will crash
So I came up with the 2nd solution bellow, which so far works fine, in both cases. Note that the 1st reading is faster, and that if it crash, the reading restart at the beginning, so you don't miss information
Function F_shellExec2(sCmd As String) As String
'Execute Windows Shell Commands
Dim oShell As New WshShell 'requires ref to Windows Script Host Object Model
'Dim oExec As WshExec 'not needed, but in case you need the type
Dim oOutput As TextStream
Dim sReturn As String
Dim iErr As Long
'Set oExec = oShell.Exec(sCmd) 'unused step, for the type
Set oOutput = oShell.Exec(sCmd).StdOut
On Error Resume Next
sReturn = oOutput.ReadAll
iErr = Err.Number
On Error GoTo 0
If iErr <> 0 Then
sReturn = ""
While Not oOutput.AtEndOfStream
sReturn = sReturn & oOutput.ReadLine & Chr(10)
Wend
End If
F_shellExec2 = sReturn
End Function
Regarding the reference to Windows Script Host Object Model:
You will need to add a reference to Windows Script Host Object Model so the types WshShell (and WshExec) are known.
(To do this go to Tools -> References in the VBA IDE's menu bar.)
You could always redirect the shell output to a file, then read the output from the file.
Sub StdOutTest()
Dim objShell As Object
Dim objWshScriptExec As Object
Dim objStdOut As Object
Dim rline As String
Dim strline As String
Set objShell = CreateObject("WScript.Shell")
Set objWshScriptExec = objShell.Exec("c:\temp\batfile.bat")
Set objStdOut = objWshScriptExec.StdOut
While Not objStdOut.AtEndOfStream
rline = objStdOut.ReadLine
If rline <> "" Then strline = strline & vbCrLf & CStr(Now) & ":" & Chr(9) & rline
' you can handle the results as they are written to and subsequently read from the StdOut object
Wend
MsgBox strline
'batfile.bat
'ping 1.1.1.1 -n 1 -w 2000 > nul
'echo 2
'ping 1.1.1.1 -n 1 -w 2000 > nul
'echo 4
'ping 1.1.1.1 -n 1 -w 2000 > nul
'echo 6
'ping 1.1.1.1 -n 1 -w 2000 > nul
'echo 8
End Sub
This function provides a quick way to run a Command Line command, using the clipboard object:
Capture command-line output:
Function getCmdlineOutput(cmd As String)
CreateObject("WScript.Shell").Run "cmd /c """ & cmd & "|clip""", 0, True 'output>clipbrd
With CreateObject("New:{1C3B4210-F441-11CE-B9EA-00AA006B1A69}") 'latebound clipbrd obj
.GetFromClipboard 'get cmdline output from clipboard
getCmdlineOutput = .GetText(1) 'return clipboard contents
End With
End Function
Example usage:
Sub Demo1()
MsgBox getCmdlineOutput("w32tm /tz") 'returns the system Time Zone information
End Sub
It uses the WShell Run command because it optionally allows for asynchronous execution, meaning it will wait for the command to finish running before VBA continues, which is important when involving the clipboard.
It also utilizes a built-in but often-forgotten command line utility called clip.exe, in this case as a destination for the piped cmdline output.
Clipboard manipulation requires a reference to the Microsoft Forms 2.0 library, which in this case I created with a Late-bound reference (which looks different since MS Forms - aka fm20.dll - is a Windows library, not VBA).
Preserving Existing Clipboard Data:
In my case it was an issue that the function above wipes the existing clipboard data, so the function below is modified to retain & replace existing text on the clipboard.
If there is something other than text on the clipboard you'll be warned that it will be lost. Some heavy coding could allow for other/any type of clipboard data to be returned... but advanced clipboard manipulation is far more complex than most users realize, and I frankly don't have the need or desire to get into it. More info here.
Note that this in this method MS Forms is Early-Bound but could be changed if desired. (But remember as a general rule of thumb, late-binding generally doubles processing time.)
Function getCmdlineOutput2(cmd As String)
'requires Reference: C:\Windows\System32\FM20.DLL (MS Forms 2.0) [Early Bound]
Dim objClipboard As DataObject, strOrigClipbrd As Variant
Set objClipboard = New MSForms.DataObject 'create clipboard object
objClipboard.GetFromClipboard 'save existing clipboard text
If Not objClipboard.GetFormat(1) Then
MsgBox "Something other than text is on the clipboard.", 64, "Clipboard to be lost!"
Else
strOrigClipbrd = objClipboard.GetText(1)
End If
'shell to hidden commandline window, pipe output to clipboard, wait for finish
CreateObject("WScript.Shell").Run "cmd /c """ & cmd & "|clip""", 0, True
objClipboard.GetFromClipboard 'get cmdline output from clipboard
getCmdlineOutput2 = objClipboard.GetText(1) 'return clipboard contents
objClipboard.SetText strOrigClipbrd, 1 'Restore original clipboard text
objClipboard.PutInClipboard
End Function
Example Usage:
Sub Demo2()
MsgBox getCmdlineOutput2("dir c:\") 'returns directory listing of C:\
End Sub
For those situations where a small return is expected, this is the shortest command I've ever seen:
MsgBox CreateObject("WScript.Shell").Exec("C:\Windows\SysWOW64\WHOAMI.EXE /USER /FO CSV").StdOut.ReadAll

Dynamically Reference a Module Variable

Simple Version:
Module A has a Public variable X
I want to be able to get the value of X from Module B, without hardcoding the name 'Module A', i.e. (obviously this is not the right code):
MsgBox Modules("Module A").X
More Advanced Version:
I have an Add-In/XLSM (it can toggle itself) called TAAA.xlsm. I use Rob Bovey's error handling system, and want to improve/expand it.
A lot of my modules create new workbooks. If the user receives an error, I want to give them the option of sending me the error to examine myself. I'd like it to prompt the user, and if they say 'yes', the error handler would use Outlook to e-mail me:
Error Log
TAAA.xlsm
Any child workbooks related to the error
My plan was to have a Public Workbook Array for each module where it would store any workbooks created/used by the code that caused the error. That way when the error handler processes, it can access that public array in order to attach the workbooks.
I suppose a "simpler" solution would be to store this data on worksheet in TAAA, though it's not as elegant.
Any thoughts would be much appreciated!
EDIT
I solved my own problem in an Answer below. However, I'm still curious if there is a good answer to my original question or if that's impossible.
So in retrospect, the answer seems pretty obvious to me.
How does the central error handler know which module the error came from? By the private module name string being passed to the central error handler.
Likewise, I can just pass the workbook array as another parameter to the central error handler!
So instead of the central error handler looking like this:
Public Function bCentralErrorHandler( _
ByVal sModule As String, _
ByVal sProc As String, _
Optional ByVal sFile As String, _
Optional ByVal bEntryPoint As Boolean) As Boolean
Static sErrMsg As String
Dim iFile As Integer
Dim lErrNum As Long
Dim sFullSource As String
Dim sPath As String
Dim sLogText As String
' Grab the error info before it's cleared by
' On Error Resume Next below.
lErrNum = Err.Number
' If this is a user cancel, set the silent error flag
' message. This will cause the error to be ignored.
If lErrNum = glUSER_CANCEL Then sErrMsg = msSILENT_ERROR
' If this is the originating error, the static error
' message variable will be empty. In that case, store
' the originating error message in the static variable.
If Len(sErrMsg) = 0 Then sErrMsg = Err.Description
' We cannot allow errors in the central error handler.
On Error Resume Next
' Load the default filename if required.
If Len(sFile) = 0 Then sFile = ThisWorkbook.Name
' Get the application directory.
sPath = ThisWorkbook.Path
If Right$(sPath, 1) <> "\" Then sPath = sPath & "\"
' Construct the fully-qualified error source name.
sFullSource = "[" & sFile & "]" & sModule & "." & sProc
' Create the error text to be logged.
sLogText = " " & Application.UserName & sFullSource & ", Error " & _
CStr(lErrNum) & ": " & sErrMsg
' Open the log file, write out the error information and
' close the log file.
iFile = FreeFile()
Open sPath & msFILE_ERROR_LOG For Append As #iFile
Print #iFile, Format$(Now(), "mm/dd/yy hh:mm:ss"); sLogText
If bEntryPoint Then Print #iFile,
Close #iFile
' Do not display or debug silent errors.
If sErrMsg <> msSILENT_ERROR Then
' Show the error message when we reach the entry point
' procedure or immediately if we are in debug mode.
If bEntryPoint Or gbDEBUG_MODE Then
Application.ScreenUpdating = True
MsgBox sErrMsg, vbCritical, gsAPP_NAME
' Clear the static error message variable once
' we've reached the entry point so that we're ready
' to handle the next error.
sErrMsg = vbNullString
End If
' The return vale is the debug mode status.
bCentralErrorHandler = gbDEBUG_MODE
Else
' If this is a silent error, clear the static error
' message variable when we reach the entry point.
If bEntryPoint Then sErrMsg = vbNullString
bCentralErrorHandler = False
End If
End Function
I would change the definition to:
Public Function bCentralErrorHandler( _
ByVal sModule As String, _
ByVal sProc As String, _
Optional ByVal wbChildWorkbooks() As Workbook, _
Optional ByVal sFile As String, _
Optional ByVal bEntryPoint As Boolean) As Boolean
Fairly obvious in retrospect. Sorry for the wasted question.

Rename and save ActiveDocument with VBA

Is it possible to rename the activedocument (the word document that I'm running the macro from) with VBA?
Right now I'm saving my activedocument under a new name and then attempt to delete the original. The latter part won't go through, so the original never gets deleted.
Anyone know if this is even possible?
I spent a lot of time doing this recently, because I disliked having to delete previous files when I did "Save As" - I wanted a "Save as and delete old file" answer. My answer is copied from here.
I added it to the quicklaunch bar which works wonderfully.
Insert following code into normal.dotm template (found in C:\Documents and Settings\user name\Application Data\Microsoft\Templates for Windows 7 for Word)
Save normal.dotm
Add this to the quicklaunch toolbar in Word.
Optional - remap a keyboard shortcut to this
Optional - digitally sign your template (recommended)
Note this actually moves the old file to the Recycle Bin rather than trashing completely and also sets the new file name in a very convenient fashion.
Option Explicit
'To send a file to the recycle bin, we'll need to use the Win32 API
'We'll be using the SHFileOperation function which uses a 'struct'
'as an argument. That struct is defined here:
Private Type SHFILEOPSTRUCT
hwnd As Long
wFunc As Long
pFrom As String
pTo As String
fFlags As Integer
fAnyOperationsAborted As Long
hNameMappings As Long
lpszProgressTitle As Long
End Type
' function declaration:
Private Declare Function SHFileOperation Lib "shell32.dll" Alias "SHFileOperationA" (lpFileOp As SHFILEOPSTRUCT) As Long
'there are some constants to declare too
Private Const FO_DELETE = &H3
Private Const FOF_ALLOWUNDO = &H40
Private Const FOF_NOCONFIRMATION = &H10
Private Const FOF_SILENT = &H4
Function RecycleFile(FileName As String, Optional UserConfirm As Boolean = True, Optional HideErrors As Boolean = False) As Long
'This function takes one mandatory argument (the file to be recycled) and two
'optional arguments: UserConfirm is used to determine if the "Are you sure..." dialog
'should be displayed before deleting the file and HideErrors is used to determine
'if any errors should be shown to the user
Dim ptFileOp As SHFILEOPSTRUCT
'We have declared FileOp as a SHFILEOPSTRUCT above, now to fill it:
With ptFileOp
.wFunc = FO_DELETE
.pFrom = FileName
.fFlags = FOF_ALLOWUNDO
If Not UserConfirm Then .fFlags = .fFlags + FOF_NOCONFIRMATION
If HideErrors Then .fFlags = .fFlags + FOF_SILENT
End With
'Note that the entire struct wasn't populated, so it would be legitimate to change it's
'declaration above and remove the unused elements. The reason we don't do that is that the
'struct is used in many operations, some of which may utilise those elements
'Now invoke the function and return the long from the call as the result of this function
RecycleFile = SHFileOperation(ptFileOp)
End Function
Sub renameAndDelete()
' Store original name
Dim sOriginalName As String
sOriginalName = ActiveDocument.FullName
' Save As
Dim sFilename As String, fDialog As FileDialog, ret As Long
Set fDialog = Application.FileDialog(msoFileDialogSaveAs)
'set initial name so you don't have to navigate to
fDialog.InitialFileName = sOriginalName
ret = fDialog.Show
If ret <> 0 Then
sFilename = fDialog.SelectedItems(1)
Else
Exit Sub
End If
Set fDialog = Nothing
'only do this if the file names are different...
If (sFilename <> sOriginalName) Then
'I love vba's pretty code
ActiveDocument.SaveAs2 FileName:=sFilename, FileFormat:= _
wdFormatXMLDocument, LockComments:=False, Password:="", AddToRecentFiles _
:=True, WritePassword:="", ReadOnlyRecommended:=False, EmbedTrueTypeFonts _
:=False, SaveNativePictureFormat:=False, SaveFormsData:=False, _
SaveAsAOCELetter:=False, CompatibilityMode:=14
' Delete original (don't care about errors, I guess)
Dim hatersGonnaHate As Integer
hatersGonnaHate = RecycleFile(sOriginalName, False, True)
End If
End Sub

Programmatically set DLL search path in VBA macro

The problem
I have a word template which uses VBA's Declare statement to link to a dll, whose path can be determined within the VBA macro
I want to delploy this to the users %APPDATA%\Microsoft\Word\STARTUP directory
I DON'T want to permanently change the user's PATH environment variable (temporarily would be OK, but this doesn't seem to work as they don't get refreshed until application restart)
Attempted solution
I tried dynamically adding the code with the Declare statements using ThisDocument.VBProject.CodeModule.AddFromString(code) which works when loading the template from a normal directory, but when the template is within Word\STARTUP, it gives the following error:
Run-time error '50289':
Can't perform operation since the
project is protected.
And setting the registry key "HKEY___LOCAL_MACHINE\Software\Microsoft\Office\11.0\Word\Security\AccessVBOM" to 1 doesn't fix this when the template is in Word\STARTUP
I'm really struggling to find a solution. If anyone knows a way to do this, that would be great.
Frankly, I don't know what's the problem with using all those VBA code injection, assembly generation for LoadLibrary() calls, etc techniques that I've seen used for this simple task. In my project I use simple code to load dll from the same location as the workbook, like this:
Declare Function MyFunc Lib "MyDll.dll" (....) As ...
Sub Test()
....
ChDir ActiveWorkbook.Path
... = MyFunc(....)
End Sub
Excel 2003 at least, has no problem loading the dll from the current path, Set ChDir to whatever path your DLL has. You might also need to change your current drive which is separate from current path. You have to do it only once, before the first function call, after it the DLL stays attached no matter where your current path is, so you may do it once in workbook_open and not bother about the path later. I provide an empty dummy function in the DLL just for this pupose. I don't think MS Word is any different on this.
Private Declare Sub Dummy Lib "MyDLL.dll" ()
Private Sub Workbook_Open()
ChDrive Left$(Me.Path, 1)
ChDir Me.Path
Dummy
End Sub
You can use LoadLibrary api.
For example in my projects the code looks like this:
If LibraryLoaded() Then
Call MyFunc ...
End If
Public Function LibraryLoaded() As Boolean
Static IsLoaded As Boolean
Static TriedToLoadAlready As Boolean
If TriedToLoadAlready Then
LibraryLoaded = IsLoaded
Exit Function
End If
Dim path As String
path = VBAProject.ThisWorkbook.path
path = Left(path, InStrRev(path, "\") - 1)
IsLoaded = LoadLibrary(path & "\bin\" & cLibraryName)
TriedToLoadAlready = True
LibraryLoaded = IsLoaded
End Function
There is another really really ugly solution, but this blogger figured it out, and I can't figure out any other way:
http://blogs.msdn.com/pranavwagh/archive/2006/08/30/How-To-Load-Win32-dlls-Dynamically-In-VBA.aspx
Basically, you write a procedure that creates a code module in VBA during runtime. This module must create a reference to the dll and it must create a dummy function (or procedure) as part of this module that calls the dll. Then, from your code, you use Application.Run(dummyfunction(), arg1, arg2...). This is necessary because otherwise, the project will not compile because dummyfunction isn't yet a function.
You'll notice in his code, he uses InputBox() to get the location of the .dll but obviously you could get the location from a range in the spreadsheet. The following code snippet may be useful.
Dim cm As CodeModule
Dim vbc As VBComponent
Set cm = Application.VBE.ActiveVBProject.VBComponents.Add(vbext_ct_StdModule).CodeModule
cm.AddFromString (decString & funcString)
cm.Name = "MyNewModule"
Set vbc = cm.Parent
Application.VBE.ActiveVBProject.VBComponents.Remove vbc
'decString' and 'funcString' were just strings I constructed like his 'ss'. The snippet shows how you can rename the code module so that you could delete it later if needed. Obviously, this just deletes it right after it is created, and you probably wouldn't want to do that, but at least it shows you how it would be done.
Having said all that, we mostly just write .exe's now and shell out. If you need VBA to wait on the shell to finish, there are solutions for that issue as well.
Here's what I ended up doing, using Pranav Wagh's methodology linked above and code from C Pearson's site (http://www.cpearson.com/excel/vbe.aspx). This code prompts the user to select the path to the dll using an Open File window, builds a new module with a Declare Function with the inputted path and a function to execute a handshake with the dll. The purpose-built function in the dll returns a 1 if successful:
Public rtn As Integer
Sub LinkToDll()
Dim path As String, default As String
MsgBox "Select Geo_DLL.dll file from next window"
With Application.FileDialog(msoFileDialogOpen)
.AllowMultiSelect = False
.Title = "Select Geo_DLL.dll file"
If .Show = True Then
path = .SelectedItems(1)
End If
End With
'Add a module
Dim VBProj As VBIDE.VBProject
Dim VBComp As VBIDE.VBComponent
Set VBProj = ActiveWorkbook.VBProject
Set VBComp = VBProj.VBComponents.Add(vbext_ct_StdModule)
VBComp.Name = "LinkModule"
'Add procedure to module
Dim CodeMod As VBIDE.CodeModule
Dim LineNum As Long
Set VBComp = VBProj.VBComponents("LinkModule")
Set CodeMod = VBComp.CodeModule
With CodeMod
LineNum = .CountOfLines + 1
.InsertLines LineNum, "Declare Function RegDll Lib " & Chr(34) & path & Chr(34) & " (ByRef rtn As Integer)"
LineNum = LineNum + 1
.InsertLines LineNum, "Sub runthisfunc(rtn)"
LineNum = LineNum + 1
.InsertLines LineNum, "On Error Resume Next"
LineNum = LineNum + 1
.InsertLines LineNum, "rtn = 0"
LineNum = LineNum + 1
.InsertLines LineNum, "RegDll rtn"
LineNum = LineNum + 1
.InsertLines LineNum, "If rtn = 1 Then MsgBox (" & Chr(34) & "DLL linked" & Chr(34) & ")"
LineNum = LineNum + 1
.InsertLines LineNum, "If rtn = 0 Then MsgBox (" & Chr(34) & "DLL not found" & Chr(34) & ")"
LineNum = LineNum + 1
.InsertLines LineNum, "End Sub"
End With
'This is what CodeMod.InsertLines is writing:
'--------------------------------------------
'Declare Function RegDll Lib "C:\path\Geo_DLL.dll" (ByRef rtn As Integer)
'Sub runthisfunc(rtn)
'On Error Resume Next
'rtn = 0
'RegDll rtn
'If rtn = 1 Then MsgBox ("DLL Linked")
'If rtn = 0 Then MsgBox (DLL not found")
'End Sub
Application.Run "runthisfunc", rtn
'Delete Module
VBProj.VBComponents.Remove VBComp
End Sub
However, once I turned the workbook (xlsm) into an addin (xlam) I found that Excel wouldn't let the macro create new modules so my LinkToDll wouldn't work. The fix was to put the Declare Function back into LinkToDll with just the dll file name ("Geo_DLL.dll") as the Lib along with the runthisfunc sub. I found having the user simply select the dll file via the Open File window was enough to point Excel to the dll even with only the file name in the Lib portion of the Declare Function statement.
Chris
In my case code below worked. I added "ChDir (ThisWorkbook.Path)"
after function. But I tested it only on my laptop. I don't know if it works on network.
Option Explicit
' Declare the function that is in the DLL
Private Declare PtrSafe Function suntransitForEXL Lib _
"sampadll.dll" (ByRef lat As Double, ByRef lon As Double, ByRef dy As Integer, ByRef mnt As Integer, ByRef yr As Integer, ByRef tmz As Double) As Double
' use function on worksheet
Function noon(latitude As Double, longtitude As Double, day As Integer, month As Integer, year As Integer, timezone As Double) As Double
ChDir (ThisWorkbook.Path) ' Set working directory to current.
Dim decimaltime As Double
Dim hour As Integer
Dim minute As Integer
Dim second As Integer
decimaltime = suntransitForEXL(latitude, longtitude, day, month, year, timezone)
hour = Fix(decimaltime)
minute = Fix((decimaltime - hour) * 60)
second = Fix(((decimaltime - hour) * 60 - minute) * 60)
noon = TimeSerial(hour, minute, second)
End Function