0

I realize this question has been asked many times here. I have looked at and tried many of the answers but none of them work for me.

I am creating an application using C# that can accept command line arguments. e.g.

  1. Start -p:SomeNameValue -h
  2. DisplayMessage -m:Hello
  3. DisplayMessage -m:'Hello World'
  4. DisplayMessage -m:"Hello World"

My args come in as a single string. I need to split by spaces except where there are single or double quotes. So the above would end up as

  1. Start -p:SomeNameValue -h
  2. DisplayMessage -m:Hello
  3. DisplayMessage -m:'Hello World'
  4. DisplayMessage -m:"Hello World"

The answers I have found on here seem to break. e.g. They remove the : character or just don't work at all. Some of the code I've tried as follows:

var res1 = Regex.Matches(payload, @"[\""].+?[\""]|[^ ]+")
    .Cast<Match>()
    .Select(m => m.Value)
    .ToList();
var res2 = payload.Split('"')
    .Select((element, index) => index % 2 == 0  
        ? element.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
        : new string[] { element })  // Keep the entire item
    .SelectMany(element => element).ToList();
var res3 = Regex
    .Matches(payload, @"\w+|""[\w\s]*""")
    .Cast<Match>()
    .Select(m => m.Groups["match"].Value)
    .ToList();
string[] res4 = Regex.Split(payload, ",(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))");
Regex regex = new Regex(@"\w+|""[\w\s]*""");
var res5 = regex.Matches(payload).Cast<Match>().ToList();

I simply want to split the arg into blocks as per above.

Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
Kieran
  • 186
  • 2
  • 13
  • 2
    What have you tried? Seems like you could easily do this in a loop where you keep track of opening and closing quotes. – Rufus L Mar 03 '21 at 01:08
  • 1
    Please show us what you've tried that hasn't worked for you. Folks on Stack Overflow are happy to help you debug your code, but not as happy to write it for you. – STLDev Mar 03 '21 at 01:09
  • 1
    You need decide if you want to consider unmatched quotes, by the way. Parsing this isn't a trivial exercise, but @RufusL has given you a good hint: process each character and build a little state machine with a flag or two to see if you are "inside something quoted" or in normal mode. You are probably better off doing it that way than with a Regex – Flydog57 Mar 03 '21 at 01:43
  • 1
    Have you checked to see if the C# infrastructure does this for free? I think if you start an app like `app.exe abc "xyz 123"`, your Main function will see two command line args, one `"abc"` and the other `"xyz 123"` – Flydog57 Mar 03 '21 at 01:45
  • Are you sure you want to allow single quotes to wrap an argument when they're also allowed to be inside a quoted string? That flexibility doesn't add much value to the end user, but makes your parsing quite a bit more difficult. – Rufus L Mar 03 '21 at 01:52
  • 1
    Also see the answers to [this question](https://stackoverflow.com/questions/491595/best-way-to-parse-command-line-arguments-in-c) for a bunch of pre-built command line parsers. – Rufus L Mar 03 '21 at 01:55
  • @Rufus L That's a very good point. I have edited the question to remove that requirement. – Kieran Mar 03 '21 at 01:56

2 Answers2

1

Here is a simple demo program that I think does exactly what you want by parsing the string.

using System;
using System.Collections.Generic;

public class Program
{
    public static void Main()
    {       
        string[] testStrings = new string[] {
            "Start -p:SomeNameValue -h",
            "DisplayMessage -m:Hello",
            "DisplayMessage -m:'Hello World'",
            "DisplayMessage -m:\"Hello World\"",
            "DisplayMessage -m:\"'Inside double quotes'\"",
            "DisplayMessage -m:'\"Inside single quotes\"'"              
        };

        foreach (string str in testStrings)
        {
            Console.WriteLine(str);
            string[] parsedStrings = ParseString(str);

            for (int i = 0; i < parsedStrings.Length; i++)
            {
                Console.WriteLine("    " + (i + 1) + ". " + parsedStrings[i]);              
            }
            Console.WriteLine();
        }
    }

    private static string[] ParseString(string str)
    {
        var retval = new List<string>();
        if (String.IsNullOrWhiteSpace(str)) return retval.ToArray();
        int ndx = 0;
        string s = "";
        bool insideDoubleQuote = false;
        bool insideSingleQuote = false;

        while (ndx < str.Length)
        {
            if (str[ndx] == ' ' && !insideDoubleQuote && !insideSingleQuote)
            {
                if (!String.IsNullOrWhiteSpace(s.Trim())) retval.Add(s.Trim());
                s = "";
            }
            if (str[ndx] == '"') insideDoubleQuote = !insideDoubleQuote;
            if (str[ndx] == '\'') insideSingleQuote = !insideSingleQuote;
            s += str[ndx];
            ndx++;
        }
        if (!String.IsNullOrWhiteSpace(s.Trim())) retval.Add(s.Trim());
        return retval.ToArray();
    }
}

This program will produce the following output:

Start -p:SomeNameValue -h

1. Start

2. -p:SomeNameValue

3. -h

DisplayMessage -m:Hello

1. DisplayMessage

2. -m:Hello

DisplayMessage -m:'Hello World'

1. DisplayMessage

2. -m:'Hello World'

DisplayMessage -m:"Hello World"

1. DisplayMessage

2. -m:"Hello World"

DisplayMessage -m:"'Inside double quotes'"

1. DisplayMessage

2. -m:"'Inside double quotes'"

DisplayMessage -m:'"Inside single quotes"'

1. DisplayMessage

2. -m:'"Inside single quotes"'

Icemanind
  • 47,519
  • 50
  • 171
  • 296
0

One way to do this would be to use a loop to check for items that contain the double-quote delimiter and use a flag variable to determine if we're inside or outside a quoted string. If we're inside the quoted string, add the current part to a temporary variable. When we exit the quoted string, add the temporary variable to our arguments collection.

I think the code is more self-explanatory. Note that I'm only allowing double quotes to delimit an argument with spaces. If you want to allow single quotes as well, it's something you can add to the code:

public static List<string> GetArgs(string cmdLine)
{
    var args = new List<string>();
    if (string.IsNullOrWhiteSpace(cmdLine)) return args;

    var parts = cmdLine.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
    var openQuote = false;
    var currentPart = new StringBuilder();

    foreach (var part in parts)
    {
        if (part.Count(c => c == '"') % 2 == 1)
        {
            if (currentPart.Length > 0) currentPart.Append(" ");
            currentPart.Append(part);

            if (openQuote)
            {
                args.Add(currentPart.ToString());
                currentPart.Clear();
            }

            openQuote = !openQuote;
        }
        else if (openQuote)
        {
            if (currentPart.Length > 0) currentPart.Append(" ");
            currentPart.Append(part);
        }
        else
        {
            args.Add(part);
        }
    }

    if (currentPart.Length > 0) args.Add(currentPart.ToString());

    return args;
}
Rufus L
  • 36,127
  • 5
  • 30
  • 43