diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj
index c2605663..ff0d2f1c 100644
--- a/src/CommandLine/CommandLine.csproj
+++ b/src/CommandLine/CommandLine.csproj
@@ -119,6 +119,9 @@
+
+
+
Code
@@ -167,7 +170,16 @@
-
+
+
+
+ ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll
+ True
+ True
+
+
+
+
..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll
@@ -176,10 +188,10 @@
-
+
- ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll
+ ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll
True
True
diff --git a/src/CommandLine/StringToCommandLine/CSharpStyleCommandLineParser.cs b/src/CommandLine/StringToCommandLine/CSharpStyleCommandLineParser.cs
new file mode 100644
index 00000000..2eb5e7b1
--- /dev/null
+++ b/src/CommandLine/StringToCommandLine/CSharpStyleCommandLineParser.cs
@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace CommandLine.StringToCommandLine
+{
+ ///
+ /// Parse commandlines like C# would parse a string, splitting at each unquoted space:
+ /// * "" ->
+ /// * "abc" -> abc
+ /// * abc abc -> abc, abc
+ /// * "\"" -> "
+ /// * asd"asd -> error
+ /// * "asd -> error unterminated string
+ /// * \ -> error unterminated escape
+ /// * \['"\0abfnrtUuvx] -> https://msdn.microsoft.com/en-us/library/ms228362.aspx?f=255&MSPPError=-2147217396
+ /// * \other -> error
+ ///
+ public class CSharpStyleCommandLineParser : StringToCommandLineParserBase
+ {
+ public override IEnumerable Parse(string commandLine)
+ {
+ if (string.IsNullOrWhiteSpace(commandLine))
+ yield break;
+ var currentArg = new StringBuilder();
+ var quoting = false;
+
+ var pos = 0;
+ while (pos < commandLine.Length)
+ {
+ var c = commandLine[pos];
+ if (c == '\\')
+ {
+ // --- Handle escape sequences
+ pos++;
+ if (pos >= commandLine.Length) throw new UnterminatedEscapeException();
+ switch (commandLine[pos])
+ {
+ case '\'':
+ c = '\'';
+ break;
+ case '\"':
+ c = '\"';
+ break;
+ case '\\':
+ c = '\\';
+ break;
+ case '0':
+ c = '\0';
+ break;
+ case 'a':
+ c = '\a';
+ break;
+ case 'b':
+ c = '\b';
+ break;
+ case 'f':
+ c = '\f';
+ break;
+ case 'n':
+ c = ' ';
+ break;
+ case 'r':
+ c = ' ';
+ break;
+ case 't':
+ c = '\t';
+ break;
+ case 'v':
+ c = '\v';
+ break;
+ case 'x':
+ // --- Hexa escape (1-4 digits)
+ var hexa = new StringBuilder(10);
+ pos++;
+ if (pos >= commandLine.Length)
+ throw new UnterminatedEscapeException();
+ c = commandLine[pos];
+ if (char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
+ {
+ hexa.Append(c);
+ pos++;
+ if (pos < commandLine.Length)
+ {
+ c = commandLine[pos];
+ if (char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
+ {
+ hexa.Append(c);
+ pos++;
+ if (pos < commandLine.Length)
+ {
+ c = commandLine[pos];
+ if (char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
+ {
+ hexa.Append(c);
+ pos++;
+ if (pos < commandLine.Length)
+ {
+ c = commandLine[pos];
+ if (char.IsDigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))
+ {
+ hexa.Append(c);
+ pos++;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ c = (char) int.Parse(hexa.ToString(), NumberStyles.HexNumber);
+ pos--;
+ break;
+ case 'u':
+ // Unicode hexa escape (exactly 4 digits)
+ pos++;
+ if (pos + 3 >= commandLine.Length)
+ throw new UnterminatedEscapeException();
+ try
+ {
+ var charValue = uint.Parse(commandLine.Substring(pos, 4), NumberStyles.HexNumber);
+ c = (char) charValue;
+ pos += 3;
+ }
+ catch (SystemException)
+ {
+ throw new UnrecognizedEscapeSequenceException();
+ }
+ break;
+ case 'U':
+ // Unicode hexa escape (exactly 8 digits, first four must be 0000)
+ pos++;
+ if (pos + 7 >= commandLine.Length)
+ throw new UnterminatedEscapeException();
+ try
+ {
+ var charValue = uint.Parse(commandLine.Substring(pos, 8), NumberStyles.HexNumber);
+ if (charValue > 0xffff)
+ throw new UnrecognizedEscapeSequenceException();
+ c = (char) charValue;
+ pos += 7;
+ }
+ catch (SystemException)
+ {
+ throw new UnrecognizedEscapeSequenceException();
+ }
+ break;
+ default:
+ throw new UnrecognizedEscapeSequenceException();
+ }
+ pos++;
+ currentArg.Append(c);
+ continue;
+ }
+ if (c == '"')
+ {
+ if (quoting)
+ {
+ pos++; //skip space
+ //check that it actually IS a space or EOF
+ if (pos < commandLine.Length && !char.IsWhiteSpace(commandLine[pos]))
+ throw new UnquotedQuoteException();
+ yield return currentArg.ToString();
+ currentArg.Clear();
+ quoting = false;
+ }
+ else
+ {
+ if (currentArg.Length > 0)
+ throw new UnquotedQuoteException();
+ quoting = true;
+ }
+ pos++;
+ continue;
+ }
+ if (char.IsWhiteSpace(c) && !quoting)
+ {
+ if (currentArg.Length > 0)
+ yield return currentArg.ToString();
+ currentArg.Clear();
+ pos++;
+ continue;
+ }
+ pos++;
+ currentArg.Append(c);
+ }
+ if (quoting && currentArg.Length > 0)
+ throw new UnterminatedStringException();
+ if (currentArg.Length > 0)
+ yield return currentArg.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommandLine/StringToCommandLine/DefaultWindowsCommandLineParser.cs b/src/CommandLine/StringToCommandLine/DefaultWindowsCommandLineParser.cs
new file mode 100644
index 00000000..3c66548a
--- /dev/null
+++ b/src/CommandLine/StringToCommandLine/DefaultWindowsCommandLineParser.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Text;
+
+namespace CommandLine.StringToCommandLine
+{
+ ///
+ /// Parse commandlines like CommandLineToArgvW:
+ /// * 2n backslashes followed by a quotation mark produce n backslashes followed by a quotation mark.
+ /// * (2n) + 1 backslashes followed by a quotation mark again produce n backslashes followed by a quotation mark.
+ /// * n backslashes not followed by a quotation mark simply produce n backslashes.
+ /// * Unterminated quoted strings at the end of the line ignores the missing quote.
+ ///
+ public class DefaultWindowsCommandLineParser : StringToCommandLineParserBase
+ {
+ public override IEnumerable Parse(string commandLine)
+ {
+ if (string.IsNullOrWhiteSpace(commandLine))
+ yield break;
+ var currentArg = new StringBuilder();
+ var quoting = false;
+ var emptyIsAnArgument = false;
+ var lastC = '\0';
+ // Iterate all characters from the input string
+ foreach (var c in commandLine)
+ {
+ if (c == '"')
+ {
+ var nrbackslash = 0;
+ for (var i = currentArg.Length - 1; i >= 0; i--)
+ {
+ if (currentArg[i] != '\\') break;
+ nrbackslash++;
+ }
+ //* 2n backslashes followed by a quotation mark produce n backslashes followed by a quotation mark.
+ //also cover nrbackslack == 0
+ if (nrbackslash%2 == 0)
+ {
+ if (nrbackslash > 0)
+ currentArg.Length = currentArg.Length - nrbackslash/2;
+ // Toggle quoted range
+ quoting = !quoting;
+ emptyIsAnArgument = true;
+ if (quoting && lastC == '"')
+ {
+ // Doubled quote within a quoted range is like escaping
+ currentArg.Append(c);
+ lastC = '\0'; //prevent double quoting
+ continue;
+ }
+ }
+ else
+ {
+ // * (2n) + 1 backslashes followed by a quotation mark again produce n backslashes followed by a quotation mark.
+ currentArg.Length = currentArg.Length - nrbackslash/2 - 1;
+ currentArg.Append(c);
+ }
+ }
+ else if (!quoting && char.IsWhiteSpace(c))
+ {
+ // Accept empty arguments only if they are quoted
+ if (currentArg.Length > 0 || emptyIsAnArgument)
+ yield return currentArg.ToString();
+ // Reset for next argument
+ currentArg.Clear();
+ emptyIsAnArgument = false;
+ }
+ else
+ {
+ // Copy character from input, no special meaning
+ currentArg.Append(c);
+ }
+ lastC = c;
+ }
+ // Save last argument
+ if (currentArg.Length > 0 || emptyIsAnArgument)
+ yield return currentArg.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CommandLine/StringToCommandLine/StringToCommandLineParserBase.cs b/src/CommandLine/StringToCommandLine/StringToCommandLineParserBase.cs
new file mode 100644
index 00000000..138c57f0
--- /dev/null
+++ b/src/CommandLine/StringToCommandLine/StringToCommandLineParserBase.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+
+namespace CommandLine.StringToCommandLine
+{
+ public abstract class StringToCommandLineParserBase
+ {
+ public abstract IEnumerable Parse(string commandLine);
+
+ public class UnterminatedStringException : ArgumentException {}
+
+ public class UnrecognizedEscapeSequenceException : ArgumentException {}
+
+ public class UnquotedQuoteException : ArgumentException {}
+
+ public class UnterminatedEscapeException : ArgumentException {}
+ }
+}
\ No newline at end of file
diff --git a/tests/CommandLine.Tests/CommandLine.Tests.csproj b/tests/CommandLine.Tests/CommandLine.Tests.csproj
index d9a9d62b..da3a53f3 100644
--- a/tests/CommandLine.Tests/CommandLine.Tests.csproj
+++ b/tests/CommandLine.Tests/CommandLine.Tests.csproj
@@ -17,7 +17,7 @@
..\..\
true
- f62f3018
+ 3dcbc90d
true
@@ -45,6 +45,12 @@
..\..\CommandLine.snk
+
+ ..\..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.dll
+
+
+ ..\..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.Core.dll
+
@@ -52,6 +58,15 @@
+
+ ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
+
+
+ ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll
+
+
+ ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll
+
@@ -114,6 +129,8 @@
+
+
@@ -138,64 +155,8 @@
-->
-
-
-
- ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\net40\FluentAssertions.dll
- True
- True
-
-
-
-
-
-
- ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\net45\FluentAssertions.dll
- True
- True
-
-
-
-
-
-
- ..\..\packages\FluentAssertions\lib\monoandroid\FluentAssertions.Core.dll
- True
- True
-
-
-
-
-
-
- ..\..\packages\FluentAssertions\lib\monotouch\FluentAssertions.Core.dll
- True
- True
-
-
-
-
- ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\sl5\FluentAssertions.dll
- True
- True
-
..\..\packages\FluentAssertions\lib\sl5\Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll
True
@@ -203,60 +164,27 @@
-
-
-
- ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\wp8\FluentAssertions.dll
- True
- True
-
-
-
-
+
+
+
-
- ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\portable-net40+sl5+win8+wp8+wpa81\FluentAssertions.dll
+
+ ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll
True
True
-
-
-
- ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.Core.dll
- True
- True
-
-
- ..\..\packages\FluentAssertions\lib\portable-win81+wpa81\FluentAssertions.dll
- True
- True
-
-
-
-
-
-
+
- ..\..\packages\FSharp.Core\lib\net20\FSharp.Core.dll
+ ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll
True
True
-
+
..\..\packages\FSharp.Core\lib\portable-net45+netcore45\FSharp.Core.dll
@@ -265,10 +193,10 @@
-
+
- ..\..\packages\FSharp.Core\lib\net40\FSharp.Core.dll
+ ..\..\packages\FSharp.Core\lib\portable-net45+monoandroid10+monotouch10+xamarinios10\FSharp.Core.dll
True
True
@@ -315,8 +243,6 @@
-
-
@@ -331,8 +257,6 @@
-
-
@@ -347,93 +271,17 @@
-
-
-
-
-
-
-
- ..\..\packages\xunit.abstractions\lib\net35\xunit.abstractions.dll
- True
- True
-
-
-
-
-
-
- ..\..\packages\xunit.abstractions\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.abstractions.dll
- True
- True
-
-
-
-
-
-
-
-
- ..\..\packages\xunit.assert\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll
- True
- True
-
-
-
- <__paket__xunit_core_props>win8\xunit.core
- <__paket__xunit_core_targets>win8\xunit.core
-
-
-
-
- <__paket__xunit_core_props>monoandroid\xunit.core
-
-
-
-
- <__paket__xunit_core_props>monotouch\xunit.core
-
-
-
-
- <__paket__xunit_core_props>wp8\xunit.core
- <__paket__xunit_core_targets>wp8\xunit.core
-
-
-
-
- <__paket__xunit_core_props>portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core
+ <__paket__xunit_runner_visualstudio_props>win8\xunit.runner.visualstudio
+ <__paket__xunit_runner_visualstudio_targets>win8\xunit.runner.visualstudio
-
+
- <__paket__xunit_core_props>portable-win81+wpa81\xunit.core
- <__paket__xunit_core_targets>portable-win81+wpa81\xunit.core
-
-
-
-
-
-
-
-
-
- ..\..\packages\xunit.extensibility.core\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll
- True
- True
-
-
-
-
-
-
-
- <__paket__xunit_runner_visualstudio_props>win8\xunit.runner.visualstudio
- <__paket__xunit_runner_visualstudio_targets>win8\xunit.runner.visualstudio
+ <__paket__xunit_runner_visualstudio_props>net20\xunit.runner.visualstudio
@@ -442,17 +290,12 @@
<__paket__xunit_runner_visualstudio_targets>wpa81\xunit.runner.visualstudio
-
-
- <__paket__xunit_runner_visualstudio_props>net20\xunit.runner.visualstudio
-
-
-
+
<__paket__xunit_runner_visualstudio_props>portable-net45+aspnetcore50+win+wpa81+wp80+monotouch+monoandroid\xunit.runner.visualstudio
-
-
+
+
\ No newline at end of file
diff --git a/tests/CommandLine.Tests/Unit/StringToCommandLine/CSharpStyleCommandLineParserTests.cs b/tests/CommandLine.Tests/Unit/StringToCommandLine/CSharpStyleCommandLineParserTests.cs
new file mode 100644
index 00000000..af91e1f4
--- /dev/null
+++ b/tests/CommandLine.Tests/Unit/StringToCommandLine/CSharpStyleCommandLineParserTests.cs
@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Linq;
+using CommandLine.StringToCommandLine;
+using Xunit;
+
+namespace CommandLine.Tests.Unit.StringToCommandLine
+{
+ public class CSharpStyleCommandLineParserTests
+ {
+ [Fact]
+ public void TestMethod1() { RunTest("test", new[] {"test"}); }
+
+ [Fact]
+ public void TestMethod2() { RunTest("test test", new[] {"test", "test"}); }
+
+ [Fact]
+ public void TestMethod3() { RunTest("test \"test\"", new[] {"test", "test"}); }
+
+ [Fact]
+ public void TestMethod4() { RunTest("test \"te\\\"s\\\"t\"", new[] {"test", "te\"s\"t"}); }
+
+ [Fact]
+ public void TestMethod4B() { RunTest("test \"te\\\"\\\"\\\"\\\"s\\\"t\"", new[] {"test", "te\"\"\"\"s\"t"}); }
+
+ [Fact]
+ public void TestMethod5() { Assert.Throws(() => RunTest("\"abc d e", new[] {""})); }
+
+ [Fact]
+ public void TestMethod6() { Assert.Throws(() => RunTest("asd\\", new[] {""})); }
+
+ [Fact]
+ public void TestMethod7() { RunTest("\\\\\\a\\b\\'\\\"\\0\\f\\t\\v", new[] {"\\\a\b\'\"\0\f\t\v"}); }
+
+ [Fact]
+ public void TestMethod8() { RunTest("Hello\\x1\\x12\\x123\\x1234", new[] {"Hello\x1\x12\x123\x1234"}); }
+
+ private static void RunTest(string commandLine, ICollection expected)
+ {
+ var parser = new CSharpStyleCommandLineParser();
+ var enumerable = parser.Parse(commandLine);
+ Assert.Equal(expected, enumerable.ToArray());
+ }
+ }
+}
diff --git a/tests/CommandLine.Tests/Unit/StringToCommandLine/WindowsCommandLineWithEscapesParserTests.cs b/tests/CommandLine.Tests/Unit/StringToCommandLine/WindowsCommandLineWithEscapesParserTests.cs
new file mode 100644
index 00000000..a3e6dcfb
--- /dev/null
+++ b/tests/CommandLine.Tests/Unit/StringToCommandLine/WindowsCommandLineWithEscapesParserTests.cs
@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Linq;
+using CommandLine.StringToCommandLine;
+using Xunit;
+
+namespace CommandLine.Tests.Unit.StringToCommandLine
+{
+ public class DefaultWindowsCommandLineParserTests
+ {
+ [Fact]
+ public void TestMethod1() { RunTest("test", new[] {"test"}); }
+
+ [Fact]
+ public void TestMethod2() { RunTest("test test", new[] {"test", "test"}); }
+
+ [Fact]
+ public void TestMethod3() { RunTest("test \"test\"", new[] {"test", "test"}); }
+
+ [Fact]
+ public void TestMethod4() { RunTest("test \"te\"s\"t\"", new[] {"test", "test"}); }
+
+ [Fact]
+ public void TestMethod4B() { RunTest("test \"te\"\"\"\"s\"t\"", new[] {"test", "te\"\"st"}); }
+
+ [Fact]
+ public void TestMethod5() { RunTest("\"abc\" d e", new[] {"abc", "d", "e"}); }
+
+ [Fact]
+ public void TestMethod6() { RunTest("a\\\\b d\"e f\"g h", new[] {"a\\\\b", "de fg", "h"}); }
+
+ [Fact]
+ public void TestMethod7() { RunTest("a\\\\\\\"b c d", new[] {"a\\\"b", "c", "d"}); }
+
+ [Fact]
+ public void TestMethod8() { RunTest("a\\\\\\\\\"b c\" d e", new[] {"a\\\\b c", "d", "e"}); }
+
+ private static void RunTest(string commandLine, ICollection expected)
+ {
+ var parser = new DefaultWindowsCommandLineParser();
+ var enumerable = parser.Parse(commandLine);
+ Assert.Equal(expected, enumerable.ToArray());
+ }
+ }
+}