Use Microsoft.Extensions.CommandLineUtils and Microsoft.Extensions.Tools.Internal in service ref programs

- add '--quiet' option
- add '--debug' option (when building Debug configuration)
- support `--` in `dotnet-getdocument`; users may want to explictly add options for GetDocument.Insider
- remove '--no-color' option
- use `IReporter` extension methods instead of static `Reporter` class
- reduce `static` use to ease testing (coming soon) and share the `IConsole` and `IReporter`
  - add `ProgramBase`
- allow mix of known and unknown command-line arguments
  - maintain existing behaviour using switch added in aspnet/Extensions#2210

nits:
- add a couple more `CommandLineApplicationExtensions` methods
- take VS suggestions
This commit is contained in:
Doug Bunting 2019-08-16 16:57:52 -07:00
parent 6d311041f0
commit e89a0519b9
31 changed files with 336 additions and 1297 deletions

View File

@ -1,12 +1,13 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project> <Project>
<!-- <!--
Settings users may update as they see fit. Settings users may update as they see fit.
--> -->
<PropertyGroup> <PropertyGroup>
<!-- <!--
Options added to the OpenAPI document generation tool ('dotnet-getdocument') command line. Available options Options added to the OpenAPI document generation tool ('dotnet-getdocument') command line. For example, the
control console output: 'no-color', 'prefix-output' and 'verbose'. All require a double-dash prefix. 'prefix-output', 'quiet' and 'verbose' options control console output and the 'help' and 'version' options show
information. Long form options require a double-dash prefix.
--> -->
<OpenApiGenerateDocumentsOptions Condition=" '$(OpenApiGenerateDocumentsOptions)' == '' " /> <OpenApiGenerateDocumentsOptions Condition=" '$(OpenApiGenerateDocumentsOptions)' == '' " />

View File

@ -1,15 +0,0 @@
// 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;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal class AnsiConsole
{
public static readonly AnsiTextWriter _out = new AnsiTextWriter(Console.Out);
public static void WriteLine(string text)
=> _out.WriteLine(text);
}
}

View File

@ -1,20 +0,0 @@
// 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.
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal static class AnsiConstants
{
public const string Reset = "\x1b[22m\x1b[39m";
public const string Bold = "\x1b[1m";
public const string Dark = "\x1b[22m";
public const string Black = "\x1b[30m";
public const string Red = "\x1b[31m";
public const string Green = "\x1b[32m";
public const string Yellow = "\x1b[33m";
public const string Blue = "\x1b[34m";
public const string Magenta = "\x1b[35m";
public const string Cyan = "\x1b[36m";
public const string Gray = "\x1b[37m";
}
}

View File

@ -1,131 +0,0 @@
// 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.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal class AnsiTextWriter
{
private readonly TextWriter _writer;
public AnsiTextWriter(TextWriter writer) => _writer = writer;
public void WriteLine(string text)
{
Interpret(text);
_writer.Write(Environment.NewLine);
}
private void Interpret(string value)
{
var matches = Regex.Matches(value, "\x1b\\[([0-9]+)?m");
var start = 0;
foreach (Match match in matches)
{
var length = match.Index - start;
if (length != 0)
{
_writer.Write(value.Substring(start, length));
}
Apply(match.Groups[1].Value);
start = match.Index + match.Length;
}
if (start != value.Length)
{
_writer.Write(value.Substring(start));
}
}
private static void Apply(string parameter)
{
switch (parameter)
{
case "1":
ApplyBold();
break;
case "22":
ResetBold();
break;
case "30":
ApplyColor(ConsoleColor.Black);
break;
case "31":
ApplyColor(ConsoleColor.DarkRed);
break;
case "32":
ApplyColor(ConsoleColor.DarkGreen);
break;
case "33":
ApplyColor(ConsoleColor.DarkYellow);
break;
case "34":
ApplyColor(ConsoleColor.DarkBlue);
break;
case "35":
ApplyColor(ConsoleColor.DarkMagenta);
break;
case "36":
ApplyColor(ConsoleColor.DarkCyan);
break;
case "37":
ApplyColor(ConsoleColor.Gray);
break;
case "39":
ResetColor();
break;
default:
Debug.Fail("Unsupported parameter: " + parameter);
break;
}
}
private static void ApplyBold()
=> Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor | 8);
private static void ResetBold()
=> Console.ForegroundColor = (ConsoleColor)((int)Console.ForegroundColor & 7);
private static void ApplyColor(ConsoleColor color)
{
var wasBold = ((int)Console.ForegroundColor & 8) != 0;
Console.ForegroundColor = color;
if (wasBold)
{
ApplyBold();
}
}
private static void ResetColor()
{
var wasBold = ((int)Console.ForegroundColor & 8) != 0;
Console.ResetColor();
if (wasBold)
{
ApplyBold();
}
}
}
}

View File

@ -1,19 +0,0 @@
// 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;
namespace Microsoft.DotNet.Cli.CommandLine
{
internal class CommandArgument
{
public CommandArgument() => Values = new List<string>();
public string Name { get; set; }
public string Description { get; set; }
public List<string> Values { get; private set; }
public bool MultipleValues { get; set; }
public string Value => Values.FirstOrDefault();
}
}

View File

