From 077dcebd433d45f8b0b828ffafd161db14d24aaf Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 14 Mar 2017 16:40:19 -0700 Subject: [PATCH] Add ArgumentEscaper and DotNetMuxer to CommandLineUtils These APIs are commonly used in other ASP.NET Core tools. This change puts the API into this shared library for use across all repos. --- .../Utilities/ArgumentEscaper.cs | 109 ++++++++++++++++++ .../Utilities/DotNetMuxer.cs | 71 ++++++++++++ .../ArgumentEscaperTests.cs | 22 ++++ .../DotNetMuxerTests.cs | 21 ++++ 4 files changed, 223 insertions(+) create mode 100644 shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/ArgumentEscaper.cs create mode 100644 shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/DotNetMuxer.cs create mode 100644 test/Microsoft.Extensions.CommandLineUtils.Tests/ArgumentEscaperTests.cs create mode 100644 test/Microsoft.Extensions.CommandLineUtils.Tests/DotNetMuxerTests.cs diff --git a/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/ArgumentEscaper.cs b/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/ArgumentEscaper.cs new file mode 100644 index 00000000000..7b696c5175d --- /dev/null +++ b/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/ArgumentEscaper.cs @@ -0,0 +1,109 @@ +// 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.Linq; +using System.Text; + +namespace Microsoft.Extensions.CommandLineUtils +{ + /// + /// A utility for escaping arguments for new processes. + /// + internal static class ArgumentEscaper + { + /// + /// Undo the processing which took place to create string[] args in Main, so that the next process will + /// receive the same string[] args. + /// + /// + /// See https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + /// + /// + /// + public static string EscapeAndConcatenate(IEnumerable args) + => string.Join(" ", args.Select(EscapeSingleArg)); + + private static string EscapeSingleArg(string arg) + { + var sb = new StringBuilder(); + + var needsQuotes = ShouldSurroundWithQuotes(arg); + var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg); + + if (needsQuotes) + { + sb.Append('"'); + } + + for (int i = 0; i < arg.Length; ++i) + { + var backslashes = 0; + + // Consume all backslashes + while (i < arg.Length && arg[i] == '\\') + { + backslashes++; + i++; + } + + if (i == arg.Length && isQuoted) + { + // Escape any backslashes at the end of the arg when the argument is also quoted. + // This ensures the outside quote is interpreted as an argument delimiter + sb.Append('\\', 2 * backslashes); + } + else if (i == arg.Length) + { + // At then end of the arg, which isn't quoted, + // just add the backslashes, no need to escape + sb.Append('\\', backslashes); + } + else if (arg[i] == '"') + { + // Escape any preceding backslashes and the quote + sb.Append('\\', (2 * backslashes) + 1); + sb.Append('"'); + } + else + { + // Output any consumed backslashes and the character + sb.Append('\\', backslashes); + sb.Append(arg[i]); + } + } + + if (needsQuotes) + { + sb.Append('"'); + } + + return sb.ToString(); + } + + private static bool ShouldSurroundWithQuotes(string argument) + { + // Don't quote already quoted strings + if (IsSurroundedWithQuotes(argument)) + { + return false; + } + + // Only quote if whitespace exists in the string + return ContainsWhitespace(argument); + } + + private static bool IsSurroundedWithQuotes(string argument) + { + if (argument.Length <= 1) + { + return false; + } + + return argument[0] == '"' && argument[argument.Length - 1] == '"'; + } + + private static bool ContainsWhitespace(string argument) + => argument.IndexOfAny(new [] { ' ', '\t', '\n' }) >= 0; + } +} diff --git a/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/DotNetMuxer.cs b/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/DotNetMuxer.cs new file mode 100644 index 00000000000..ffa6ae26961 --- /dev/null +++ b/shared/Microsoft.Extensions.CommandLineUtils.Sources/Utilities/DotNetMuxer.cs @@ -0,0 +1,71 @@ +// 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. + +// System.AppContext.GetData is not available in these frameworks +#if !NET451 && !NET452 && !NET46 && !NET461 + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.Extensions.CommandLineUtils +{ + /// + /// Utilities for finding the "dotnet.exe" file from the currently running .NET Core application + /// + internal static class DotNetMuxer + { + private const string MuxerName = "dotnet"; + + static DotNetMuxer() + { + MuxerPath = TryFindMuxerPath(); + } + + /// + /// The full filepath to the .NET Core muxer. + /// + public static string MuxerPath { get; } + + /// + /// Finds the full filepath to the .NET Core muxer, + /// or returns a string containing the default name of the .NET Core muxer ('dotnet'). + /// + /// The path or a string named 'dotnet' + public static string MuxerPathOrDefault() + => MuxerPath ?? MuxerName; + + private static string TryFindMuxerPath() + { + var fileName = MuxerName; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileName += ".exe"; + } + + var fxDepsFile = AppContext.GetData("FX_DEPS_FILE") as string; + + if (string.IsNullOrEmpty(fxDepsFile)) + { + return null; + } + + var muxerDir = new FileInfo(fxDepsFile) // Microsoft.NETCore.App.deps.json + .Directory? // (version) + .Parent? // Microsoft.NETCore.App + .Parent? // shared + .Parent; // DOTNET_HOME + + if (muxerDir == null) + { + return null; + } + + var muxer = Path.Combine(muxerDir.FullName, fileName); + return File.Exists(muxer) + ? muxer + : null; + } + } +} +#endif diff --git a/test/Microsoft.Extensions.CommandLineUtils.Tests/ArgumentEscaperTests.cs b/test/Microsoft.Extensions.CommandLineUtils.Tests/ArgumentEscaperTests.cs new file mode 100644 index 00000000000..9743877b444 --- /dev/null +++ b/test/Microsoft.Extensions.CommandLineUtils.Tests/ArgumentEscaperTests.cs @@ -0,0 +1,22 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.CommandLineUtils +{ + public class ArgumentEscaperTests + { + [Theory] + [InlineData(new[] { "one", "two", "three" }, "one two three")] + [InlineData(new[] { "line1\nline2", "word1\tword2" }, "\"line1\nline2\" \"word1\tword2\"")] + [InlineData(new[] { "with spaces" }, "\"with spaces\"")] + [InlineData(new[] { @"with\backslash" }, @"with\backslash")] + [InlineData(new[] { @"""quotedwith\backslash""" }, @"\""quotedwith\backslash\""")] + [InlineData(new[] { @"C:\Users\" }, @"C:\Users\")] + [InlineData(new[] { @"C:\Program Files\dotnet\" }, @"""C:\Program Files\dotnet\\""")] + [InlineData(new[] { @"backslash\""preceedingquote" }, @"backslash\\\""preceedingquote")] + public void EscapesArguments(string[] args, string expected) + => Assert.Equal(expected, ArgumentEscaper.EscapeAndConcatenate(args)); + } +} diff --git a/test/Microsoft.Extensions.CommandLineUtils.Tests/DotNetMuxerTests.cs b/test/Microsoft.Extensions.CommandLineUtils.Tests/DotNetMuxerTests.cs new file mode 100644 index 00000000000..f3b6b78e442 --- /dev/null +++ b/test/Microsoft.Extensions.CommandLineUtils.Tests/DotNetMuxerTests.cs @@ -0,0 +1,21 @@ +// 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. + +#if !NET452 +using System.IO; +using Xunit; + +namespace Microsoft.Extensions.CommandLineUtils +{ + public class DotNetMuxerTests + { + [Fact] + public void FindsTheMuxer() + { + var muxerPath = DotNetMuxer.MuxerPath; + Assert.NotNull(muxerPath); + Assert.True(File.Exists(muxerPath), "The file did not exist"); + } + } +} +#endif