From 21951d62f3c923d4dcc7934ca78154660de1a26c Mon Sep 17 00:00:00 2001 From: Victor Hurdugaci Date: Fri, 22 Apr 2016 10:38:24 -0700 Subject: [PATCH] Fix command line parsing around the -- separator --- Microsoft.DotNet.Watcher.Tools.sln | 9 +- .../DotNetWatcher.cs | 34 +++---- src/Microsoft.DotNet.Watcher.Tools/Program.cs | 71 ++++++++++---- .../Properties/AssemblyInfo.cs | 4 +- .../ArgumentSeparatorTests.cs | 95 +++++++++++++++++++ ...Microsoft.DotNet.Watcher.Tools.Tests.xproj | 19 ++++ .../project.json | 27 ++++++ 7 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentSeparatorTests.cs create mode 100644 test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj create mode 100644 test/Microsoft.DotNet.Watcher.Tools.Tests/project.json diff --git a/Microsoft.DotNet.Watcher.Tools.sln b/Microsoft.DotNet.Watcher.Tools.sln index 05d379e36b..08e32fa200 100644 --- a/Microsoft.DotNet.Watcher.Tools.sln +++ b/Microsoft.DotNet.Watcher.Tools.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.23107.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}" EndProject @@ -29,6 +29,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AppWithDeps", "test\TestApp EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Dependency", "test\TestApps\Dependency\Dependency.xproj", "{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Watcher.Tools.Tests", "test\Microsoft.DotNet.Watcher.Tools.Tests\Microsoft.DotNet.Watcher.Tools.Tests.xproj", "{2E2FE108-0EB7-48CE-BD52-147E90180090}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,10 @@ Global {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.Build.0 = Release|Any CPU + {2E2FE108-0EB7-48CE-BD52-147E90180090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E2FE108-0EB7-48CE-BD52-147E90180090}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E2FE108-0EB7-48CE-BD52-147E90180090}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E2FE108-0EB7-48CE-BD52-147E90180090}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -76,5 +82,6 @@ Global {2AB1A28B-2022-49EA-AF77-AC8A875915CC} = {2876B12E-5841-4792-85A8-2929AEE11885} {F7734E61-F510-41E0-AD15-301A64081CD1} = {2876B12E-5841-4792-85A8-2929AEE11885} {2F48041A-F7D1-478F-9C38-D41F0F05E8CA} = {2876B12E-5841-4792-85A8-2929AEE11885} + {2E2FE108-0EB7-48CE-BD52-147E90180090} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} EndGlobalSection EndGlobal diff --git a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs index 9ef05d2eb0..61b1cc586b 100644 --- a/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs +++ b/src/Microsoft.DotNet.Watcher.Core/DotNetWatcher.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.Watcher.Core.Internal; @@ -58,29 +57,24 @@ namespace Microsoft.DotNet.Watcher.Core throw new ArgumentNullException(nameof(cancellationToken)); } - if (dotnetArguments.Length > 0) - { - dotnetArguments = new string[] { command, "--" } - .Concat(dotnetArguments) - .ToArray(); - } - else - { - dotnetArguments = new string[] { command }; - } + var fullDotnetArgs = new string[dotnetArguments.Length + 1]; + fullDotnetArgs[0] = command; - dotnetArguments = dotnetArguments - .Select(arg => + for (var i = 0; i < dotnetArguments.Length; i++) + { + var arg = dotnetArguments[i]; + foreach (char c in arg) { - // If the argument has spaces, make sure we quote it - if (arg.Contains(" ") || arg.Contains("\t")) + if (c == ' ' || + c == '\t') { - return $"\"{arg}\""; + arg = $"\"{arg}\""; + break; } - - return arg; - }) - .ToArray(); + } + fullDotnetArgs[i + 1] = arg; + } + dotnetArguments = fullDotnetArgs; var dotnetArgumentsAsString = string.Join(" ", dotnetArguments); diff --git a/src/Microsoft.DotNet.Watcher.Tools/Program.cs b/src/Microsoft.DotNet.Watcher.Tools/Program.cs index 96fdb8404a..776cf0cd2a 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Program.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Program.cs @@ -3,13 +3,12 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.CommandLineUtils; -using Microsoft.Extensions.PlatformAbstractions; using Microsoft.DotNet.Watcher.Core; +using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.DotNet.Watcher.Tools { @@ -50,31 +49,46 @@ namespace Microsoft.DotNet.Watcher.Tools // To pass "--help" to the app being watched, you must use "--": dotnet watch -- --help internal static void SeparateWatchArguments(string[] args, out string[] watchArgs, out string[] appArgs) { + if (args.Length == 0) + { + watchArgs = new string[0]; + appArgs = new string[0]; + return; + } + // Special case "--help" - if (args.Length > 0 && ( - args[0].Equals("--help", StringComparison.OrdinalIgnoreCase) || + if (args[0].Equals("--help", StringComparison.OrdinalIgnoreCase) || args[0].Equals("-h", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-?", StringComparison.OrdinalIgnoreCase))) + args[0].Equals("-?", StringComparison.OrdinalIgnoreCase)) { watchArgs = new string[] { args[0] }; appArgs = new string[0]; return; } - int argsIndex = -1; - watchArgs = args.TakeWhile((arg, idx) => + var separatorIndex = -1; + for (var i = 0; i < args.Length; i++) { - argsIndex = idx; - return !string.Equals(arg, AppArgumentSeparator, StringComparison.OrdinalIgnoreCase); - }).ToArray(); + if (string.Equals(args[i], AppArgumentSeparator, StringComparison.OrdinalIgnoreCase)) + { + separatorIndex = i; + break; + } + } - appArgs = args.Skip(argsIndex + 1).ToArray(); - - if (appArgs.Length == 0) + if (separatorIndex == -1) { - // If no explicit watcher arguments then all arguments get passed to the app being watched - appArgs = watchArgs; watchArgs = new string[0]; + appArgs = args; + } + else + { + watchArgs = new string[separatorIndex]; + Array.Copy(args, 0, watchArgs, 0, separatorIndex); + + var appArgsLength = args.Length - separatorIndex - 1; + appArgs = new string[appArgsLength]; + Array.Copy(args, separatorIndex + 1, appArgs, 0, appArgsLength); } } @@ -101,19 +115,34 @@ namespace Microsoft.DotNet.Watcher.Tools "Optional. The watcher will exit when a file change is detected instead of restarting the process. Default: not set.", CommandOptionType.NoValue); - app.OnExecute(() => { var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName); + var command = commandArg.Value(); + if (!commandArg.HasValue()) + { + // The default command is "run". In this case we always assume the arguments are passed to the application being run. + // If you want a different behaviour for run you need to use --command and pass the full arguments + // Run is special because it requires a "--" before the arguments being passed to the application, + // so the two command below are equivalent and resolve to "dotnet run -- --foo": + // 1. dotnet watch --foo + // 2. dotnet watch --command run -- -- --foo (yes, there are two "--") + if (appArgs.Length > 0) + { + var newAppArgs = new string[appArgs.Length + 1]; + newAppArgs[0] = AppArgumentSeparator; + appArgs.CopyTo(newAppArgs, 1); + appArgs = newAppArgs; + } + + command = "run"; + } + var workingDir = workingDirArg.HasValue() ? workingDirArg.Value() : Path.GetDirectoryName(projectToWatch); - var command = commandArg.HasValue() ? - commandArg.Value() : - "run"; - var watcher = DotNetWatcher.CreateDefault(_loggerFactory); watcher.ExitOnChange = exitOnChangeArg.HasValue(); diff --git a/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs index e29eda63c2..35b4b58d45 100644 --- a/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs +++ b/src/Microsoft.DotNet.Watcher.Tools/Properties/AssemblyInfo.cs @@ -9,4 +9,6 @@ using System.Runtime.CompilerServices; [assembly: NeutralResourcesLanguage("en-US")] [assembly: AssemblyCompany("Microsoft Corporation.")] [assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] -[assembly: AssemblyProduct("Microsoft .NET")] \ No newline at end of file +[assembly: AssemblyProduct("Microsoft .NET")] + +[assembly: InternalsVisibleTo("Microsoft.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentSeparatorTests.cs b/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentSeparatorTests.cs new file mode 100644 index 0000000000..a9178eb1ad --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/ArgumentSeparatorTests.cs @@ -0,0 +1,95 @@ +// 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.DotNet.Watcher.Tools.Tests +{ + public class ArgumentSeparatorTests + { + [Theory] + [InlineData(new string[0], + new string[0], + new string[0])] + [InlineData(new string[] { "--" }, + new string[0], + new string[0])] + [InlineData(new string[] { "--appArg1" }, + new string[0], + new string[] { "--appArg1" })] + [InlineData(new string[] { "--command", "test" }, + new string[0], + new string[] { "--command", "test" })] + [InlineData(new string[] { "--command", "test", "--" }, + new string[] { "--command", "test" }, + new string[0])] + [InlineData(new string[] { "--command", "test", "--", "--appArg1", "arg1Value" }, + new string[] { "--command", "test" }, + new string[] { "--appArg1", "arg1Value" })] + [InlineData(new string[] { "--", "--appArg1", "arg1Value" }, + new string[0], + new string[] { "--appArg1", "arg1Value" })] + [InlineData(new string[] { "--", "--" }, + new string[0], + new string[] { "--" })] + [InlineData(new string[] { "--", "--", "--" }, + new string[0], + new string[] { "--", "--" })] + [InlineData(new string[] { "--command", "run", "--", "--", "--appArg", "foo" }, + new string[] { "--command", "run" }, + new string[] { "--", "--appArg", "foo" })] + [InlineData(new string[] { "--command", "run", "--", "-f", "net451", "--", "--appArg", "foo" }, + new string[] { "--command", "run" }, + new string[] { "-f", "net451", "--", "--appArg", "foo" })] + public void SeparateWatchArguments(string[] args, string[] expectedWatchArgs, string[] expectedAppArgs) + { + SeparateWatchArgumentsTest(args, expectedWatchArgs, expectedAppArgs); + } + + [Theory] + // Help is special if it's the first argument + [InlineData(new string[] { "--help" }, + new string[] { "--help" }, + new string[0])] + [InlineData(new string[] { "-h" }, + new string[] { "-h" }, + new string[0])] + [InlineData(new string[] { "-?" }, + new string[] { "-?" }, + new string[0])] + [InlineData(new string[] { "--help", "--this-is-ignored" }, + new string[] { "--help" }, + new string[0])] + [InlineData(new string[] { "--help", "--", "--this-is-ignored" }, + new string[] { "--help" }, + new string[0])] + // But not otherwise + [InlineData(new string[] { "--", "--help" }, + new string[0], + new string[] { "--help" })] + [InlineData(new string[] { "--foo", "--help" }, + new string[0], + new string[] { "--foo", "--help" })] + [InlineData(new string[] { "--foo", "--help" }, + new string[0], + new string[] { "--foo", "--help" })] + [InlineData(new string[] { "--foo", "--", "--help" }, + new string[] { "--foo" }, + new string[] { "--help" })] + public void SeparateWatchArguments_Help(string[] args, string[] expectedWatchArgs, string[] expectedAppArgs) + { + SeparateWatchArgumentsTest(args, expectedWatchArgs, expectedAppArgs); + } + + private static void SeparateWatchArgumentsTest(string[] args, string[] expectedWatchArgs, string[] expectedAppArgs) + { + string[] actualWatchArgs; + string[] actualAppArgs; + + Program.SeparateWatchArguments(args, out actualWatchArgs, out actualAppArgs); + + Assert.Equal(expectedWatchArgs, actualWatchArgs); + Assert.Equal(expectedAppArgs, actualAppArgs); + } + } +} diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj new file mode 100644 index 0000000000..2c2d6b6b34 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/Microsoft.DotNet.Watcher.Tools.Tests.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25123 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 2e2fe108-0eb7-48ce-bd52-147e90180090 + Microsoft.DotNet.Watcher.Tools.Tests + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json new file mode 100644 index 0000000000..4ce4fce759 --- /dev/null +++ b/test/Microsoft.DotNet.Watcher.Tools.Tests/project.json @@ -0,0 +1,27 @@ +{ + "compilationOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "dependencies": { + "dotnet-test-xunit": "1.0.0-*", + "Microsoft.DotNet.Watcher.Tools": "1.0.0-*", + "NETStandard.Library": "1.5.0-*", + "xunit": "2.1.0" + }, + "frameworks": { + "netcoreapp1.0": { + "imports": [ + "portable-net451+win8", + "dnxcore50" + ], + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.0.0-*" + } + } + } + }, + "testRunner": "xunit" +} \ No newline at end of file