@ -1,604 +0,0 @@
// 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.DotNet.Cli.CommandLine
{
internal class CommandLineApplication
{
private enum ParseOptionResult
{
Succeeded,
ShowHelp,
ShowVersion,
UnexpectedArgs,
}
// Indicates whether the parser should throw an exception when it runs into an unexpected argument.
// If this field is set to false, the parser will stop parsing when it sees an unexpected argument, and all
// remaining arguments, including the first unexpected argument, will be stored in RemainingArguments property.
private readonly bool _throwOnUnexpectedArg;
public CommandLineApplication(bool throwOnUnexpectedArg = true)
{
_throwOnUnexpectedArg = throwOnUnexpectedArg;
Options = new List<CommandOption>();
Arguments = new List<CommandArgument>();
Commands = new List<CommandLineApplication>();
RemainingArguments = new List<string>();
Invoke = () => 0;
}
public CommandLineApplication Parent { get; set; }
public string Name { get; set; }
public string FullName { get; set; }
public string Syntax { get; set; }
public string Description { get; set; }
public List<CommandOption> Options { get; private set; }
public CommandOption OptionHelp { get; private set; }
public CommandOption OptionVersion { get; private set; }
public List<CommandArgument> Arguments { get; private set; }
public List<string> RemainingArguments { get; private set; }
public bool IsShowingInformation { get; protected set; } // Is showing help or version?
public Func<int> Invoke { get; set; }
public Func<string> LongVersionGetter { get; set; }
public Func<string> ShortVersionGetter { get; set; }
public List<CommandLineApplication> Commands { get; private set; }
public bool HandleResponseFiles { get; set; }
public bool AllowArgumentSeparator { get; set; }
public bool HandleRemainingArguments { get; set; }
public string ArgumentSeparatorHelpText { get; set; }
public CommandLineApplication Command(string name, bool throwOnUnexpectedArg = true)
=> Command(name, _ => { }, throwOnUnexpectedArg);
public CommandLineApplication Command(string name, Action<CommandLineApplication> configuration,
bool throwOnUnexpectedArg = true)
{
var command = new CommandLineApplication(throwOnUnexpectedArg) { Name = name, Parent = this };
Commands.Add(command);
configuration(command);
return command;
}
public CommandOption Option(string template, string description, CommandOptionType optionType)
=> Option(template, description, optionType, _ => { });
public CommandOption Option(string template, string description, CommandOptionType optionType, Action<CommandOption> configuration)
{
var option = new CommandOption(template, optionType) { Description = description };
Options.Add(option);
configuration(option);
return option;
}
public CommandArgument Argument(string name, string description, bool multipleValues = false)
=> Argument(name, description, _ => { }, multipleValues);
public CommandArgument Argument(string name, string description, Action<CommandArgument> configuration, bool multipleValues = false)
{
var lastArg = Arguments.LastOrDefault();
if (lastArg != null && lastArg.MultipleValues)
{
var message = string.Format("The last argument '{0}' accepts multiple values. No more argument can be added.",
lastArg.Name);
throw new InvalidOperationException(message);
}
var argument = new CommandArgument { Name = name, Description = description, MultipleValues = multipleValues };
Arguments.Add(argument);
configuration(argument);
return argument;
}
public void OnExecute(Func<int> invoke) => Invoke = invoke;
public void OnExecute(Func<Task<int>> invoke) => Invoke = () => invoke().Result;
public int Execute(params string[] args)
{
var command = this;
IEnumerator<CommandArgument> arguments = null;
if (HandleResponseFiles)
{
args = ExpandResponseFiles(args).ToArray();
}
for (var index = 0; index < args.Length; index++)
{
var arg = args[index];
var isLongOption = arg.StartsWith("--");
if (isLongOption || arg.StartsWith("-"))
{
var result = ParseOption(isLongOption, command, args, ref index, out var option);
if (result == ParseOptionResult.ShowHelp)
{
command.ShowHelp();
return 0;
}
else if (result == ParseOptionResult.ShowVersion)
{
command.ShowVersion();
return 0;
}
}
else
{
var subcommand = ParseSubCommand(arg, command);
if (subcommand != null)
{
command = subcommand;
}
else
{
if (arguments == null)
{
arguments = new CommandArgumentEnumerator(command.Arguments.GetEnumerator());
}
if (arguments.MoveNext())
{
arguments.Current.Values.Add(arg);
}
else
{
HandleUnexpectedArg(command, args, index, argTypeName: "command or argument");
}
}
}
}
return command.Invoke();
}
private ParseOptionResult ParseOption(
bool isLongOption,
CommandLineApplication command,
string[] args,
ref int index,
out CommandOption option)
{
option = null;
var result = ParseOptionResult.Succeeded;
var arg = args[index];
var optionPrefixLength = isLongOption ? 2 : 1;
var optionComponents = arg.Substring(optionPrefixLength).Split(new[] { ':', '=' }, 2);
var optionName = optionComponents[0];
if (isLongOption)
{
option = command.Options.SingleOrDefault(
opt => string.Equals(opt.LongName, optionName, StringComparison.Ordinal));
}
else
{
option = command.Options.SingleOrDefault(
opt => string.Equals(opt.ShortName, optionName, StringComparison.Ordinal));
if (option == null)
{
option = command.Options.SingleOrDefault(
opt => string.Equals(opt.SymbolName, optionName, StringComparison.Ordinal));
}
}
if (option == null)
{
if (isLongOption && string.IsNullOrEmpty(optionName) &&
!command._throwOnUnexpectedArg && AllowArgumentSeparator)
{
// a stand-alone "--" is the argument separator, so skip it and
// handle the rest of the args as unexpected args
index++;
}
HandleUnexpectedArg(command, args, index, argTypeName: "option");
result = ParseOptionResult.UnexpectedArgs;
}
else if (command.OptionHelp == option)
{
result = ParseOptionResult.ShowHelp;
}
else if (command.OptionVersion == option)
{
result = ParseOptionResult.ShowVersion;
}
else
{
if (optionComponents.Length == 2)
{
if (!option.TryParse(optionComponents[1]))
{
command.ShowHint();
throw new CommandParsingException(command,
$"Unexpected value '{optionComponents[1]}' for option '{optionName}'");
}
}
else
{
if (option.OptionType == CommandOptionType.NoValue ||
option.OptionType == CommandOptionType.BoolValue)
{
// No value is needed for this option
option.TryParse(null);
}
else
{
index++;
arg = args[index];
if (!option.TryParse(arg))
{
command.ShowHint();
throw new CommandParsingException(command, $"Unexpected value '{arg}' for option '{optionName}'");
}
}
}
}
return result;
}
private static CommandLineApplication ParseSubCommand(string arg, CommandLineApplication command)
{
foreach (var subcommand in command.Commands)
{
if (string.Equals(subcommand.Name, arg, StringComparison.OrdinalIgnoreCase))
{
return subcommand;
}
}
return null;
}
// Helper method that adds a help option
public CommandOption HelpOption(string template)
{
// Help option is special because we stop parsing once we see it
// So we store it separately for further use
OptionHelp = Option(template, "Show help information", CommandOptionType.NoValue);
return OptionHelp;
}
public CommandOption VersionOption(string template,
string shortFormVersion,
string longFormVersion = null)
{
if (longFormVersion == null)
{
return VersionOption(template, () => shortFormVersion);
}
else
{
return VersionOption(template, () => shortFormVersion, () => longFormVersion);
}
}
// Helper method that adds a version option
public CommandOption VersionOption(string template,
Func<string> shortFormVersionGetter,
Func<string> longFormVersionGetter = null)
{
// Version option is special because we stop parsing once we see it
// So we store it separately for further use
OptionVersion = Option(template, "Show version information", CommandOptionType.NoValue);
ShortVersionGetter = shortFormVersionGetter;
LongVersionGetter = longFormVersionGetter ?? shortFormVersionGetter;
return OptionVersion;
}
// Show short hint that reminds users to use help option
public void ShowHint()
{
if (OptionHelp != null)
{
Console.WriteLine(string.Format("Specify --{0} for a list of available options and commands.", OptionHelp.LongName));
}
}
// Show full help
public void ShowHelp(string commandName = null)
{
var headerBuilder = new StringBuilder("Usage:");
var usagePrefixLength = headerBuilder.Length;
for (var cmd = this; cmd != null; cmd = cmd.Parent)
{
cmd.IsShowingInformation = true;
if (cmd != this && cmd.Arguments.Any())
{
var args = string.Join(" ", cmd.Arguments.Select(arg => arg.Name));
headerBuilder.Insert(usagePrefixLength, string.Format(" {0} {1}", cmd.Name, args));
}
else
{
headerBuilder.Insert(usagePrefixLength, string.Format(" {0}", cmd.Name));
}
}
CommandLineApplication target;
if (commandName == null || string.Equals(Name, commandName, StringComparison.OrdinalIgnoreCase))
{
target = this;
}
else
{
target = Commands.SingleOrDefault(cmd => string.Equals(cmd.Name, commandName, StringComparison.OrdinalIgnoreCase));
if (target != null)
{
headerBuilder.AppendFormat(" {0}", commandName);
}
else
{
// The command name is invalid so don't try to show help for something that doesn't exist
target = this;
}
}
var optionsBuilder = new StringBuilder();
var commandsBuilder = new StringBuilder();
var argumentsBuilder = new StringBuilder();
var argumentSeparatorBuilder = new StringBuilder();
var maxArgLen = 0;
for (var cmd = target; cmd != null; cmd = cmd.Parent)
{
if (cmd.Arguments.Any())
{
if (cmd == target)
{
headerBuilder.Append(" [arguments]");
}
if (argumentsBuilder.Length == 0)
{
argumentsBuilder.AppendLine();
argumentsBuilder.AppendLine("Arguments:");
}
maxArgLen = Math.Max(maxArgLen, MaxArgumentLength(cmd.Arguments));
}
}
for (var cmd = target; cmd != null; cmd = cmd.Parent)
{
if (cmd.Arguments.Any())
{
var outputFormat = " {0}{1}";
foreach (var arg in cmd.Arguments)
{
argumentsBuilder.AppendFormat(
outputFormat,
arg.Name.PadRight(maxArgLen + 2),
arg.Description);
argumentsBuilder.AppendLine();
}
}
}
if (target.Options.Any())
{
headerBuilder.Append(" [options]");
optionsBuilder.AppendLine();
optionsBuilder.AppendLine("Options:");
var maxOptLen = MaxOptionTemplateLength(target.Options);
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxOptLen + 2);
foreach (var opt in target.Options)
{
optionsBuilder.AppendFormat(outputFormat, opt.Template, opt.Description);
optionsBuilder.AppendLine();
}
}
if (target.Commands.Any())
{
headerBuilder.Append(" [command]");
commandsBuilder.AppendLine();
commandsBuilder.AppendLine("Commands:");
var maxCmdLen = MaxCommandLength(target.Commands);
var outputFormat = string.Format(" {{0, -{0}}}{{1}}", maxCmdLen + 2);
foreach (var cmd in target.Commands.OrderBy(c => c.Name))
{
commandsBuilder.AppendFormat(outputFormat, cmd.Name, cmd.Description);
commandsBuilder.AppendLine();
}
if (OptionHelp != null)
{
commandsBuilder.AppendLine();
commandsBuilder.AppendFormat("Use \"{0} [command] --help\" for more information about a command.", Name);
commandsBuilder.AppendLine();
}
}
if (target.AllowArgumentSeparator || target.HandleRemainingArguments)
{
if (target.AllowArgumentSeparator)
{
headerBuilder.Append(" [[--] <arg>...]]");
}
else
{
headerBuilder.Append(" [args]");
}
if (!string.IsNullOrEmpty(target.ArgumentSeparatorHelpText))
{
argumentSeparatorBuilder.AppendLine();
argumentSeparatorBuilder.AppendLine("Args:");
argumentSeparatorBuilder.AppendLine($" {target.ArgumentSeparatorHelpText}");
argumentSeparatorBuilder.AppendLine();
}
}
headerBuilder.AppendLine();
var nameAndVersion = new StringBuilder();
nameAndVersion.AppendLine(GetFullNameAndVersion());
nameAndVersion.AppendLine();
Console.Write("{0}{1}{2}{3}{4}{5}", nameAndVersion, headerBuilder, argumentsBuilder, optionsBuilder, commandsBuilder, argumentSeparatorBuilder);
}
public void ShowVersion()
{
for (var cmd = this; cmd != null; cmd = cmd.Parent)
{
cmd.IsShowingInformation = true;
}
Console.WriteLine(FullName);
Console.WriteLine(LongVersionGetter());
}
public string GetFullNameAndVersion()
=> ShortVersionGetter == null ? FullName : string.Format("{0} {1}", FullName, ShortVersionGetter());
public void ShowRootCommandFullNameAndVersion()
{
var rootCmd = this;
while (rootCmd.Parent != null)
{
rootCmd = rootCmd.Parent;
}
Console.WriteLine(rootCmd.GetFullNameAndVersion());
Console.WriteLine();
}
private static int MaxOptionTemplateLength(IEnumerable<CommandOption> options)
{
var maxLen = 0;
foreach (var opt in options)
{
maxLen = opt.Template.Length > maxLen ? opt.Template.Length : maxLen;
}
return maxLen;
}
private static int MaxCommandLength(IEnumerable<CommandLineApplication> commands)
{
var maxLen = 0;
foreach (var cmd in commands)
{
maxLen = cmd.Name.Length > maxLen ? cmd.Name.Length : maxLen;
}
return maxLen;
}
private static int MaxArgumentLength(IEnumerable<CommandArgument> arguments)
{
var maxLen = 0;
foreach (var arg in arguments)
{
maxLen = arg.Name.Length > maxLen ? arg.Name.Length : maxLen;
}
return maxLen;
}
private static void HandleUnexpectedArg(CommandLineApplication command, string[] args, int index, string argTypeName)
{
if (command._throwOnUnexpectedArg)
{
command.ShowHint();
throw new CommandParsingException(command, $"Unrecognized {argTypeName} '{args[index]}'");
}
else
{
command.RemainingArguments.Add(args[index]);
}
}
private IEnumerable<string> ExpandResponseFiles(IEnumerable<string> args)
{
foreach (var arg in args)
{
if (!arg.StartsWith("@", StringComparison.Ordinal))
{
yield return arg;
}
else
{
var fileName = arg.Substring(1);
var responseFileArguments = ParseResponseFile(fileName);
// ParseResponseFile can suppress expanding this response file by
// returning null. In that case, we'll treat the response
// file token as a regular argument.
if (responseFileArguments == null)
{
yield return arg;
}
else
{
foreach (var responseFileArgument in responseFileArguments)
{
yield return responseFileArgument.Trim();
}
}
}
}
}
private IEnumerable<string> ParseResponseFile(string fileName)
{
if (!HandleResponseFiles)
{
return null;
}
if (!File.Exists(fileName))
{
throw new InvalidOperationException($"Response file '{fileName}' doesn't exist.");
}
return File.ReadLines(fileName);
}
private class CommandArgumentEnumerator : IEnumerator<CommandArgument>
{
private readonly IEnumerator<CommandArgument> _enumerator;
public CommandArgumentEnumerator(IEnumerator<CommandArgument> enumerator) => _enumerator = enumerator;
public CommandArgument Current => _enumerator.Current;
object IEnumerator.Current => Current;
public void Dispose() => _enumerator.Dispose();
public bool MoveNext()
{
if (Current == null || !Current.MultipleValues)
{
return _enumerator.MoveNext();
}
// If current argument allows multiple values, we don't move forward and
// all later values will be added to current CommandArgument.Values
return true;
}
public void Reset() => _enumerator.Reset();
}
}
}

