Basic Argument Forwarding and Escaping Code Additions/Changes

This commit is contained in:
Bryan 2016-01-22 14:03:40 -08:00 committed by Bryan Thornbury
parent fce9666f37
commit e794ad6a10
4 changed files with 349 additions and 58 deletions

View file

@ -0,0 +1,216 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Cli.Utils
{
public static class ArgumentEscaper
{
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static string EscapeAndConcatenateArgArray(IEnumerable<string> args, bool cmd=false)
{
var sb = new StringBuilder();
var first = false;
foreach (var arg in args)
{
if (first)
{
first = false;
}
else
{
sb.Append(' ');
}
sb.Append(EscapeArg(arg, cmd));
}
return sb.ToString();
}
public static string EscapeAndConcatenateArgArrayForBash(IEnumerable<string> args)
{
return EscapeAndConcatenateArgArray(EscapeArgArrayForBash(args));
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForCmd(IEnumerable<string> args)
{
return EscapeAndConcatenateArgArray(EscapeArgArrayForCmd(args), true);
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static IEnumerable<string> EscapeArgArray(IEnumerable<string> args)
{
var escapedArgs = new List<string>();
foreach (var arg in args)
{
escapedArgs.Add(EscapeArg(arg));
}
return escapedArgs;
}
public static IEnumerable<string> EscapeArgArrayForBash(IEnumerable<string> arguments)
{
var escapedArgs = new List<string>();
foreach (var arg in arguments)
{
escapedArgs.Add(EscapeArgForBash(arg));
}
return escapedArgs;
}
/// <summary>
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static IEnumerable<string> EscapeArgArrayForCmd(IEnumerable<string> arguments)
{
var escapedArgs = new List<string>();
foreach (var arg in arguments)
{
escapedArgs.Add(EscapeArgForCmd(arg));
}
return escapedArgs;
}
private static string EscapeArg(string arg, bool cmd=false)
{
var sb = new StringBuilder();
// Always quote beginning and end to account for possible spaces
if (cmd) sb.Append('^');
sb.Append('"');
if (!cmd)
{
for (int i = 0; i < arg.Length; ++i)
{
var backslashCount = 0;
// Consume All Backslashes
while (i < arg.Length && arg[i] == '\\')
{
backslashCount++;
i++;
}
// Escape any backslashes at the end of the arg
// This ensures the outside quote is interpreted as
// an argument delimiter
if (i == arg.Length)
{
sb.Append('\\', 2 * backslashCount);
}
// Escape any preceding backslashes and the quote
else if (arg[i] == '"')
{
sb.Append('\\', (2 * backslashCount) + 1);
sb.Append('"');
}
// Output any consumed backslashes and the character
else
{
sb.Append('\\', backslashCount);
sb.Append(arg[i]);
}
}
}
else
{
for (int i = 0; i < arg.Length; ++i)
{
if (arg[i] == '"')
{
sb.Append('"');
sb.Append('^');
sb.Append(arg[i]);
}
else
{
sb.Append(arg[i]);
}
}
}
if (cmd) sb.Append('^');
sb.Append('"');
return sb.ToString();
}
private static string EscapeArgForBash(string arguments)
{
throw new NotImplementedException();
}
/// <summary>
/// Prepare as single argument to
/// roundtrip properly through cmd.
///
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
///
/// See here for more info:
/// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static string EscapeArgForCmd(string arguments)
{
var sb = new StringBuilder();
foreach (var character in arguments)
{
sb.Append('^');
sb.Append(character);
}
return sb.ToString();
}
}
}

View file

@ -39,20 +39,25 @@ namespace Microsoft.DotNet.Cli.Utils
ResolutionStrategy = commandSpec.ResolutionStrategy; ResolutionStrategy = commandSpec.ResolutionStrategy;
} }
public static Command Create(string commandName, IEnumerable<string> args, NuGetFramework framework = null) /// <summary>
/// Create a command with the specified arg array. Args will be
/// escaped properly to ensure that exactly the strings in this
/// array will be present in the corresponding argument array
/// in the command's process.
/// </summary>
/// <param name="commandName"></param>
/// <param name="args"></param>
/// <param name="framework"></param>
/// <returns></returns>
public static Command Create(string commandName, IEnumerable<string> args, NuGetFramework framework = null, bool useComSpec = false)
{ {
return Create(commandName, string.Join(" ", args), framework); var commandSpec = CommandResolver.TryResolveCommandSpec(commandName, args, framework, useComSpec=useComSpec);
}
public static Command Create(string commandName, string args, NuGetFramework framework = null)
{
var commandSpec = CommandResolver.TryResolveCommandSpec(commandName, args, framework);
if (commandSpec == null) if (commandSpec == null)
{ {
throw new CommandUnknownException(commandName); throw new CommandUnknownException(commandName);
} }
var command = new Command(commandSpec); var command = new Command(commandSpec);
return command; return command;
@ -60,6 +65,7 @@ namespace Microsoft.DotNet.Cli.Utils
public CommandResult Execute() public CommandResult Execute()
{ {
Reporter.Verbose.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); Reporter.Verbose.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}");
ThrowIfRunning(); ThrowIfRunning();

View file

@ -12,45 +12,56 @@ namespace Microsoft.DotNet.Cli.Utils
{ {
internal static class CommandResolver internal static class CommandResolver
{ {
public static CommandSpec TryResolveCommandSpec(string commandName, string args, NuGetFramework framework = null) public static CommandSpec TryResolveCommandSpec(string commandName, IEnumerable<string> args, NuGetFramework framework = null, bool useComSpec = false)
{ {
return ResolveFromRootedCommand(commandName, args) ?? return ResolveFromRootedCommand(commandName, args, useComSpec) ??
ResolveFromProjectDependencies(commandName, args, framework) ?? ResolveFromProjectDependencies(commandName, args, framework, useComSpec) ??
ResolveFromProjectTools(commandName, args) ?? ResolveFromProjectTools(commandName, args, useComSpec) ??
ResolveFromAppBase(commandName, args) ?? ResolveFromAppBase(commandName, args, useComSpec) ??
ResolveFromPath(commandName, args); ResolveFromPath(commandName, args, useComSpec);
} }
private static CommandSpec ResolveFromPath(string commandName, string args) private static CommandSpec ResolveFromPath(string commandName, IEnumerable<string> args, bool useComSpec = false)
{ {
var commandPath = Env.GetCommandPath(commandName); var commandPath = Env.GetCommandPath(commandName);
if (commandPath != null) Console.WriteLine("path?");
return commandPath == null return commandPath == null
? null ? null
: CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.Path); : CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.Path, useComSpec);
} }
private static CommandSpec ResolveFromAppBase(string commandName, string args) private static CommandSpec ResolveFromAppBase(string commandName, IEnumerable<string> args, bool useComSpec = false)
{ {
var commandPath = Env.GetCommandPathFromAppBase(AppContext.BaseDirectory, commandName); var commandPath = Env.GetCommandPathFromAppBase(AppContext.BaseDirectory, commandName);
if (commandPath != null) Console.WriteLine("appbase?");
return commandPath == null return commandPath == null
? null ? null
: CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.BaseDirectory); : CreateCommandSpecPreferringExe(commandName, args, commandPath, CommandResolutionStrategy.BaseDirectory, useComSpec);
} }
private static CommandSpec ResolveFromRootedCommand(string commandName, string args) private static CommandSpec ResolveFromRootedCommand(string commandName, IEnumerable<string> args, bool useComSpec = false)
{ {
if (Path.IsPathRooted(commandName)) if (Path.IsPathRooted(commandName))
{ {
return new CommandSpec(commandName, args, CommandResolutionStrategy.Path); Console.WriteLine("rooted?");
if (useComSpec)
{
return CreateComSpecCommandSpec(commandName, args, CommandResolutionStrategy.Path);
}
else
{
var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArray(args);
return new CommandSpec(commandName, escapedArgs, CommandResolutionStrategy.Path);
}
} }
return null; return null;
} }
public static CommandSpec ResolveFromProjectDependencies(string commandName, string args, public static CommandSpec ResolveFromProjectDependencies(string commandName, IEnumerable<string> args,
NuGetFramework framework) NuGetFramework framework, bool useComSpec = false)
{ {
if (framework == null) return null; if (framework == null) return null;
@ -64,7 +75,7 @@ namespace Microsoft.DotNet.Cli.Utils
var depsPath = GetDepsPath(projectContext, Constants.DefaultConfiguration); var depsPath = GetDepsPath(projectContext, Constants.DefaultConfiguration);
return ConfigureCommandFromPackage(commandName, args, commandPackage, projectContext, depsPath); return ConfigureCommandFromPackage(commandName, args, commandPackage, projectContext, depsPath, useComSpec);
} }
private static ProjectContext GetProjectContext(NuGetFramework framework) private static ProjectContext GetProjectContext(NuGetFramework framework)
@ -93,7 +104,7 @@ namespace Microsoft.DotNet.Cli.Utils
e == FileNameSuffixes.DotNet.DynamicLib)); e == FileNameSuffixes.DotNet.DynamicLib));
} }
public static CommandSpec ResolveFromProjectTools(string commandName, string args) public static CommandSpec ResolveFromProjectTools(string commandName, IEnumerable<string> args, bool useComSpec = false)
{ {
var context = GetProjectContext(FrameworkConstants.CommonFrameworks.DnxCore50); var context = GetProjectContext(FrameworkConstants.CommonFrameworks.DnxCore50);
@ -129,7 +140,7 @@ namespace Microsoft.DotNet.Cli.Utils
: null; : null;
} }
private static CommandSpec ConfigureCommandFromPackage(string commandName, string args, string packageDir) private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable<string> args, string packageDir)
{ {
var commandPackage = new PackageFolderReader(packageDir); var commandPackage = new PackageFolderReader(packageDir);
@ -138,8 +149,8 @@ namespace Microsoft.DotNet.Cli.Utils
return ConfigureCommandFromPackage(commandName, args, files, packageDir); return ConfigureCommandFromPackage(commandName, args, files, packageDir);
} }
private static CommandSpec ConfigureCommandFromPackage(string commandName, string args, private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable<string> args,
PackageDescription commandPackage, ProjectContext projectContext, string depsPath = null) PackageDescription commandPackage, ProjectContext projectContext, string depsPath = null, bool useComSpec = false)
{ {
var files = commandPackage.Library.Files; var files = commandPackage.Library.Files;
@ -149,11 +160,11 @@ namespace Microsoft.DotNet.Cli.Utils
var packageDir = Path.Combine(packageRoot, packagePath); var packageDir = Path.Combine(packageRoot, packagePath);
return ConfigureCommandFromPackage(commandName, args, files, packageDir, depsPath); return ConfigureCommandFromPackage(commandName, args, files, packageDir, depsPath, useComSpec);
} }
private static CommandSpec ConfigureCommandFromPackage(string commandName, string args, private static CommandSpec ConfigureCommandFromPackage(string commandName, IEnumerable<string> args,
IEnumerable<string> files, string packageDir, string depsPath = null) IEnumerable<string> files, string packageDir, string depsPath = null, bool useComSpec = false)
{ {
var fileName = string.Empty; var fileName = string.Empty;
@ -169,21 +180,35 @@ namespace Microsoft.DotNet.Cli.Utils
fileName = CoreHost.HostExePath; fileName = CoreHost.HostExePath;
var depsArg = string.Empty; var additionalArgs = new List<string>();
additionalArgs.Add(dllPath);
if (depsPath != null) if (depsPath != null)
{ {
depsArg = $"\"--depsfile:{depsPath}\" "; additionalArgs.Add("--depsfile");
additionalArgs.Add(depsPath);
} }
args = $"\"{dllPath}\" {depsArg}{args}"; args = additionalArgs.Concat(args);
} }
else else
{ {
fileName = Path.Combine(packageDir, commandPath); fileName = Path.Combine(packageDir, commandPath);
} }
return new CommandSpec(fileName, args, CommandResolutionStrategy.NugetPackage);
if (useComSpec)
{
return CreateComSpecCommandSpec(fileName, args, CommandResolutionStrategy.NugetPackage);
}
else
{
var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArray(args);
return new CommandSpec(fileName, escapedArgs, CommandResolutionStrategy.NugetPackage);
}
} }
private static string GetDepsPath(ProjectContext context, string buildConfiguration) private static string GetDepsPath(ProjectContext context, string buildConfiguration)
@ -192,23 +217,59 @@ namespace Microsoft.DotNet.Cli.Utils
context.ProjectFile.Name + FileNameSuffixes.Deps); context.ProjectFile.Name + FileNameSuffixes.Deps);
} }
private static CommandSpec CreateCommandSpecPreferringExe(string commandName, string args, string commandPath, private static CommandSpec CreateCommandSpecPreferringExe(
CommandResolutionStrategy resolutionStrategy) string commandName,
IEnumerable<string> args,
string commandPath,
CommandResolutionStrategy resolutionStrategy,
bool useComSpec = false)
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase)) Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase))
{ {
var preferredCommandPath = Env.GetCommandPath(commandName, ".exe"); var preferredCommandPath = Env.GetCommandPath(commandName, ".exe");
if (preferredCommandPath != null) // Use cmd if we can't find an exe
if (preferredCommandPath == null)
{ {
commandPath = Environment.GetEnvironmentVariable("ComSpec"); useComSpec = true;
}
args = $"/S /C \"\"{preferredCommandPath}\" {args}\""; else
{
commandPath = preferredCommandPath;
} }
} }
return new CommandSpec(commandPath, args, resolutionStrategy); if (useComSpec)
{
return CreateComSpecCommandSpec(commandPath, args, resolutionStrategy);
}
else
{
var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArray(args);
return new CommandSpec(commandPath, escapedArgs, resolutionStrategy);
}
}
private static CommandSpec CreateComSpecCommandSpec(
string command,
IEnumerable<string> args,
CommandResolutionStrategy resolutionStrategy)
{
// To prevent Command Not Found, comspec gets passed in as
// the command already in some cases
var comSpec = Environment.GetEnvironmentVariable("ComSpec");
if (command.Equals(comSpec, StringComparison.OrdinalIgnoreCase))
{
command = args.FirstOrDefault();
args = args.Skip(1);
}
var cmdEscapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForCmd(args);
var escapedArgString = $"/s /c \"\"{command}\" {cmdEscapedArgs}\"";
return new CommandSpec(comSpec, escapedArgString, resolutionStrategy);
} }
} }
} }

