diff --git a/src/Orchard.Specs/Bindings/CommandLine.cs b/src/Orchard.Specs/Bindings/CommandLine.cs index 50d0e361e..b494a69d8 100644 --- a/src/Orchard.Specs/Bindings/CommandLine.cs +++ b/src/Orchard.Specs/Bindings/CommandLine.cs @@ -10,10 +10,10 @@ namespace Orchard.Specs.Bindings { [Binding] public class CommandLine : BindingBase { [When(@"I execute >(.*)")] - public void WhenIExecute(string commandLine) { + public void WhenIExecute(string commandLine) { var details = new RequestDetails(); Binding().Host.Execute(() => { - var args = commandLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + var args = new CommandLineParser().Parse(commandLine); var parameters = new CommandParametersParser().Parse(args); var agent = new CommandHostAgent(); var input = new StringReader(""); diff --git a/src/Orchard.sln b/src/Orchard.sln index b38424da6..f52de9bea 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -75,6 +75,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Packaging", "Orchar EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrchardCLI", "Tools\OrchardCli\OrchardCLI.csproj", "{71A006E0-85BD-4CC4-ADF9-B548D5CA72A7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Tests", "Tools\Orchard.Tests\Orchard.Tests.csproj", "{0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -213,6 +215,10 @@ Global {71A006E0-85BD-4CC4-ADF9-B548D5CA72A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {71A006E0-85BD-4CC4-ADF9-B548D5CA72A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {71A006E0-85BD-4CC4-ADF9-B548D5CA72A7}.Release|Any CPU.Build.0 = Release|Any CPU + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -247,6 +253,7 @@ Global {33B1BC8D-E292-4972-A363-22056B207156} = {383DBA32-4A3E-48D1-AAC3-75377A694452} {8A4E42CE-79F8-4BE2-8B1E-A6B83432123B} = {383DBA32-4A3E-48D1-AAC3-75377A694452} {71A006E0-85BD-4CC4-ADF9-B548D5CA72A7} = {383DBA32-4A3E-48D1-AAC3-75377A694452} + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE} = {383DBA32-4A3E-48D1-AAC3-75377A694452} {E65E5633-C0FF-453C-A906-481C14F969D6} = {E75A4CE4-CAA6-41E4-B951-33ACC60DC77C} EndGlobalSection EndGlobal diff --git a/src/Tools/Orchard.Tests/CommandLineParserTests.cs b/src/Tools/Orchard.Tests/CommandLineParserTests.cs new file mode 100644 index 000000000..a81d98fb1 --- /dev/null +++ b/src/Tools/Orchard.Tests/CommandLineParserTests.cs @@ -0,0 +1,185 @@ +using System.Linq; +using NUnit.Framework; +using Orchard.Parameters; + +namespace Orchard.Tests { + [TestFixture] + public class CommandLineParseTests { + [Test] + public void ParserUnderstandsSimpleArguments() { + // a b cdef + // => a + // => b + // => cdef + var result = new CommandLineParser().Parse("a b cdef").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("a")); + Assert.That(result[1], Is.EqualTo("b")); + Assert.That(result[2], Is.EqualTo("cdef")); + } + + [Test] + public void ParserIgnoresExtraSpaces() { + // a b cdef + // => a + // => b + // => cdef + var result = new CommandLineParser().Parse(" a b cdef ").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("a")); + Assert.That(result[1], Is.EqualTo("b")); + Assert.That(result[2], Is.EqualTo("cdef")); + } + + [Test] + public void ParserGroupsQuotedArguments() { + // feature enable "a b cdef" + // => feature + // => enable + // => a b cdef + var result = new CommandLineParser().Parse("feature enable \"a b cdef\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("a b cdef")); + } + + [Test] + public void ParserUnderstandsQuotesInsideArgument() { + // feature enable /foo:"a b cdef" + // => feature + // => enable + // => /foo:a b cdef + var result = new CommandLineParser().Parse("feature enable /foo:\"a b cdef\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("/foo:a b cdef")); + } + + [Test] + public void ParserBackslashEscapesQuote() { + // feature enable \"a b cdef\" + // => feature + // => enable + // => "a + // => b + // => cdef" + var result = new CommandLineParser().Parse("feature enable \\\"a b cdef\\\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(5)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("\"a")); + Assert.That(result[3], Is.EqualTo("b")); + Assert.That(result[4], Is.EqualTo("cdef\"")); + } + + [Test] + public void ParserBackslashDoesnotEscapeBackslash() { + // feature enable \\a + // => feature + // => enable + // => \\a + var result = new CommandLineParser().Parse("feature enable \\\\a").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("\\\\a")); + } + + [Test] + public void ParserBackslashDoesnotEscapeOtherCharacters() { + // feature enable \a + // => feature + // => enable + // => \a + var result = new CommandLineParser().Parse("feature enable \\a").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("\\a")); + } + + [Test] + public void ParserUnderstandsTrailingBackslash() { + // feature enable \ + // => feature + // => enable + // => \ + var result = new CommandLineParser().Parse("feature enable \\").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("\\")); + } + + [Test] + public void ParserUnderstandsTrailingBackslash2() { + // feature enable b\ + // => feature + // => enable + // => b\ + var result = new CommandLineParser().Parse("feature enable b\\").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("b\\")); + } + + [Test] + public void ParserUnderstandsEmptyArgument() { + // feature enable "" + // => feature + // => enable + // => + var result = new CommandLineParser().Parse("feature enable \"\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("")); + } + + [Test] + public void ParserUnderstandsTrailingQuote() { + // feature enable " + // => feature + // => enable + // => + var result = new CommandLineParser().Parse("feature enable \"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("feature")); + Assert.That(result[1], Is.EqualTo("enable")); + Assert.That(result[2], Is.EqualTo("")); + } + + [Test] + public void ParserUnderstandsEmptyArgument2() { + // " + // => + var result = new CommandLineParser().Parse("\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0], Is.EqualTo("")); + } + [Test] + public void ParserUnderstandsEmptyArgument3() { + // "" + // => + var result = new CommandLineParser().Parse("\"\"").ToList(); + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0], Is.EqualTo("")); + } + } +} diff --git a/src/Tools/Orchard.Tests/Orchard.Tests.csproj b/src/Tools/Orchard.Tests/Orchard.Tests.csproj new file mode 100644 index 000000000..931d5d79d --- /dev/null +++ b/src/Tools/Orchard.Tests/Orchard.Tests.csproj @@ -0,0 +1,113 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {0DFA2E10-96C8-4E05-BC10-B710B97ECCDE} + Library + Properties + Orchard.Tests + Orchard.Tests + v4.0 + 512 + + + 3.5 + + false + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + x86 + AllRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + AllRules.ruleset + + + + False + ..\..\..\lib\moq\Moq.dll + + + False + ..\..\..\lib\nunit\nunit.framework.dll + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + False + Windows Installer 3.1 + true + + + + + {33B1BC8D-E292-4972-A363-22056B207156} + Orchard + + + + + \ No newline at end of file diff --git a/src/Tools/Orchard.Tests/Properties/AssemblyInfo.cs b/src/Tools/Orchard.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5f187c007 --- /dev/null +++ b/src/Tools/Orchard.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchad.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("Copyright © CodePlex Foundation 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("db4cb512-5c00-44c9-9173-7ede47af1967")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.5.0")] +[assembly: AssemblyFileVersion("0.5.0")] diff --git a/src/Tools/Orchard/Orchard.csproj b/src/Tools/Orchard/Orchard.csproj index 93d375fb4..00534449f 100644 --- a/src/Tools/Orchard/Orchard.csproj +++ b/src/Tools/Orchard/Orchard.csproj @@ -77,6 +77,7 @@ + diff --git a/src/Tools/Orchard/Parameters/CommandLineParser.cs b/src/Tools/Orchard/Parameters/CommandLineParser.cs new file mode 100644 index 000000000..35eed1861 --- /dev/null +++ b/src/Tools/Orchard/Parameters/CommandLineParser.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Orchard.Parameters { + public interface ICommandLineParser { + IEnumerable Parse(string commandLine); + } + + public class CommandLineParser : ICommandLineParser { + public IEnumerable Parse(string commandLine) { + return SplitArgs(commandLine); + } + + public class State { + private readonly string _commandLine; + private readonly StringBuilder _stringBuilder; + private readonly List _arguments; + private int _index; + + public State(string commandLine) { + _commandLine = commandLine; + _stringBuilder = new StringBuilder(); + _arguments = new List(); + } + + public StringBuilder StringBuilder { get { return _stringBuilder; } } + public bool EOF { get { return _index >= _commandLine.Length; } } + public char Current { get { return _commandLine[_index]; } } + public IEnumerable Arguments { get { return _arguments; } } + + public void AddArgument() { + _arguments.Add(StringBuilder.ToString()); + StringBuilder.Clear(); + } + + public void AppendCurrent() { + StringBuilder.Append(Current); + } + + public void Append(char ch) { + StringBuilder.Append(ch); + } + + public void MoveNext() { + if (!EOF) + _index++; + } + } + + /// + /// Implement the same logic as found at + /// http://msdn.microsoft.com/en-us/library/17w5ykft.aspx + /// The 3 special characters are quote, backslash and whitespaces, in order + /// of priority. + /// The semantics of a quote is: whatever the state of the lexer, copy + /// all characters verbatim until the next quote or EOF. + /// The semantics of backslash is: If the next character is a backslash or a quote, + /// copy the next character. Otherwise, copy the backslash and the next character. + /// The semantics of whitespace is: end the current argument and move on to the next one. + /// + private IEnumerable SplitArgs(string commandLine) { + var state = new State(commandLine); + while (!state.EOF) { + switch (state.Current) { + case '"': + ProcessQuote(state); + break; + + case '\\': + ProcessBackslash(state); + break; + + case ' ': + case '\t': + if (state.StringBuilder.Length > 0) + state.AddArgument(); + state.MoveNext(); + break; + + default: + state.AppendCurrent(); + state.MoveNext(); + break; + } + } + if (state.StringBuilder.Length > 0) + state.AddArgument(); + return state.Arguments; + } + + private void ProcessQuote(State state) { + state.MoveNext(); + while (!state.EOF) { + if (state.Current == '"') { + state.MoveNext(); + break; + } + state.AppendCurrent(); + state.MoveNext(); + } + + state.AddArgument(); + } + + private void ProcessBackslash(State state) { + state.MoveNext(); + if (state.EOF) { + state.Append('\\'); + return; + } + + if (state.Current == '"') { + state.Append('"'); + state.MoveNext(); + } + else { + state.Append('\\'); + state.AppendCurrent(); + state.MoveNext(); + } + } + } +} \ No newline at end of file diff --git a/src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs b/src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs index 037039c41..9a2c527d7 100644 --- a/src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs +++ b/src/Tools/Orchard/ResponseFiles/ResponseFileReader.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; +using Orchard.Parameters; namespace Orchard.ResponseFiles { public class ResponseLine : MarshalByRefObject { @@ -24,48 +24,10 @@ namespace Orchard.ResponseFiles { Filename = filename, LineText = lineText, LineNumber = i, - Args = SplitArgs(lineText).ToArray() + Args = new CommandLineParser().Parse(lineText).ToArray() }; } } } - - public static IEnumerable SplitArgs(string text) { - var sb = new StringBuilder(); - bool inString = false; - foreach (char ch in text) { - switch(ch){ - case '"': - if (inString) { - inString = false; - yield return sb.ToString(); - sb.Length = 0; - } - else { - inString = true; - sb.Length = 0; - } - break; - - case ' ': - case '\t': - if (sb.Length > 0) { - yield return sb.ToString(); - sb.Length = 0; - } - break; - - default: - sb.Append(ch); - break; - } - } - - // If there was anything accumulated - if (sb.Length > 0) { - yield return sb.ToString(); - sb.Length = 0; - } - } } } diff --git a/src/Tools/OrchardCli/CLIHost.cs b/src/Tools/OrchardCli/CLIHost.cs index 6f24a4370..d2a5ff5c7 100644 --- a/src/Tools/OrchardCli/CLIHost.cs +++ b/src/Tools/OrchardCli/CLIHost.cs @@ -90,7 +90,7 @@ namespace OrchardCLI { private int RunCommandInSession(CommandHostContext context, string command) { try { - var args = new OrchardParametersParser().Parse(new CommandParametersParser().Parse(ResponseFileReader.SplitArgs(command))); + var args = new OrchardParametersParser().Parse(new CommandParametersParser().Parse(new CommandLineParser().Parse(command))); return context.CommandHost.RunCommandInSession(_input, _output, context.Logger, args); } catch (AppDomainUnloadedException) { diff --git a/src/Tools/OrchardCli/OrchardCLI.csproj b/src/Tools/OrchardCli/OrchardCLI.csproj index 840eefac8..e6081f584 100644 --- a/src/Tools/OrchardCli/OrchardCLI.csproj +++ b/src/Tools/OrchardCli/OrchardCLI.csproj @@ -98,6 +98,9 @@ OrchardParametersParser.cs + + Parameters\CommandLineParser.cs + Parameters\CommandParameters.cs