View File

@ -1,18 +0,0 @@
// 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.
namespace Microsoft.DotNet.Cli.CommandLine
{
internal static class CommandLineApplicationExtensions
{
public static CommandOption Option(this CommandLineApplication command, string template, string description)
=> command.Option(
template,
description,
template.IndexOf('<') != -1
? template.EndsWith(">...")
? CommandOptionType.MultipleValue
: CommandOptionType.SingleValue
: CommandOptionType.NoValue);
}
}

View File

@ -1,125 +0,0 @@
// 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.Linq;
namespace Microsoft.DotNet.Cli.CommandLine
{
internal class CommandOption
{
public CommandOption(string template, CommandOptionType optionType)
{
Template = template;
OptionType = optionType;
Values = new List<string>();
foreach (var part in Template.Split(new[] { ' ', '|' }, StringSplitOptions.RemoveEmptyEntries))
{
if (part.StartsWith("--"))
{
LongName = part.Substring(2);
}
else if (part.StartsWith("-"))
{
var optName = part.Substring(1);
// If there is only one char and it is not an English letter, it is a symbol option (e.g. "-?")
if (optName.Length == 1 && !IsEnglishLetter(optName[0]))
{
SymbolName = optName;
}
else
{
ShortName = optName;
}
}
else if (part.StartsWith("<") && part.EndsWith(">"))
{
ValueName = part.Substring(1, part.Length - 2);
}
else if (optionType == CommandOptionType.MultipleValue && part.StartsWith("<") && part.EndsWith(">..."))
{
ValueName = part.Substring(1, part.Length - 5);
}
else
{
throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template));
}
}
if (string.IsNullOrEmpty(LongName) && string.IsNullOrEmpty(ShortName) && string.IsNullOrEmpty(SymbolName))
{
throw new ArgumentException($"Invalid template pattern '{template}'", nameof(template));
}
}
public string Template { get; set; }
public string ShortName { get; set; }
public string LongName { get; set; }
public string SymbolName { get; set; }
public string ValueName { get; set; }
public string Description { get; set; }
public List<string> Values { get; private set; }
public bool? BoolValue { get; private set; }
public CommandOptionType OptionType { get; private set; }
public bool TryParse(string value)
{
switch (OptionType)
{
case CommandOptionType.MultipleValue:
Values.Add(value);
break;
case CommandOptionType.SingleValue:
if (Values.Any())
{
return false;
}
Values.Add(value);
break;
case CommandOptionType.BoolValue:
if (Values.Any())
{
return false;
}
if (value == null)
{
// add null to indicate that the option was present, but had no value
Values.Add(null);
BoolValue = true;
}
else
{
if (!bool.TryParse(value, out var boolValue))
{
return false;
}
Values.Add(value);
BoolValue = boolValue;
}
break;
case CommandOptionType.NoValue:
if (value != null)
{
return false;
}
// Add a value to indicate that this option was specified
Values.Add("on");
break;
default:
break;
}
return true;
}
public bool HasValue() => Values.Any();
public string Value() => HasValue() ? Values[0] : null;
private static bool IsEnglishLetter(char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
}