View file

@ -22,7 +22,7 @@ namespace Microsoft.DotNet.Cli.Utils
var scriptArguments = CommandGrammar.Process( var scriptArguments = CommandGrammar.Process(
scriptCommandLine, scriptCommandLine,
GetScriptVariable(project, getVariable), GetScriptVariable(project, getVariable),
preserveSurroundingQuotes: true); preserveSurroundingQuotes: false);
// Ensure the array won't be empty and the elements won't be null or empty strings. // Ensure the array won't be empty and the elements won't be null or empty strings.
scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray(); scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray();
@ -31,6 +31,8 @@ namespace Microsoft.DotNet.Cli.Utils
return null; return null;
} }
var useComSpec = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
// Only forward slashes are used in script blocks. Replace with backslashes to correctly // Only forward slashes are used in script blocks. Replace with backslashes to correctly
@ -45,24 +47,24 @@ namespace Microsoft.DotNet.Cli.Utils
var comSpec = Environment.GetEnvironmentVariable("ComSpec"); var comSpec = Environment.GetEnvironmentVariable("ComSpec");
if (!string.IsNullOrEmpty(comSpec)) if (!string.IsNullOrEmpty(comSpec))
{ {
scriptArguments = useComSpec=true;
new[] { comSpec, "/S", "/C", "\"" }
.Concat(scriptArguments) List<string> concatenatedArgs = new List<string>();
.Concat(new[] { "\"" }) concatenatedArgs.Add(comSpec);
.ToArray(); concatenatedArgs.AddRange(scriptArguments);
scriptArguments = concatenatedArgs.ToArray();
} }
} }
else else
{ {
// Special-case a script name that, perhaps with added .sh, matches an existing file. // Special-case a script name that, perhaps with added .sh, matches an existing file.
var surroundWithQuotes = false;
var scriptCandidate = scriptArguments[0]; var scriptCandidate = scriptArguments[0];
if (scriptCandidate.StartsWith("\"", StringComparison.Ordinal) && if (scriptCandidate.StartsWith("\"", StringComparison.Ordinal) &&
scriptCandidate.EndsWith("\"", StringComparison.Ordinal)) scriptCandidate.EndsWith("\"", StringComparison.Ordinal))
{ {
// Strip surrounding quotes; they were required in project.json to keep the script name // Strip surrounding quotes; they were required in project.json to keep the script name
// together but confuse File.Exists() e.g. "My Script", lacking ./ prefix and .sh suffix. // together but confuse File.Exists() e.g. "My Script", lacking ./ prefix and .sh suffix.
surroundWithQuotes = true;
scriptCandidate = scriptCandidate.Substring(1, scriptCandidate.Length - 2); scriptCandidate = scriptCandidate.Substring(1, scriptCandidate.Length - 2);
} }
@ -76,19 +78,25 @@ namespace Microsoft.DotNet.Cli.Utils
// scriptCandidate may be a path relative to the project root. If so, likely will not be found // scriptCandidate may be a path relative to the project root. If so, likely will not be found
// in the $PATH; add ./ to let bash know where to look. // in the $PATH; add ./ to let bash know where to look.
var prefix = Path.IsPathRooted(scriptCandidate) ? string.Empty : "./"; var prefix = Path.IsPathRooted(scriptCandidate) ? string.Empty : "./";
var quote = surroundWithQuotes ? "\"" : string.Empty; scriptArguments[0] = $"{prefix}{scriptCandidate}";
scriptArguments[0] = $"{ quote }{ prefix }{ scriptCandidate }{ quote }";
} }
// Always use /usr/bin/env bash -c in order to support redirection and so on; similar to Windows case. // Always use /usr/bin/env bash -c in order to support redirection and so on; similar to Windows case.
// Unlike Windows, must escape quotation marks within the newly-quoted string. // Unlike Windows, must escape quotation marks within the newly-quoted string.
scriptArguments = new[] { "/usr/bin/env", "bash", "-c", "\"" }
.Concat(scriptArguments.Select(argument => argument.Replace("\"", "\\\""))) // TODO change this back to original, not doing anything special for bash
.Concat(new[] { "\"" }) var bashArgs = ArgumentEscaper.EscapeArgArrayForBash(scriptArguments);
.ToArray();
List<string> concatenatedArgs = new List<string>();
concatenatedArgs.Add("/usr/bin/env");
concatenatedArgs.Add("bash");
concatenatedArgs.Add("-c");
concatenatedArgs.AddRange(bashArgs);
scriptArguments = concatenatedArgs.ToArray();
} }
return Command.Create(scriptArguments.FirstOrDefault(), string.Join(" ", scriptArguments.Skip(1))) return Command.Create(scriptArguments.FirstOrDefault(), scriptArguments.Skip(1), useComSpec: useComSpec)
.WorkingDirectory(project.ProjectDirectory); .WorkingDirectory(project.ProjectDirectory);
} }
private static Func<string, string> WrapVariableDictionary(IDictionary<string, string> contextVariables) private static Func<string, string> WrapVariableDictionary(IDictionary<string, string> contextVariables)