Skip to content
This repository has been archived by the owner on Dec 18, 2017. It is now read-only.

Commit

Permalink
Fix #876, Correct handling of quotation marks in commands
Browse files Browse the repository at this point in the history
- don't strip quotes surrounding command-line arguments when used in `scripts`
 - prevented command paths and grouping arguments containing spaces
 - pass `preserveSurroundingQuotes` into `CommandGrammar` to control stripping
- use `cmd /s /c` when executing `scripts` commands on Windows
 - allows `&&` and similar
- add functional tests of commands and scripts (together with `dnu restore`)
 - no `dnu restore` for .NET Core due to extra time required

note:
- `CommandGrammar` may need additional generalizations
 - e.g. unquoted term can't contain more than one escape sequence
 - but it's probably good enough for now
- _not_ adding quotes around variables replacements inserted into commands
 - no way of determining if replacement is already quoted
 - basically, leave it to project.json author to perform appropriate quoting for their platform

nits:
- let VS do its thing with test services
- make `CommandGrammar` constructor private; used only in `Process()` method
  • Loading branch information
dougbu committed Jun 2, 2015
1 parent 8e6538d commit 1f0f120
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 13 deletions.
Expand Up @@ -9,12 +9,12 @@ namespace Microsoft.Framework.ApplicationHost.Impl.Syntax
{
internal class CommandGrammar : Grammar
{
public CommandGrammar(Func<string, string> variable)
private CommandGrammar(Func<string, string> variable, bool preserveSurroundingQuotes)
{
var environmentVariablePiece = Ch('%').And(Rep(Ch().Not(Ch('%')))).And(Ch('%')).Left().Down().Str()
.Build(key => variable(key) ?? "%" + key + "%");

var escapeSequencePiece =
var escapeSequencePiece =
Ch('%').And(Ch('%')).Build(_=>"%")
.Or(Ch('^').And(Ch('^')).Build(_ => "^"))
.Or(Ch('\\').And(Ch('\\')).Build(_ => "\\"))
Expand All @@ -30,6 +30,12 @@ public CommandGrammar(Func<string, string> variable)
var unquotedTerm = Rep1(unquotedPiece.Or(specialPiece)).Str();

var quotedTerm = Ch('\"').And(Rep(quotedPiece.Or(specialPiece)).Str()).And(Ch('\"')).Left().Down();
if (preserveSurroundingQuotes)
{
// Str() value assigned to quotedTerm does not include quotation marks surrounding the quoted or
// special piece. Add those quotes back if requested.
quotedTerm = quotedTerm.Build(str => "\"" + str + "\"");
}

var whitespace = Rep(Ch(' '));

Expand All @@ -40,9 +46,9 @@ public CommandGrammar(Func<string, string> variable)

public readonly Parser<IList<string>> Parse;

public static string[] Process(string text, Func<string, string> variables)
public static string[] Process(string text, Func<string, string> variables, bool preserveSurroundingQuotes)
{
var grammer = new CommandGrammar(variables);
var grammer = new CommandGrammar(variables, preserveSurroundingQuotes);
var cursor = new Cursor(text, 0, text.Length);

var result = grammer.Parse(cursor);
Expand Down
8 changes: 5 additions & 3 deletions src/Microsoft.Framework.ApplicationHost/Program.cs
Expand Up @@ -50,9 +50,11 @@ public Task<int> Main(string[] args)
string replacementCommand;
if (host.Project.Commands.TryGetValue(lookupCommand, out replacementCommand))
{
var replacementArgs = CommandGrammar.Process(
replacementCommand,
GetVariable).ToArray();
// preserveSurroundingQuotes: false to imitate a shell. Shells remove quotation marks before calling
// Main methods. Here however we are invoking Main() without involving a shell.
var replacementArgs = CommandGrammar
.Process(replacementCommand, GetVariable, preserveSurroundingQuotes: false)
.ToArray();
options.ApplicationName = replacementArgs.First();
programArgs = replacementArgs.Skip(1).Concat(programArgs).ToArray();
}
Expand Down
13 changes: 8 additions & 5 deletions src/Microsoft.Framework.PackageManager/Scripts/ScriptExecutor.cs
Expand Up @@ -34,9 +34,12 @@ public bool Execute(Runtime.Project project, string scriptName, Func<string, str

foreach (var scriptCommandLine in scriptCommandLines)
{
// Preserve quotation marks around arguments since command is about to be passed to a shell. May need
// the quotes to ensure that shell groups arguments correctly.
var scriptArguments = CommandGrammar.Process(
scriptCommandLine,
GetScriptVariable(project, getVariable));
GetScriptVariable(project, getVariable),
preserveSurroundingQuotes: true);

// Ensure the array won't be empty and the first element won't be null or empty string.
scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray();
Expand All @@ -49,17 +52,17 @@ public bool Execute(Runtime.Project project, string scriptName, Func<string, str
if (!PlatformHelper.IsMono)
{
// Forward-slash is used in script blocked only. Replace them with back-slash to correctly
// locate the script. The directory separator is platform-specific.
// locate the script. The directory separator is platform-specific.
scriptArguments[0] = scriptArguments[0].Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

// Command-lines on Windows are executed via "cmd /C" in order
// to support batch files, &&, built-in commands like echo, etc.
// Command-lines on Windows are executed via "cmd /S /C" in order to support batch files, &&,
// built-in commands like echo, et cetera. /S allows quoting the command as well as the arguments.
// ComSpec is Windows-specific, and contains the full path to cmd.exe
var comSpec = Environment.GetEnvironmentVariable("ComSpec");
if (!string.IsNullOrEmpty(comSpec))
{
scriptArguments =
new[] { comSpec, "/C", "\"" }
new[] { comSpec, "/S", "/C", "\"" }
.Concat(scriptArguments)
.Concat(new[] { "\"" })
.ToArray();
Expand Down
@@ -1,12 +1,12 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Versioning;
using Bootstrapper.FunctionalTests;
using Microsoft.Framework.CommonTestUtils;
using Microsoft.Framework.PackageManager;
using Microsoft.Framework.Runtime;
using Microsoft.Framework.Runtime.Common.Impl;
using NuGet;
Expand Down Expand Up @@ -170,5 +170,136 @@ public void AppHostShowsErrorWhenCurrentTargetFrameworkWasNotFoundInProjectJson(
Assert.Contains(expectedErrorMsg, stdErr);
}
}

public static IEnumerable<object[]> ClrRuntimeComponentsAndCommandsSet
{
get
{
// command name -> expected output
var commands = new Dictionary<string, string>
{
{
"one",
@"0: 'one'
1: 'two'
2: 'extra'
"
},
{
"two",
@"0: '^>three'
1: '&&>>^""'
2: 'extra'
"
},
{
"three",
@"0: 'four'
1: 'argument five'
2: 'extra'
"
},
{
"run",
@"0: 'extra'
"
},
};

var data = new TheoryData<object, object, object, string, string>();
foreach (var component in TestUtils.GetClrRuntimeComponents())
{
foreach (var command in commands)
{
data.Add(component[0], component[1], component[2], command.Key, command.Value);
}
}

return data;
}
}

[Theory]
[MemberData(nameof(ClrRuntimeComponentsAndCommandsSet))]
public void AppHost_ExecutesCommands(
string flavor,
string os,
string architecture,
string command,
string expectedOutput)
{
var environment = new Dictionary<string, string>
{
{ "DNX_TRACE", "0" },
};

var projectName = "Project Name";
var projectStructure =
$@"{{
'.': ['Program.cs', '{ Project.ProjectFileName }']
}}";
var programContents =
@"using System;
namespace Project_Name
{
public class Program
{
public void Main(string[] arguments)
{
for (var i = 0; i < arguments.Length; i++)
{
var argument = arguments[i];
if (!string.IsNullOrWhiteSpace(argument))
{
Console.WriteLine($""{ i }: '{ argument }'"");
}
}
}
}
}";
var projectJsonContents =
$@"{{
""commands"": {{
""one"": ""\""{ projectName }\"" one two"",
""two"": ""\""{ projectName }\"" ^>three &&>>^\"""",
""three"": ""\""{ projectName }\"" four \""argument five\""""
}},
""frameworks"" : {{
""dnx451"": {{ }}
}}
}}";

using (var applicationRoot = TestUtils.CreateTempDir())
{
var projectPath = Path.Combine(applicationRoot, projectName);
DirTree.CreateFromJson(projectStructure)
.WithFileContents("Program.cs", programContents)
.WithFileContents(Project.ProjectFileName, projectJsonContents)
.WriteTo(projectPath);
var runtimeHomePath = _fixture.GetRuntimeHomeDir(flavor, os, architecture);

var exitCode = DnuTestUtils.ExecDnu(
runtimeHomePath,
subcommand: "restore",
arguments: null,
environment: environment,
workingDir: projectPath);
Assert.Equal(0, exitCode); // Guard

string output;
string error;
exitCode = BootstrapperTestUtils.ExecBootstrapper(
runtimeHomePath,
arguments: $@"""{ projectPath }"" { command } extra",
stdOut: out output,
stdErr: out error,
environment: environment);

Assert.Equal(0, exitCode);
Assert.Empty(error);
Assert.Equal(expectedOutput, output);
}
}
}
}
@@ -0,0 +1,149 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.IO;
using Microsoft.Framework.CommonTestUtils;
using Microsoft.Framework.PackageManager.FunctionalTests;
using Xunit;

namespace Microsoft.Framework.PackageManager
{
[Collection(nameof(PackageManagerFunctionalTestCollection))]
public class DnuRestoreTests
{
private readonly PackageManagerFunctionalTestFixture _fixture;

public DnuRestoreTests(PackageManagerFunctionalTestFixture fixture)
{
_fixture = fixture;
}

public static IEnumerable<object[]> RuntimeComponents
{
get
{
return TestUtils.GetRuntimeComponentsCombinations();
}
}

[Theory]
[MemberData(nameof(RuntimeComponents))]
public void DnuRestore_ExecutesScripts(string flavor, string os, string architecture)
{
bool isWindows = TestUtils.CurrentRuntimeEnvironment.OperatingSystem == "Windows";
var environment = new Dictionary<string, string>
{
{ "DNX_TRACE", "0" },
};

var expectedPreContent =
@"""one""
""two""
"">three""
""four""
";
var expectedPostContent =
@"""five""
""six""
""argument seven""
""argument eight""
";

string projectJsonContent;
string scriptContent;
string scriptName;
if (isWindows)
{
projectJsonContent =
@"{
""frameworks"": {
""dnx451"": { }
},
""scripts"": {
""prerestore"": [
""script.cmd one two > pre"",
""script.cmd ^>three >> pre && script.cmd ^ four >> pre""
],
""postrestore"": [
""\""%project:Directory%/script.cmd\"" five six > post"",
""\""%project:Directory%/script.cmd\"" \""argument seven\"" \""argument eight\"" >> post""
]
}
}";
scriptContent =
@"@echo off
:argumentStart
if ""%~1""=="""" goto argumentEnd
echo ""%~1""
shift
goto argumentStart
:argumentEnd";
scriptName = "script.cmd";
}
else
{
projectJsonContent =
@"{
""frameworks"": {
""dnx451"": { }
},
""scripts"": {
""prerestore"": [
""script.sh one two > pre"",
""script.sh \>three >> pre; script.sh \ four >> pre""
],
""postrestore"": [
""\""%project:Directory%/script.sh\"" five six > post"",
""\""%project:Directory%/script.sh\"" \""argument seven\"" \""argument eight\"" >> post""
]
}
}";
scriptContent =
@"#!/bin/bash --restricted
set -o errexit
for arg in ""$@""; do
printf ""\""%s\""\n"" ""$arg""
done";
scriptName = "script.sh";
}

var projectStructure =
$@"{{
'.': ['project.json', '{ scriptName }']
}}";
var runtimeHomePath = _fixture.GetRuntimeHomeDir(flavor, os, architecture);
using (var testEnv = new DnuTestEnvironment(runtimeHomePath, projectName: "Project Name"))
{
DirTree.CreateFromJson(projectStructure)
.WithFileContents("project.json", projectJsonContent)
.WithFileContents(scriptName, scriptContent)
.WriteTo(testEnv.ProjectPath);
FileOperationUtils.MarkExecutable(Path.Combine(testEnv.ProjectPath, scriptName));

string output;
string error;
var exitCode = DnuTestUtils.ExecDnu(
runtimeHomePath,
subcommand: "restore",
arguments: null,
stdOut: out output,
stdErr: out error,
environment: environment,
workingDir: testEnv.ProjectPath);

Assert.Equal(0, exitCode);
Assert.Empty(error);
Assert.Contains("Executing script 'prerestore' in project.json", output);
Assert.Contains("Executing script 'postrestore' in project.json", output);

var preContent = File.ReadAllText(Path.Combine(testEnv.ProjectPath, "pre"));
Assert.Equal(expectedPreContent, preContent);
var postContent = File.ReadAllText(Path.Combine(testEnv.ProjectPath, "post"));
Assert.Equal(expectedPostContent, postContent);
}
}
}
}

0 comments on commit 1f0f120

Please sign in to comment.