Tech Blog‎ > ‎

VB.NET - The right way to read command line arguments

posted Feb 8, 2013, 9:05 AM by Victor Zakharov   [ updated Feb 8, 2013, 9:11 AM ]
While it may appear to be a simple question, there is really more to it.
Please read this article carefully, even if you believe you know how to handle this situation.
And especially if you have been writing command line utilities for 10+ years and never encountered any issues using standard .NET functionality.

Approaches you may be thinking about at the moment, which are often suggested at forums, unfortunately, do not work:
  1. Declare args() As String in Sub Main (when using a module) or Shared Sub Main (when using a class)
  2. Retrieve arguments using Environment.GetCommandLineArgs.
Problem is that a WinAPI function, which both of these are based on, is seriously flawed. Question on Stackoverflow:
Bottom line is that if you use them, you will have trouble handling double quotes and backslash character inside parameters.

Here is a Comment by Jon Galloway regarding this weird behavior, suggesting a workaround to argument passing.
But it implies that you have to know about this deficiency, and how to handle it, when working with Windows command line.

Jonathan Levison proposed a solution in C#, to be used in conjunction with Environment.CommandLine (which returns a single/unparsed string).
It is basically a proper implementation of Environment.GetCommandLineArgs - one that actually works as expected.

And here my converted version for VB.NET (see below) - preliminary testing shows that it's working.

If I later encounter any issues with it, I will update this post.
If you encounter any issues with it, you can let me know at neolisk@gmail.com.
No guarantees, but I will try to reply and/or fix the issue ASAP.

Imports System.Text

'original version by Jonathan Levison (C#)'
'http://sleepingbits.com/2010/01/command-line-arguments-with-double-quotes-in-net/
'converted using http://www.developerfusion.com/tools/convert/csharp-to-vb/
'and then some manual effort to fix language discrepancies
Friend Class CommandLineHelper
  ''' <summary>
  ''' C-like argument parser
  ''' </summary>
  ''' <param name="commandLine">Command line string with arguments. Use Environment.CommandLine</param>
  ''' <returns>The args[] array (argv)</returns>
  Public Shared Function CreateArgs(commandLine As String) As String()
    Dim argsBuilder As New StringBuilder(commandLine)
    Dim inQuote As Boolean = False

    ' Convert the spaces to a newline sign so we can split at newline later on
    ' Only convert spaces which are outside the boundries of quoted text
    For i As Integer = 0 To argsBuilder.Length - 1
      If argsBuilder(i).Equals(""""c) Then
        inQuote = Not inQuote
      End If

      If argsBuilder(i).Equals(" "c) AndAlso Not inQuote Then
        argsBuilder(i) = ControlChars.Lf
      End If
    Next

    ' Split to args array
    Dim args As String() = argsBuilder.ToString().Split(New Char() {ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries)

    ' Clean the '"' signs from the args as needed.
    For i As Integer = 0 To args.Length - 1
      args(i) = ClearQuotes(args(i))
    Next

    Return args
  End Function

  ''' <summary>
  ''' Cleans quotes from the arguments.<br/>
  ''' All signle quotes (") will be removed.<br/>
  ''' Every pair of quotes ("") will transform to a single quote.<br/>
  ''' </summary>
  ''' <param name="stringWithQuotes">A string with quotes.</param>
  ''' <returns>The same string if its without quotes, or a clean string if its with quotes.</returns>
  Private Shared Function ClearQuotes(stringWithQuotes As String) As String
    Dim quoteIndex As Integer = stringWithQuotes.IndexOf(""""c)
    If quoteIndex = -1 Then Return stringWithQuotes

    ' Linear sb scan is faster than string assignemnt if quote count is 2 or more (=always)
    Dim sb As New StringBuilder(stringWithQuotes)
    Dim i As Integer = quoteIndex
    Do While i < sb.Length
      If sb(i).Equals(""""c) Then
        ' If we are not at the last index and the next one is '"', we need to jump one to preserve one
        If i <> sb.Length - 1 AndAlso sb(i + 1).Equals(""""c) Then
          i += 1
        End If

        ' We remove and then set index one backwards.
        ' This is because the remove itself is going to shift everything left by 1.
        sb.Remove(System.Math.Max(System.Threading.Interlocked.Decrement(i), i + 1), 1)
      End If
      i += 1
    Loop

    Return sb.ToString()
  End Function
End Class