Add support for '--' argument separator

Also refactors command line parsing into a separate class.
This commit is contained in:
Nate McMaster 2016-09-06 15:26:21 -07:00
parent 11bbd6df8e
commit f90594a647
No known key found for this signature in database
GPG Key ID: BD729980AA6A21BD
12 changed files with 211 additions and 62 deletions

3
.gitignore vendored
View File

@ -29,4 +29,5 @@ project.lock.json
.build/
/.vs/
testWorkDir/
*.nuget.props
*.nuget.targets

View File

@ -1,4 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
@ -33,6 +33,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Secret
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.SecretManager.Tools.Tests", "test\Microsoft.Extensions.SecretManager.Tools.Tests\Microsoft.Extensions.SecretManager.Tools.Tests.xproj", "{7B331122-83B1-4F08-A119-DC846959844C}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Watcher.Tools.Tests", "test\Microsoft.DotNet.Watcher.Tools.Tests\Microsoft.DotNet.Watcher.Tools.Tests.xproj", "{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.Caching.SqlConfig.Tools", "src\Microsoft.Extensions.Caching.SqlConfig.Tools\Microsoft.Extensions.Caching.SqlConfig.Tools.xproj", "{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}"
EndProject
Global
@ -81,6 +83,10 @@ Global
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Release|Any CPU
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -97,5 +103,6 @@ Global
{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E} = {66517987-2A5A-4330-B130-207039378FD4}
{7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
{53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4}
{8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection
EndGlobal

View File

@ -2,9 +2,11 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Watcher.Core.Internal;
using Microsoft.Extensions.Logging;
@ -33,7 +35,7 @@ namespace Microsoft.DotNet.Watcher.Core
_logger = _loggerFactory.CreateLogger(nameof(DotNetWatcher));
}
public async Task WatchAsync(string projectFile, string[] dotnetArguments, CancellationToken cancellationToken)
public async Task WatchAsync(string projectFile, IEnumerable<string> dotnetArguments, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(projectFile))
{
@ -48,24 +50,7 @@ namespace Microsoft.DotNet.Watcher.Core
throw new ArgumentNullException(nameof(cancellationToken));
}
// If any argument has spaces then quote it because we're going to convert everything
// to string
for (var i = 0; i < dotnetArguments.Length; i++)
{
var arg = dotnetArguments[i];
foreach (char c in arg)
{
if (c == ' ' ||
c == '\t')
{
arg = $"\"{arg}\"";
break;
}
}
dotnetArguments[i] = arg;
}
var dotnetArgumentsAsString = string.Join(" ", dotnetArguments);
var dotnetArgumentsAsString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(dotnetArguments);
var workingDir = Path.GetDirectoryName(projectFile);

View File

@ -17,6 +17,7 @@
},
"dependencies": {
"Microsoft.DotNet.ProjectModel": "1.0.0-*",
"Microsoft.DotNet.Cli.Utils": "1.0.0-*",
"Microsoft.Extensions.FileProviders.Abstractions": "1.1.0-*",
"Microsoft.Extensions.FileProviders.Physical": "1.1.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.1.0-*",

View File

@ -0,0 +1,54 @@
// 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 Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.DotNet.Watcher.Tools
{
internal class CommandLineOptions
{
public bool IsHelp { get; private set; }
public IList<string> RemainingArguments { get; private set; }
public static CommandLineOptions Parse(string[] args, TextWriter consoleOutput)
{
if (args == null)
{
throw new ArgumentNullException(nameof(args));
}
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{
Name = "dotnet watch",
FullName = "Microsoft DotNet File Watcher",
Out = consoleOutput,
AllowArgumentSeparator = true
};
app.HelpOption("-?|-h|--help");
app.OnExecute(() =>
{
if (app.RemainingArguments.Count == 0)
{
app.ShowHelp();
}
return 0;
});
if (app.Execute(args) != 0)
{
return null;
}
return new CommandLineOptions
{
RemainingArguments = app.RemainingArguments,
IsHelp = app.IsShowingInformation
};
}
}
}

View File

@ -6,7 +6,6 @@ using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.Watcher.Core;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging;
namespace Microsoft.DotNet.Watcher.Tools
@ -14,9 +13,24 @@ namespace Microsoft.DotNet.Watcher.Tools
public class Program
{
private readonly ILoggerFactory _loggerFactory;
private readonly CancellationToken _cancellationToken;
private readonly TextWriter _out;
public Program()
public Program(TextWriter consoleOutput, CancellationToken cancellationToken)
{
if (consoleOutput == null)
{
throw new ArgumentNullException(nameof(consoleOutput));
}
if (cancellationToken == null)
{
throw new ArgumentNullException(nameof(cancellationToken));
}
_cancellationToken = cancellationToken;
_out = consoleOutput;
_loggerFactory = new LoggerFactory();
var logVar = Environment.GetEnvironmentVariable("DOTNET_WATCH_LOG_LEVEL");
@ -44,49 +58,44 @@ namespace Microsoft.DotNet.Watcher.Tools
ev.Cancel = false;
};
return new Program().MainInternal(args, ctrlCTokenSource.Token);
int exitCode;
try
{
exitCode = new Program(Console.Out, ctrlCTokenSource.Token)
.MainInternalAsync(args)
.GetAwaiter()
.GetResult();
}
catch (TaskCanceledException)
{
// swallow when only exception is the CTRL+C exit cancellation task
exitCode = 0;
}
return exitCode;
}
}
private int MainInternal(string[] args, CancellationToken cancellationToken)
private async Task<int> MainInternalAsync(string[] args)
{
var app = new CommandLineApplication();
app.Name = "dotnet-watch";
app.FullName = "Microsoft dotnet File Watcher";
app.HelpOption("-?|-h|--help");
app.OnExecute(() =>
var options = CommandLineOptions.Parse(args, _out);
if (options == null)
{
var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName);
var watcher = DotNetWatcher.CreateDefault(_loggerFactory);
try
{
watcher.WatchAsync(projectToWatch, args, cancellationToken).Wait();
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.Count != 1 || !(ex.InnerException is TaskCanceledException))
{
throw;
}
}
return 0;
});
if (args == null ||
args.Length == 0 ||
args[0].Equals("--help", StringComparison.OrdinalIgnoreCase) ||
args[0].Equals("-h", StringComparison.OrdinalIgnoreCase) ||
args[0].Equals("-?", StringComparison.OrdinalIgnoreCase))
{
app.ShowHelp();
// invalid args syntax
return 1;
}
return app.Execute();
if (options.IsHelp)
{
return 2;
}
var projectToWatch = Path.Combine(Directory.GetCurrentDirectory(), ProjectModel.Project.FileName);
await DotNetWatcher
.CreateDefault(_loggerFactory)
.WatchAsync(projectToWatch, options.RemainingArguments, _cancellationToken);
return 0;
}
}
}

