Most people believe escaping arguments to a process is easy. Surround it with quotes, right? Nope. Escape the quotes and surround it with quotes? Nope. Escape the quotes and backslashes and then surround it with quotes? Nope. It’s actually a lot more strange than that.

When do you need to escape and quote an argument? You need to escape and quote any string that either contains whitespace, or a quote. So why would you escape a single quote (A”B)?Actually this would be fine, it’s really two consecutive quotes that are problem (A”"B). These are interpreted as a single quote character unless quoted and escaped. Of course if it begins with a quote then you’d also need to escape it. So to make life easier, just escape anything with whitespace or a quote.

How do you escape a command line argument? Once you’ve determined that your going to escape and quote the program argument it get’s more interesting. You will need to find and escape all consecutive backslash ‘\’ characters that are immediately followed by either a quote, or appear at the end of the argument. You will also need to escape all the quote characters, again by prefixing a new backslash. So (\asdf\) becomes (“\asdf\\”) and (as\”df) becomes (“as\\\”df”). Then there are a few characters you cannot represent, null ‘\0′, newline ‘\n’, and linefeed ‘\r’.

So here it is all together, tested, and ready to go:

    /// <summary>
    /// Quotes all arguments that contain whitespace, or begin with a quote and returns a single
    /// argument string for use with Process.Start().
    /// </summary>
    /// <param name="args">A list of strings for arguments, may not contain null, '\0', '\r', or '\n'</param>
    /// <returns>The combined list of escaped/quoted strings</returns>
    /// <exception cref="System.ArgumentNullException">Raised when one of the arguments is null</exception>
    /// Raised if an argument contains '\0', '\r', or '\n'
    public static string EscapeArguments(params string[] args)
    {
        StringBuilder arguments = new StringBuilder();
        Regex invalidChar = new Regex("[\x00\x0a\x0d]");//  these can not be escaped
        Regex needsQuotes = new Regex(@"\s|""");//          contains whitespace or two quote characters
        Regex escapeQuote = new Regex(@"(\\*)(""|$)");//    one or more '\' followed with a quote or end of string
        for (int carg = 0; args != null && carg < args.Length; carg++)
        {
            if (args[carg] == null) { throw new ArgumentNullException("args[" + carg + "]"); }
            if (invalidChar.IsMatch(args[carg])) { throw new ArgumentOutOfRangeException("args[" + carg + "]"); }
            if (args[carg] == String.Empty) { arguments.Append("\"\""); }
            else if (!needsQuotes.IsMatch(args[carg])) { arguments.Append(args[carg]); }
            else
            {
                arguments.Append('"');
                arguments.Append(escapeQuote.Replace(args[carg], m =>
                                 m.Groups[1].Value + m.Groups[1].Value +
                                 (m.Groups[2].Value == "\"" ? "\\\"" : "")
                    ));
                arguments.Append('"');
            }
            if (carg + 1 < args.Length)
                arguments.Append(' ');
        }
        return arguments.ToString();
    }

Of course you could improve on the performance here by moving the Regex instances to statics and/or compiling or even replacing them. Performance wasn't exactly a major consideration here since spawning the process will be several orders of magnitude more time consuming.

If your interested, the following are some of the test patterns used. The above method was also tested with random arguments used to spawn a process that echoed each argument back for verification.

    Assert.AreEqual("\"\"", EscapeArguments(""));
    Assert.AreEqual("\"\\\"\"", EscapeArguments("\""));
    Assert.AreEqual("\"\\\"\\\"\"", EscapeArguments("\"\""));
    Assert.AreEqual("\"\\\"a\\\"\"", EscapeArguments("\"a\""));
    Assert.AreEqual("\\", EscapeArguments("\\"));
    Assert.AreEqual("\"a b\"", EscapeArguments("a b"));
    Assert.AreEqual("a \" b\"", EscapeArguments("a", " b"));
    Assert.AreEqual("a\\\\b", EscapeArguments("a\\\\b"));
    Assert.AreEqual("\"a\\\\b c\"", EscapeArguments("a\\\\b c"));
    Assert.AreEqual("\" \\\\\"", EscapeArguments(" \\"));
    Assert.AreEqual("\" \\\\\\\"\"", EscapeArguments(" \\\""));
    Assert.AreEqual("\" \\\\\\\\\"", EscapeArguments(" \\\\"));
    Assert.AreEqual("\"C:\\Program Files\\\\\"", EscapeArguments("C:\\Program Files\\"));
    Assert.AreEqual("\"dafc\\\"\\\"\\\"a\"", EscapeArguments("dafc\"\"\"a"));

			
Comments