Fix command line parser to support quotes

--HG--
branch : dev
This commit is contained in:
Renaud Paquay 2010-07-28 13:06:48 -07:00
parent b2be561e3f
commit f846335852
10 changed files with 474 additions and 43 deletions

View File

@ -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<WebAppHosting>().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("");

View File

@ -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

View File

@ -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
// => <empty arg>
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
// => <empty arg>
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() {
// "
// => <empty arg>
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() {
// ""
// => <empty arg>
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(""));
}
}
}

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{0DFA2E10-96C8-4E05-BC10-B710B97ECCDE}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.Tests</RootNamespace>
<AssemblyName>Orchard.Tests</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>3.5</OldToolsVersion>
<UpgradeBackupLocation />
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<PlatformTarget>x86</PlatformTarget>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Moq, Version=4.0.812.4, Culture=neutral, PublicKeyToken=69f491c39445e920, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\lib\moq\Moq.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=2.5.2.9222, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\lib\nunit\nunit.framework.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Xml.Linq">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data.DataSetExtensions">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="CommandLineParserTests.cs" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Windows.Installer.3.1">
<Visible>False</Visible>
<ProductName>Windows Installer 3.1</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Orchard\Orchard.csproj">
<Project>{33B1BC8D-E292-4972-A363-22056B207156}</Project>
<Name>Orchard</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -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")]

View File

@ -77,6 +77,7 @@
<Compile Include="HostContext\ICommandHostContextProvider.cs" />
<Compile Include="Logger.cs" />
<Compile Include="OrchardHost.cs" />
<Compile Include="Parameters\CommandLineParser.cs" />
<Compile Include="Parameters\ICommandParametersParser.cs" />
<Compile Include="IOrchardParametersParser.cs" />
<Compile Include="OrchardParameters.cs" />

View File

@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Orchard.Parameters {
public interface ICommandLineParser {
IEnumerable<string> Parse(string commandLine);
}
public class CommandLineParser : ICommandLineParser {
public IEnumerable<string> Parse(string commandLine) {
return SplitArgs(commandLine);
}
public class State {
private readonly string _commandLine;
private readonly StringBuilder _stringBuilder;
private readonly List<string> _arguments;
private int _index;
public State(string commandLine) {
_commandLine = commandLine;
_stringBuilder = new StringBuilder();
_arguments = new List<string>();
}
public StringBuilder StringBuilder { get { return _stringBuilder; } }
public bool EOF { get { return _index >= _commandLine.Length; } }
public char Current { get { return _commandLine[_index]; } }
public IEnumerable<string> 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++;
}
}
/// <summary>
/// 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.
/// </summary>
private IEnumerable<string> 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();
}
}
}
}

View File

@ -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<string> 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;
}
}
}
}

View File

@ -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) {

View File

@ -98,6 +98,9 @@
<Compile Include="..\Orchard\OrchardParametersParser.cs">
<Link>OrchardParametersParser.cs</Link>
</Compile>
<Compile Include="..\Orchard\Parameters\CommandLineParser.cs">
<Link>Parameters\CommandLineParser.cs</Link>
</Compile>
<Compile Include="..\Orchard\Parameters\CommandParameters.cs">
<Link>Parameters\CommandParameters.cs</Link>
</Compile>