View File

@ -3,12 +3,9 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
[assembly: AssemblyMetadata("Serviceable", "True")]
[assembly: NeutralResourcesLanguage("en-US")]
[assembly: AssemblyCompany("Microsoft Corporation.")]
[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
[assembly: AssemblyProduct("Microsoft .NET")]
[assembly: InternalsVisibleTo("Microsoft.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -0,0 +1,6 @@
// 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.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.DotNet.Watcher.Tools.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

View File

@ -18,7 +18,9 @@ Add `Microsoft.DotNet.Watcher.Tools` to the `tools` section of your `project.jso
### How To Use
dotnet watch [dotnet arguments]
dotnet watch [-?|-h|--help]
dotnet watch [[--] <dotnet arguments>...]
Add `watch` after `dotnet` in the command that you want to run:

View File

@ -0,0 +1,46 @@
// 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.IO;
using System.Linq;
using System.Text;
using Xunit;
namespace Microsoft.DotNet.Watcher.Tools.FunctionalTests
{
public class CommandLineOptionsTests
{
[Theory]
[InlineData(new object[] { new[] { "-h" } })]
[InlineData(new object[] { new[] { "-?" } })]
[InlineData(new object[] { new[] { "--help" } })]
[InlineData(new object[] { new[] { "--help", "--bogus" } })]
[InlineData(new object[] { new[] { "--" } })]
[InlineData(new object[] { new string[0] })]
public void HelpArgs(string[] args)
{
var stdout = new StringBuilder();
var options = CommandLineOptions.Parse(args, new StringWriter(stdout));
Assert.True(options.IsHelp);
Assert.Contains("Usage: dotnet watch ", stdout.ToString());
}
[Theory]
[InlineData(new[] { "run" }, new[] { "run" })]
[InlineData(new[] { "run", "--", "subarg" }, new[] { "run", "--", "subarg" })]
[InlineData(new[] { "--", "run", "--", "subarg" }, new[] { "run", "--", "subarg" })]
[InlineData(new[] { "--unrecognized-arg" }, new[] { "--unrecognized-arg" })]
public void ParsesRemainingArgs(string[] args, string[] expected)
{
var stdout = new StringBuilder();
var options = CommandLineOptions.Parse(args, new StringWriter(stdout));
Assert.Equal(expected, options.RemainingArguments.ToArray());
Assert.False(options.IsHelp);
Assert.Empty(stdout.ToString());
}
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0.25420" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0.25420</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>8a2e6961-6b12-4a8e-8215-3e7301d52eac</ProjectGuid>
<RootNamespace>Microsoft.DotNet.Watcher.Tools.Tests</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,22 @@
{
"buildOptions": {
"warningsAsErrors": true,
"keyFile": "../../tools/Key.snk"
},
"dependencies": {
"Microsoft.DotNet.Watcher.Tools": "1.0.0-*",
"dotnet-test-xunit": "2.2.0-*",
"xunit": "2.2.0-*"
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.0",
"type": "platform"
}
}
}
},
"testRunner": "xunit"
}