View File

@ -1,13 +0,0 @@
// 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.
namespace Microsoft.DotNet.Cli.CommandLine
{
internal enum CommandOptionType
{
MultipleValue,
SingleValue,
BoolValue,
NoValue
}
}

View File

@ -1,15 +0,0 @@
// 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;
namespace Microsoft.DotNet.Cli.CommandLine
{
internal class CommandParsingException : Exception
{
public CommandParsingException(CommandLineApplication command, string message)
: base(message) => Command = command;
public CommandLineApplication Command { get; }
}
}

View File

@ -1,26 +1,43 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.DotNet.Cli.CommandLine; using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
internal abstract class CommandBase internal abstract class CommandBase
{ {
private readonly IConsole _console;
public bool IsQuiet { get; private set; }
public bool IsVerbose { get; private set; }
protected IReporter Reporter { get; private set; }
protected CommandBase(IConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
Reporter = new ConsoleReporter(_console);
}
public virtual void Configure(CommandLineApplication command) public virtual void Configure(CommandLineApplication command)
{ {
var verbose = command.Option("-v|--verbose", Resources.VerboseDescription);
var noColor = command.Option("--no-color", Resources.NoColorDescription);
var prefixOutput = command.Option("--prefix-output", Resources.PrefixDescription); var prefixOutput = command.Option("--prefix-output", Resources.PrefixDescription);
var quiet = command.Option("-q|--quiet", Resources.QuietDescription);
command.HandleResponseFiles = true; var verbose = command.VerboseOption();
command.OnExecute( command.OnExecute(
() => () =>
{ {
Reporter.IsVerbose = verbose.HasValue(); IsQuiet = quiet.HasValue();
Reporter.NoColor = noColor.HasValue(); IsVerbose = verbose.HasValue() || CliContext.IsGlobalVerbose();
Reporter.PrefixOutput = prefixOutput.HasValue(); ReporterExtensions.PrefixOutput = prefixOutput.HasValue();
// Update the reporter now that we know the option values.
Reporter = new ConsoleReporter(_console, IsVerbose, IsQuiet);
Validate(); Validate();
@ -30,6 +47,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
protected virtual void Validate() protected virtual void Validate()
{ {
if (IsQuiet && IsVerbose)
{
throw new CommandException(Resources.QuietAndVerboseSpecified);
}
} }
protected virtual int Execute() => 0; protected virtual int Execute() => 0;

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -8,7 +8,8 @@ using System.Reflection;
#if NETCOREAPP2_1 #if NETCOREAPP2_1
using System.Runtime.Loader; using System.Runtime.Loader;
#endif #endif
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
@ -17,6 +18,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
private CommandOption _fileListPath; private CommandOption _fileListPath;
private CommandOption _output; private CommandOption _output;
public GetDocumentCommand(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command) public override void Configure(CommandLineApplication command)
{ {
base.Configure(command); base.Configure(command);
@ -124,13 +129,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
FileListPath = _fileListPath.Value(), FileListPath = _fileListPath.Value(),
OutputDirectory = _output.Value(), OutputDirectory = _output.Value(),
ProjectName = ProjectName.Value(), ProjectName = ProjectName.Value(),
Reporter = Reporter,
}; };
return GetDocumentCommandWorker.Process(context); return new GetDocumentCommandWorker(context).Process();
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.Error.WriteLine(ex.ToString()); Reporter.WriteError(ex.ToString());
return 2; return 2;
} }
} }

View File

@ -1,7 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
@ -17,5 +18,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
public string OutputDirectory { get; set; } public string OutputDirectory { get; set; }
public string ProjectName { get; set; } public string ProjectName { get; set; }
public IReporter Reporter { get; set; }
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -8,6 +8,7 @@ using System.Reflection;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
@ -32,14 +33,23 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
private static readonly Type[] GenerateMethodParameterTypes = new[] { typeof(string), typeof(TextWriter) }; private static readonly Type[] GenerateMethodParameterTypes = new[] { typeof(string), typeof(TextWriter) };
private static readonly Type GenerateMethodReturnType = typeof(Task); private static readonly Type GenerateMethodReturnType = typeof(Task);
public static int Process(GetDocumentCommandContext context) private readonly GetDocumentCommandContext _context;
private readonly IReporter _reporter;
public GetDocumentCommandWorker(GetDocumentCommandContext context)
{ {
var assemblyName = new AssemblyName(context.AssemblyName); _context = context ?? throw new ArgumentNullException(nameof(context));
_reporter = context.Reporter;
}
public int Process()
{
var assemblyName = new AssemblyName(_context.AssemblyName);
var assembly = Assembly.Load(assemblyName); var assembly = Assembly.Load(assemblyName);
var entryPointType = assembly.EntryPoint?.DeclaringType; var entryPointType = assembly.EntryPoint?.DeclaringType;
if (entryPointType == null) if (entryPointType == null)
{ {
Reporter.WriteError(Resources.FormatMissingEntryPoint(context.AssemblyPath)); _reporter.WriteError(Resources.FormatMissingEntryPoint(_context.AssemblyPath));
return 3; return 3;
} }
@ -48,7 +58,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var serviceFactory = HostFactoryResolver.ResolveServiceProviderFactory(assembly); var serviceFactory = HostFactoryResolver.ResolveServiceProviderFactory(assembly);
if (serviceFactory == null) if (serviceFactory == null)
{ {
Reporter.WriteError(Resources.FormatMethodsNotFound( _reporter.WriteError(Resources.FormatMethodsNotFound(
HostFactoryResolver.BuildWebHost, HostFactoryResolver.BuildWebHost,
HostFactoryResolver.CreateHostBuilder, HostFactoryResolver.CreateHostBuilder,
HostFactoryResolver.CreateWebHostBuilder, HostFactoryResolver.CreateWebHostBuilder,
@ -60,7 +70,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var services = serviceFactory(Array.Empty<string>()); var services = serviceFactory(Array.Empty<string>());
if (services == null) if (services == null)
{ {
Reporter.WriteError(Resources.FormatServiceProviderNotFound( _reporter.WriteError(Resources.FormatServiceProviderNotFound(
typeof(IServiceProvider), typeof(IServiceProvider),
HostFactoryResolver.BuildWebHost, HostFactoryResolver.BuildWebHost,
HostFactoryResolver.CreateHostBuilder, HostFactoryResolver.CreateHostBuilder,
@ -70,7 +80,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
return 5; return 5;
} }
var success = GetDocuments(context, services); var success = GetDocuments(services);
if (!success) if (!success)
{ {
return 6; return 6;
@ -78,14 +88,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
} }
catch (Exception ex) catch (Exception ex)
{ {
Reporter.WriteError(ex.ToString()); _reporter.WriteError(ex.ToString());
return 7; return 7;
} }
return 0; return 0;
} }
private static bool GetDocuments(GetDocumentCommandContext context, IServiceProvider services) private bool GetDocuments(IServiceProvider services)
{ {
Type serviceType = null; Type serviceType = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
@ -99,7 +109,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
if (serviceType == null) if (serviceType == null)
{ {
Reporter.WriteError(Resources.FormatServiceTypeNotFound(DocumentService)); _reporter.WriteError(Resources.FormatServiceTypeNotFound(DocumentService));
return false; return false;
} }
@ -126,7 +136,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var service = services.GetService(serviceType); var service = services.GetService(serviceType);
if (service == null) if (service == null)
{ {
Reporter.WriteError(Resources.FormatServiceNotFound(DocumentService)); _reporter.WriteError(Resources.FormatServiceNotFound(DocumentService));
return false; return false;
} }
@ -138,14 +148,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
// Write out the documents. // Write out the documents.
var found = false; var found = false;
Directory.CreateDirectory(context.OutputDirectory); Directory.CreateDirectory(_context.OutputDirectory);
var filePathList = new List<string>(); var filePathList = new List<string>();
foreach (var documentName in documentNames) foreach (var documentName in documentNames)
{ {
var filePath = GetDocument( var filePath = GetDocument(
documentName, documentName,
context.ProjectName, _context.ProjectName,
context.OutputDirectory, _context.OutputDirectory,
generateMethod, generateMethod,
service); service);
if (filePath == null) if (filePath == null)
@ -158,26 +168,26 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
} }
// Write out the cache file. // Write out the cache file.
var stream = File.Create(context.FileListPath); var stream = File.Create(_context.FileListPath);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
writer.WriteLine(string.Join(Environment.NewLine, filePathList)); writer.WriteLine(string.Join(Environment.NewLine, filePathList));
if (!found) if (!found)
{ {
Reporter.WriteError(Resources.DocumentsNotFound); _reporter.WriteError(Resources.DocumentsNotFound);
} }
return found; return found;
} }
private static string GetDocument( private string GetDocument(
string documentName, string documentName,
string projectName, string projectName,
string outputDirectory, string outputDirectory,
MethodInfo generateMethod, MethodInfo generateMethod,
object service) object service)
{ {
Reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName)); _reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName));
using var stream = new MemoryStream(); using var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, UTF8EncodingWithoutBOM, bufferSize: 1024, leaveOpen: true)) using (var writer = new StreamWriter(stream, UTF8EncodingWithoutBOM, bufferSize: 1024, leaveOpen: true))
@ -192,21 +202,21 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var finished = resultTask.Wait(TimeSpan.FromMinutes(1)); var finished = resultTask.Wait(TimeSpan.FromMinutes(1));
if (!finished) if (!finished)
{ {
Reporter.WriteError(Resources.FormatMethodTimedOut(GenerateMethodName, DocumentService, 1)); _reporter.WriteError(Resources.FormatMethodTimedOut(GenerateMethodName, DocumentService, 1));
return null; return null;
} }
} }
if (stream.Length == 0L) if (stream.Length == 0L)
{ {
Reporter.WriteError( _reporter.WriteError(
Resources.FormatMethodWroteNoContent(GenerateMethodName, DocumentService, documentName)); Resources.FormatMethodWroteNoContent(GenerateMethodName, DocumentService, documentName));
return null; return null;
} }
var filePath = GetDocumentPath(documentName, projectName, outputDirectory); var filePath = GetDocumentPath(documentName, projectName, outputDirectory);
Reporter.WriteInformation(Resources.FormatWritingDocument(documentName, filePath)); _reporter.WriteInformation(Resources.FormatWritingDocument(documentName, filePath));
try try
{ {
stream.Position = 0L; stream.Position = 0L;
@ -256,24 +266,24 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
return path; return path;
} }
private static MethodInfo GetMethod(string methodName, Type type, Type[] parameterTypes, Type returnType) private MethodInfo GetMethod(string methodName, Type type, Type[] parameterTypes, Type returnType)
{ {
var method = type.GetMethod(methodName, parameterTypes); var method = type.GetMethod(methodName, parameterTypes);
if (method == null) if (method == null)
{ {
Reporter.WriteError(Resources.FormatMethodNotFound(methodName, type)); _reporter.WriteError(Resources.FormatMethodNotFound(methodName, type));
return null; return null;
} }
if (method.IsStatic) if (method.IsStatic)
{ {
Reporter.WriteError(Resources.FormatMethodIsStatic(methodName, type)); _reporter.WriteError(Resources.FormatMethodIsStatic(methodName, type));
return null; return null;
} }
if (!returnType.IsAssignableFrom(method.ReturnType)) if (!returnType.IsAssignableFrom(method.ReturnType))
{ {
Reporter.WriteError( _reporter.WriteError(
Resources.FormatMethodReturnTypeUnsupported(methodName, type, method.ReturnType, returnType)); Resources.FormatMethodReturnTypeUnsupported(methodName, type, method.ReturnType, returnType));
return null; return null;
@ -282,12 +292,12 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
return method; return method;
} }
private static object InvokeMethod(MethodInfo method, object instance, object[] arguments) private object InvokeMethod(MethodInfo method, object instance, object[] arguments)
{ {
var result = method.Invoke(instance, arguments); var result = method.Invoke(instance, arguments);
if (result == null) if (result == null)
{ {
Reporter.WriteError( _reporter.WriteError(
Resources.FormatMethodReturnedNull(method.Name, method.DeclaringType, method.ReturnType)); Resources.FormatMethodReturnedNull(method.Name, method.DeclaringType, method.ReturnType));
} }

View File

@ -1,16 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
internal class HelpCommandBase : CommandBase internal class HelpCommandBase : CommandBase
{ {
public HelpCommandBase(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command) public override void Configure(CommandLineApplication command)
{ {
command.HelpOption("-h|--help"); command.HelpOption();
command.VersionOption("--version", ProductInfo.GetVersion); command.VersionOptionFromAssemblyAttributes();
base.Configure(command); base.Configure(command);
} }
} }

View File

@ -1,12 +1,17 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{ {
internal abstract class ProjectCommandBase : HelpCommandBase internal abstract class ProjectCommandBase : HelpCommandBase
{ {
public ProjectCommandBase(IConsole console) : base(console)
{
}
public CommandOption AssemblyPath { get; private set; } public CommandOption AssemblyPath { get; private set; }
public CommandOption ProjectName { get; private set; } public CommandOption ProjectName { get; private set; }

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>GetDocument.Insider</AssemblyName> <AssemblyName>GetDocument.Insider</AssemblyName>
<Description>GetDocument Command-line Tool inside man</Description> <Description>GetDocument Command-line Tool inside man</Description>
@ -15,6 +15,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="$(ToolSharedSourceRoot)CommandLine/**/*.cs" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" /> <Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
</ItemGroup> </ItemGroup>

View File

@ -1,16 +0,0 @@
// 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.Reflection;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal static class ProductInfo
{
public static string GetVersion()
=> typeof(ProductInfo)
.Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
.InformationalVersion;
}
}

View File

@ -1,47 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.ApiDescription.Tool.Commands; using Microsoft.Extensions.ApiDescription.Tool.Commands;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool namespace Microsoft.Extensions.ApiDescription.Tool
{ {
internal static class Program internal class Program : ProgramBase
{ {
public Program(IConsole console) : base(console)
{
}
private static int Main(string[] args) private static int Main(string[] args)
{ {
if (Console.IsOutputRedirected) DebugHelper.HandleDebugSwitch(ref args);
{
Console.OutputEncoding = Encoding.UTF8;
}
var app = new CommandLineApplication() var console = GetConsole();
{
FullName = Resources.CommandFullName,
Name = Resources.CommandFullName,
};
new GetDocumentCommand().Configure(app); return new Program(console).Run(args, new GetDocumentCommand(console), throwOnUnexpectedArg: true);
try
{
return app.Execute(args);
}
catch (Exception ex)
{
if (ex is CommandException || ex is CommandParsingException)
{
Reporter.WriteError(ex.Message);
}
else
{
Reporter.WriteError(ex.ToString());
}
return 1;
}
} }
} }
} }

View File

@ -0,0 +1,68 @@
// 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.Text;
using Microsoft.Extensions.ApiDescription.Tool.Commands;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal abstract class ProgramBase
{
private readonly IConsole _console;
private readonly IReporter _reporter;
public ProgramBase(IConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_reporter = new ConsoleReporter(_console, verbose: false, quiet: false);
}
protected static IConsole GetConsole()
{
var console = PhysicalConsole.Singleton;
if (console.IsOutputRedirected)
{
Console.OutputEncoding = Encoding.UTF8;
}
return console;
}
protected int Run(string[] args, CommandBase command, bool throwOnUnexpectedArg)
{
try
{
// AllowArgumentSeparator and continueAfterUnexpectedArg are ignored when !throwOnUnexpectedArg _except_
// AllowArgumentSeparator=true changes the help text (ignoring throwOnUnexpectedArg).
var app = new CommandLineApplication(throwOnUnexpectedArg, continueAfterUnexpectedArg: true)
{
AllowArgumentSeparator = !throwOnUnexpectedArg,
Error = _console.Error,
FullName = Resources.CommandFullName,
Name = Resources.CommandFullName,
Out = _console.Out,
};
command.Configure(app);
return app.Execute(args);
}
catch (Exception ex)
{
if (ex is CommandException || ex is CommandParsingException)
{
_reporter.WriteError(ex.Message);
}
else
{
_reporter.WriteError(ex.ToString());
}
return 1;
}
}
}
}

View File

@ -1,61 +0,0 @@
// 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.Linq;
using static Microsoft.Extensions.ApiDescription.Tool.AnsiConstants;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal static class Reporter
{
private static AnsiTextWriter Error = new AnsiTextWriter(Console.Error);
private static AnsiTextWriter Out = new AnsiTextWriter(Console.Out);
public static bool IsVerbose { get; set; }
public static bool NoColor { get; set; }
public static bool PrefixOutput { get; set; }
public static string Colorize(string value, Func<string, string> colorizeFunc)
=> NoColor ? value : colorizeFunc(value);
public static void WriteError(string message)
=> WriteLine(Prefix("error: ", Colorize(message, x => Bold + Red + x + Reset)), isError: true);
public static void WriteWarning(string message)
=> WriteLine(Prefix("warn: ", Colorize(message, x => Bold + Yellow + x + Reset)));
public static void WriteInformation(string message)
=> WriteLine(Prefix("info: ", message));
public static void WriteData(string message)
=> WriteLine(Prefix("data: ", Colorize(message, x => Bold + Gray + x + Reset)));
public static void WriteVerbose(string message)
{
if (IsVerbose)
{
WriteLine(Prefix("verbose: ", Colorize(message, x => Gray + x + Reset)));
}
}
private static string Prefix(string prefix, string value)
=> PrefixOutput
? string.Join(
Environment.NewLine,
value.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Select(l => prefix + l))
: value;
private static void WriteLine(string value, bool isError = false)
{
if (NoColor)
{
(isError ? Console.Error : Console.Out).WriteLine(value);
}
else
{
(isError ? Error : Out).WriteLine(value);
}
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Linq;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool
{
internal static class ReporterExtensions
{
public static bool PrefixOutput { get; set; }
public static void WriteError(this IReporter reporter, string message)
=> reporter.Error(Prefix("error: ", message));
public static void WriteWarning(this IReporter reporter, string message)
=> reporter.Warn(Prefix("warn: ", message));
public static void WriteInformation(this IReporter reporter, string message)
=> reporter.Output(Prefix("info: ", message));
public static void WriteVerbose(this IReporter reporter, string message)
=> reporter.Verbose(Prefix("verbose: ", message));
private static string Prefix(string prefix, string value)
{
if (PrefixOutput)
{
return string.Join(
Environment.NewLine,
value
.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
.Select(l => prefix + l));
}
return value;
}
}
}

View File

@ -123,9 +123,6 @@
<data name="MissingOption" xml:space="preserve"> <data name="MissingOption" xml:space="preserve">
<value>Missing required option '--{0}'.</value> <value>Missing required option '--{0}'.</value>
</data> </data>
<data name="NoColorDescription" xml:space="preserve">
<value>Do not colorize console output.</value>
</data>
<data name="OutputDescription" xml:space="preserve"> <data name="OutputDescription" xml:space="preserve">
<value>The directory where the document files should be written. Required.</value> <value>The directory where the document files should be written. Required.</value>
</data> </data>
@ -191,4 +188,10 @@
<value>Unable to find any registered documents. Update the 'Startup' class to register a document.</value> <value>Unable to find any registered documents. Update the 'Startup' class to register a document.</value>
<comment>Do not translate 'Startup'</comment> <comment>Do not translate 'Startup'</comment>
</data> </data>
<data name="QuietAndVerboseSpecified" xml:space="preserve">
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
</data>
<data name="QuietDescription" xml:space="preserve">
<value>Suppresses all output except warnings and errors.</value>
</data>
</root> </root>

View File

@ -21,6 +21,17 @@ namespace Microsoft.Extensions.CommandLineUtils
return 0; return 0;
}); });
public static CommandOption Option(this CommandLineApplication command, string template, string description)
=> command.Option(
template,
description,
template.IndexOf('<') != -1
? template.EndsWith(">...") ? CommandOptionType.MultipleValue : CommandOptionType.SingleValue
: CommandOptionType.NoValue);
public static void VersionOptionFromAssemblyAttributes(this CommandLineApplication app)
=> app.VersionOptionFromAssemblyAttributes(typeof(CommandLineApplicationExtensions).Assembly);
public static void VersionOptionFromAssemblyAttributes(this CommandLineApplication app, Assembly assembly) public static void VersionOptionFromAssemblyAttributes(this CommandLineApplication app, Assembly assembly)
=> app.VersionOption("--version", GetInformationalVersion(assembly)); => app.VersionOption("--version", GetInformationalVersion(assembly));

View File

@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Tools.Internal
{ {
public class ConsoleReporter : IReporter public class ConsoleReporter : IReporter
{ {
private object _writeLock = new object(); private readonly object _writeLock = new object();
public ConsoleReporter(IConsole console) public ConsoleReporter(IConsole console)
: this(console, verbose: false, quiet: false) : this(console, verbose: false, quiet: false)

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
@ -6,7 +6,8 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -19,6 +20,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
private readonly ProjectOptions _projectOptions = new ProjectOptions(); private readonly ProjectOptions _projectOptions = new ProjectOptions();
private IList<string> _args; private IList<string> _args;
public InvokeCommand(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command) public override void Configure(CommandLineApplication command)
{ {
base.Configure(command); base.Configure(command);
@ -75,7 +80,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
targetFramework.Version)); targetFramework.Version));
} }
executable = "dotnet"; executable = DotNetMuxer.MuxerPathOrDefault();
toolsDirectory = Path.Combine(thisPath, "netcoreapp2.1"); toolsDirectory = Path.Combine(thisPath, "netcoreapp2.1");
args.Add("exec"); args.Add("exec");
@ -133,22 +138,22 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
args.Add("--tools-directory"); args.Add("--tools-directory");
args.Add(toolsDirectory); args.Add(toolsDirectory);
if (Reporter.IsVerbose) if (ReporterExtensions.PrefixOutput)
{
args.Add("--verbose");
}
if (Reporter.NoColor)
{
args.Add("--no-color");
}
if (Reporter.PrefixOutput)
{ {
args.Add("--prefix-output"); args.Add("--prefix-output");
} }
return Exe.Run(executable, args); if (IsQuiet)
{
args.Add("--quiet");
}
if (IsVerbose)
{
args.Add("--verbose");
}
return Exe.Run(executable, args, Reporter);
} }
finally finally
{ {

View File

@ -1,10 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool namespace Microsoft.Extensions.ApiDescription.Tool
{ {
@ -13,12 +14,13 @@ namespace Microsoft.Extensions.ApiDescription.Tool
public static int Run( public static int Run(
string executable, string executable,
IReadOnlyList<string> args, IReadOnlyList<string> args,
IReporter reporter,
string workingDirectory = null, string workingDirectory = null,
bool interceptOutput = false) bool interceptOutput = false)
{ {
var arguments = ToArguments(args); var arguments = ArgumentEscaper.EscapeAndConcatenate(args);
Reporter.WriteVerbose(executable + " " + arguments); reporter.WriteVerbose(executable + " " + arguments);
var startInfo = new ProcessStartInfo var startInfo = new ProcessStartInfo
{ {
@ -32,100 +34,30 @@ namespace Microsoft.Extensions.ApiDescription.Tool
startInfo.WorkingDirectory = workingDirectory; startInfo.WorkingDirectory = workingDirectory;
} }
using (var process = Process.Start(startInfo)) using var process = Process.Start(startInfo);
if (interceptOutput)
{ {
if (interceptOutput) string line;
while ((line = process.StandardOutput.ReadLine()) != null)
{ {
string line; reporter.WriteVerbose(line);
while ((line = process.StandardOutput.ReadLine()) != null)
{
Reporter.WriteVerbose(line);
}
} }
// Follow precedent set in Razor integration tests and ensure process events and output are complete.
// https://github.com/aspnet/Razor/blob/d719920fdcc7d1db3a6f74cd5404d66fa098f057/test/Microsoft.NET.Sdk.Razor.Test/IntegrationTests/MSBuildProcessManager.cs#L91-L102
// Timeout is double how long the inside man waits for the IDocumentProcessor to wrap up.
if (!process.WaitForExit((int)(TimeSpan.FromMinutes(2).TotalMilliseconds)))
{
process.Kill();
// Should be unreachable in almost every case.
throw new TimeoutException($"Process {executable} timed out after 2 minutes.");
}
process.WaitForExit();
return process.ExitCode;
}
}
private static string ToArguments(IReadOnlyList<string> args)
{
var builder = new StringBuilder();
for (var i = 0; i < args.Count; i++)
{
if (i != 0)
{
builder.Append(" ");
}
if (args[i].IndexOf(' ') == -1)
{
builder.Append(args[i]);
continue;
}
builder.Append("\"");
var pendingBackslashes = 0;
for (var j = 0; j < args[i].Length; j++)
{
switch (args[i][j])
{
case '\"':
if (pendingBackslashes != 0)
{
builder.Append('\\', pendingBackslashes * 2);
pendingBackslashes = 0;
}
builder.Append("\\\"");
break;
case '\\':
pendingBackslashes++;
break;
default:
if (pendingBackslashes != 0)
{
if (pendingBackslashes == 1)
{
builder.Append("\\");
}
else
{
builder.Append('\\', pendingBackslashes * 2);
}
pendingBackslashes = 0;
}
builder.Append(args[i][j]);
break;
}
}
if (pendingBackslashes != 0)
{
builder.Append('\\', pendingBackslashes * 2);
}
builder.Append("\"");
} }
return builder.ToString(); // Follow precedent set in Razor integration tests and ensure process events and output are complete.
// https://github.com/aspnet/Razor/blob/d719920fdcc7d1db3a6f74cd5404d66fa098f057/test/Microsoft.NET.Sdk.Razor.Test/IntegrationTests/MSBuildProcessManager.cs#L91-L102
// Timeout is double how long the inside man waits for the IDocumentProcessor to wrap up.
if (!process.WaitForExit((int)(TimeSpan.FromMinutes(2).TotalMilliseconds)))
{
process.Kill();
// Should be unreachable in almost every case.
throw new TimeoutException($"Process {executable} timed out after 2 minutes.");
}
process.WaitForExit();
return process.ExitCode;
} }
} }
} }

View File

@ -1,41 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.ApiDescription.Tool.Commands; using Microsoft.Extensions.ApiDescription.Tool.Commands;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool namespace Microsoft.Extensions.ApiDescription.Tool
{ {
internal static class Program internal class Program : ProgramBase
{ {
public Program(IConsole console) : base(console)
{
}
private static int Main(string[] args) private static int Main(string[] args)
{ {
var app = new CommandLineApplication(throwOnUnexpectedArg: false) DebugHelper.HandleDebugSwitch(ref args);
{
FullName = Resources.CommandFullName,
Name = Resources.CommandFullName,
};
new InvokeCommand().Configure(app); var console = GetConsole();
try return new Program(console).Run(args, new InvokeCommand(console), throwOnUnexpectedArg: false);
{
return app.Execute(args);
}
catch (Exception ex)
{
if (ex is CommandException || ex is CommandParsingException)
{
Reporter.WriteError(ex.Message);
}
else
{
Reporter.WriteError(ex.ToString());
}
return 1;
}
} }
} }
} }

View File

@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.DotNet.Cli.CommandLine; using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.Extensions.ApiDescription.Tool namespace Microsoft.Extensions.ApiDescription.Tool
{ {

View File

@ -129,9 +129,6 @@
<data name="NETStandardProject" xml:space="preserve"> <data name="NETStandardProject" xml:space="preserve">
<value>Project '{0}' targets framework '.NETStandard'. There is no runtime associated with this framework and projects targeting it cannot be executed directly. To use the dotnet-getdocument tool with this project, update this project to target .NET Core and / or .NET Framework.</value> <value>Project '{0}' targets framework '.NETStandard'. There is no runtime associated with this framework and projects targeting it cannot be executed directly. To use the dotnet-getdocument tool with this project, update this project to target .NET Core and / or .NET Framework.</value>
</data> </data>
<data name="NoColorDescription" xml:space="preserve">
<value>Do not colorize console output.</value>
</data>
<data name="PrefixDescription" xml:space="preserve"> <data name="PrefixDescription" xml:space="preserve">
<value>Prefix console output with logging level.</value> <value>Prefix console output with logging level.</value>
</data> </data>
@ -159,4 +156,10 @@
<data name="AssemblyDescription" xml:space="preserve"> <data name="AssemblyDescription" xml:space="preserve">
<value>The assembly path to use. Required.</value> <value>The assembly path to use. Required.</value>
</data> </data>
<data name="QuietAndVerboseSpecified" xml:space="preserve">
<value>Cannot specify both '--quiet' and '--verbose' options.</value>
</data>
<data name="QuietDescription" xml:space="preserve">
<value>Suppresses all output except warnings and errors.</value>
</data>
</root> </root>

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyName>dotnet-getdocument</AssemblyName> <AssemblyName>dotnet-getdocument</AssemblyName>
<Description>GetDocument Command-line Tool outside man</Description> <Description>GetDocument Command-line Tool outside man</Description>
@ -12,14 +12,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="../../GetDocumentInsider/src/Ansi*.cs" /> <Compile Include="$(ToolSharedSourceRoot)CommandLine/**/*.cs" />
<Compile Include="../../GetDocumentInsider/src/CommandException.cs" /> <Compile Include="../../GetDocumentInsider/src/CommandException.cs" />
<Compile Include="../../GetDocumentInsider/src/CommandLineUtils/*.cs" LinkBase="CommandLineUtils" />
<Compile Include="../../GetDocumentInsider/src/Commands/CommandBase.cs" Link="Commands/CommandBase.cs" /> <Compile Include="../../GetDocumentInsider/src/Commands/CommandBase.cs" Link="Commands/CommandBase.cs" />
<Compile Include="../../GetDocumentInsider/src/Commands/HelpCommandBase.cs" Link="Commands/HelpCommandBase.cs" /> <Compile Include="../../GetDocumentInsider/src/Commands/HelpCommandBase.cs" Link="Commands/HelpCommandBase.cs" />
<Compile Include="../../GetDocumentInsider/src/ProductInfo.cs" /> <Compile Include="../../GetDocumentInsider/src/ProgramBase.cs" />
<Compile Include="../../GetDocumentInsider/src/Reporter.cs" /> <Compile Include="../../GetDocumentInsider/src/ReporterExtensions.cs" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<Reference Include="Newtonsoft.Json" /> <Reference Include="Newtonsoft.Json" />
</ItemGroup> </ItemGroup>
</Project> </Project>