2012-03-19

Read Command Line Parameters from VBA

Recently, I needed to read a command line parameter within a VBA code (it's for an Iconics SCADA application, but I developed it with Excel VBA which is essentially the same). It doesn't sound like a big deal but there were a few not-so-obvious tricks that I thought were worth sharing.
I started with the simplest solution using a "GetCommandLineA" Windows API function from kernel32.dll. It was supposed to return a pointer to the command line string, so I assumed that its return type would be string (sounds logical to me).
' The return type of the GetCommandLineA is long
' But it is supposed to be a pointer to the string. 
' Assumming that all VBA strings are passed by pointers
' Just declare it's return type to be a string
Declare Function GetCommandLineA Lib "Kernel32" () As String
 
Sub ReadCmdLine()

   Dim strCmdLine As String ' Command line string   
   ' Read command line parameters into the string
   strCmdLine = GetCommandLineA

End Sub
Surprise - it caused exception and crashed the application. Hm, apparently VBA handles a pointer to the string differently than GetCommandLineA. Not wanting to spend time figuring out why, I decided to copy the string to VBA string using another kernel32.dll function "lstrcpynA".
' Declare the return type to be a pointer (long)
Declare Function GetCommandLineA Lib "Kernel32" () As Long
Declare Function lstrcpynA Lib "kernel32" ( _ 
   ByVal pDestination As String, ByVal pSource As Long, _
   ByVal iMaxLength As Integer) As Long
 
Sub ReadCmdLine()

   Dim pCmdLine as long     ' Pointer to the string
   Dim strCmdLine As String ' Command line string     

   pCmdLine = GetCommandLineA
   ' Copy from the pointer to VBA-Style string 
   ' 300 characters for command line seems to be enough
   lstrcpynA strCmdLine , pCmdLine, 300    

End Sub

This worked better by no longer crashing the application, but the strCmdLine was always empty. After short Google search I found this Microsoft article. Apparently lstrcpynA function (as other DLL functions returning strings) can't change the size of the VBA-style string. In order to reserve the space for that return data, we need to fill the string with a bunch of zeros (vbNullChar).
Declare Function GetCommandLineA Lib "Kernel32" () As Long
Declare Function lstrcpynA Lib "kernel32" ( _
   ByVal pDestination As String, ByVal pSource As Long, _
   ByVal iMaxLength As Integer) As Long
 
Sub ReadCmdLine()

   Dim pCmdLine as long     ' Pointer to the string
   Dim strCmdLine As String ' Command line string   

   ' Get the pointer to the command line string
   pCmdLine = GetCommandLineA

   ' Fill the string with zeros
   ' 300 characters for command line seems to be enough
   strCmdLine = String$(300, vbNullChar)

   ' Copy from the pointer to VBA-style string
   lstrcpynA strCmdLine , pCmdLine, Len(strCmdLine )

   ' At this point we got the string
   ' But rest of it filled with 0 characters.
   strCmdLine = Left(strCmdLine , InStr(1, strCmdLine , _
      vbNullChar) - 1)
       
End Sub

This code finally worked as expected, returning command line arguments.
Hopefully, this article is useful in saving someone 15 minutes of frustration.

2 comments:

  1. For this to work on Server 2012 running office 2013, I had to change long to longPtr:

    #If VBA7 Then
    Declare PtrSafe Function GetCommandLineA Lib "Kernel32" () As LongPtr
    Declare PtrSafe Function lstrcpynA Lib "Kernel32" (ByVal pDestination As String, ByVal pSource As LongPtr, ByVal iMaxLength As Integer) As Long
    #Else
    Declare Function GetCommandLineA Lib "Kernel32" () As Long
    Declare Function lstrcpynA Lib "Kernel32" (ByVal pDestination As String, ByVal pSource As Long, ByVal iMaxLength As Integer) As Long
    #End If

    Function ReadCmdLine() As String
    #If VBA7 Then
    Dim pCmdLine As LongPtr ' Pointer to the string
    #Else
    Dim pCmdLine As Long ' Pointer to the string
    #End If
    Dim strCmdLine As String ' Command line string

    ' Get the pointer to the command line string
    pCmdLine = GetCommandLineA
    ' Fill the string with zeros
    ' 300 characters for command line seems to be enough
    strCmdLine = String$(300, vbNullChar)

    ' Copy from the pointer to VBA-style string
    lstrcpynA strCmdLine, pCmdLine, Len(strCmdLine)
    ' At this point we got the string
    ' But rest of it filled with 0 characters.
    ReadCmdLine = Left(strCmdLine, InStr(1, strCmdLine, vbNullChar) - 1)
    End Function

    ReplyDelete
    Replies
    1. Thank you for the comment.
      I haven't tried it with any modern applications, so good to know that the script has to be updated.

      Delete