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>
<!--
Settings users may update as they see fit.
-->
<PropertyGroup>
<!--
Options added to the OpenAPI document generation tool ('dotnet-getdocument') command line. Available options
control console output: 'no-color', 'prefix-output' and 'verbose'. All require a double-dash prefix.
Options added to the OpenAPI document generation tool ('dotnet-getdocument') command line. For example, the
'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)' == '' " />

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.
using Microsoft.DotNet.Cli.CommandLine;
using System;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{
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)
{
var verbose = command.Option("-v|--verbose", Resources.VerboseDescription);
var noColor = command.Option("--no-color", Resources.NoColorDescription);
var prefixOutput = command.Option("--prefix-output", Resources.PrefixDescription);
command.HandleResponseFiles = true;
var quiet = command.Option("-q|--quiet", Resources.QuietDescription);
var verbose = command.VerboseOption();
command.OnExecute(
() =>
{
Reporter.IsVerbose = verbose.HasValue();
Reporter.NoColor = noColor.HasValue();
Reporter.PrefixOutput = prefixOutput.HasValue();
IsQuiet = quiet.HasValue();
IsVerbose = verbose.HasValue() || CliContext.IsGlobalVerbose();
ReporterExtensions.PrefixOutput = prefixOutput.HasValue();
// Update the reporter now that we know the option values.
Reporter = new ConsoleReporter(_console, IsVerbose, IsQuiet);
Validate();
@ -30,6 +47,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
protected virtual void Validate()
{
if (IsQuiet && IsVerbose)
{
throw new CommandException(Resources.QuietAndVerboseSpecified);
}
}
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.
using System;
@ -8,7 +8,8 @@ using System.Reflection;
#if NETCOREAPP2_1
using System.Runtime.Loader;
#endif
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{
@ -17,6 +18,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
private CommandOption _fileListPath;
private CommandOption _output;
public GetDocumentCommand(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command)
{
base.Configure(command);
@ -124,13 +129,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
FileListPath = _fileListPath.Value(),
OutputDirectory = _output.Value(),
ProjectName = ProjectName.Value(),
Reporter = Reporter,
};
return GetDocumentCommandWorker.Process(context);
return new GetDocumentCommandWorker(context).Process();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex.ToString());
Reporter.WriteError(ex.ToString());
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.
using System;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{
@ -17,5 +18,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
public string OutputDirectory { 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.
using System;
@ -8,6 +8,7 @@ using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Tools.Internal;
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 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 entryPointType = assembly.EntryPoint?.DeclaringType;
if (entryPointType == null)
{
Reporter.WriteError(Resources.FormatMissingEntryPoint(context.AssemblyPath));
_reporter.WriteError(Resources.FormatMissingEntryPoint(_context.AssemblyPath));
return 3;
}
@ -48,7 +58,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var serviceFactory = HostFactoryResolver.ResolveServiceProviderFactory(assembly);
if (serviceFactory == null)
{
Reporter.WriteError(Resources.FormatMethodsNotFound(
_reporter.WriteError(Resources.FormatMethodsNotFound(
HostFactoryResolver.BuildWebHost,
HostFactoryResolver.CreateHostBuilder,
HostFactoryResolver.CreateWebHostBuilder,
@ -60,7 +70,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var services = serviceFactory(Array.Empty<string>());
if (services == null)
{
Reporter.WriteError(Resources.FormatServiceProviderNotFound(
_reporter.WriteError(Resources.FormatServiceProviderNotFound(
typeof(IServiceProvider),
HostFactoryResolver.BuildWebHost,
HostFactoryResolver.CreateHostBuilder,
@ -70,7 +80,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
return 5;
}
var success = GetDocuments(context, services);
var success = GetDocuments(services);
if (!success)
{
return 6;
@ -78,14 +88,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
}
catch (Exception ex)
{
Reporter.WriteError(ex.ToString());
_reporter.WriteError(ex.ToString());
return 7;
}
return 0;
}
private static bool GetDocuments(GetDocumentCommandContext context, IServiceProvider services)
private bool GetDocuments(IServiceProvider services)
{
Type serviceType = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
@ -99,7 +109,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
if (serviceType == null)
{
Reporter.WriteError(Resources.FormatServiceTypeNotFound(DocumentService));
_reporter.WriteError(Resources.FormatServiceTypeNotFound(DocumentService));
return false;
}
@ -126,7 +136,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
var service = services.GetService(serviceType);
if (service == null)
{
Reporter.WriteError(Resources.FormatServiceNotFound(DocumentService));
_reporter.WriteError(Resources.FormatServiceNotFound(DocumentService));
return false;
}
@ -138,14 +148,14 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
// Write out the documents.
var found = false;
Directory.CreateDirectory(context.OutputDirectory);
Directory.CreateDirectory(_context.OutputDirectory);
var filePathList = new List<string>();
foreach (var documentName in documentNames)
{
var filePath = GetDocument(
documentName,
context.ProjectName,
context.OutputDirectory,
_context.ProjectName,
_context.OutputDirectory,
generateMethod,
service);
if (filePath == null)
@ -158,26 +168,26 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
}
// Write out the cache file.
var stream = File.Create(context.FileListPath);
var stream = File.Create(_context.FileListPath);
using var writer = new StreamWriter(stream);
writer.WriteLine(string.Join(Environment.NewLine, filePathList));
if (!found)
{
Reporter.WriteError(Resources.DocumentsNotFound);
_reporter.WriteError(Resources.DocumentsNotFound);
}
return found;
}
private static string GetDocument(
private string GetDocument(
string documentName,
string projectName,
string outputDirectory,
MethodInfo generateMethod,
object service)
{
Reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName));
_reporter.WriteInformation(Resources.FormatGeneratingDocument(documentName));
using var stream = new MemoryStream();
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));
if (!finished)
{
Reporter.WriteError(Resources.FormatMethodTimedOut(GenerateMethodName, DocumentService, 1));
_reporter.WriteError(Resources.FormatMethodTimedOut(GenerateMethodName, DocumentService, 1));
return null;
}
}
if (stream.Length == 0L)
{
Reporter.WriteError(
_reporter.WriteError(
Resources.FormatMethodWroteNoContent(GenerateMethodName, DocumentService, documentName));
return null;
}
var filePath = GetDocumentPath(documentName, projectName, outputDirectory);
Reporter.WriteInformation(Resources.FormatWritingDocument(documentName, filePath));
_reporter.WriteInformation(Resources.FormatWritingDocument(documentName, filePath));
try
{
stream.Position = 0L;
@ -256,24 +266,24 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
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);
if (method == null)
{
Reporter.WriteError(Resources.FormatMethodNotFound(methodName, type));
_reporter.WriteError(Resources.FormatMethodNotFound(methodName, type));
return null;
}
if (method.IsStatic)
{
Reporter.WriteError(Resources.FormatMethodIsStatic(methodName, type));
_reporter.WriteError(Resources.FormatMethodIsStatic(methodName, type));
return null;
}
if (!returnType.IsAssignableFrom(method.ReturnType))
{
Reporter.WriteError(
_reporter.WriteError(
Resources.FormatMethodReturnTypeUnsupported(methodName, type, method.ReturnType, returnType));
return null;
@ -282,12 +292,12 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
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);
if (result == null)
{
Reporter.WriteError(
_reporter.WriteError(
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.
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{
internal class HelpCommandBase : CommandBase
{
public HelpCommandBase(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command)
{
command.HelpOption("-h|--help");
command.VersionOption("--version", ProductInfo.GetVersion);
command.HelpOption();
command.VersionOptionFromAssemblyAttributes();
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.
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool.Commands
{
internal abstract class ProjectCommandBase : HelpCommandBase
{
public ProjectCommandBase(IConsole console) : base(console)
{
}
public CommandOption AssemblyPath { 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>
<AssemblyName>GetDocument.Insider</AssemblyName>
<Description>GetDocument Command-line Tool inside man</Description>
@ -15,6 +15,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="$(ToolSharedSourceRoot)CommandLine/**/*.cs" />
<Reference Include="Microsoft.Extensions.CommandLineUtils.Sources" />
<Reference Include="Microsoft.Extensions.HostFactoryResolver.Sources" />
</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.
using System;
using System.Text;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.ApiDescription.Tool.Commands;
using Microsoft.Extensions.Tools.Internal;
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)
{
if (Console.IsOutputRedirected)
{
Console.OutputEncoding = Encoding.UTF8;
}
DebugHelper.HandleDebugSwitch(ref args);
var app = new CommandLineApplication()
{
FullName = Resources.CommandFullName,
Name = Resources.CommandFullName,
};
var console = GetConsole();
new GetDocumentCommand().Configure(app);
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;
}
return new Program(console).Run(args, new GetDocumentCommand(console), throwOnUnexpectedArg: true);
}
}
}

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">
<value>Missing required option '--{0}'.</value>
</data>
<data name="NoColorDescription" xml:space="preserve">
<value>Do not colorize console output.</value>
</data>
<data name="OutputDescription" xml:space="preserve">
<value>The directory where the document files should be written. Required.</value>
</data>
@ -191,4 +188,10 @@
<value>Unable to find any registered documents. Update the 'Startup' class to register a document.</value>
<comment>Do not translate 'Startup'</comment>
</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>

View File

@ -21,6 +21,17 @@ namespace Microsoft.Extensions.CommandLineUtils
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)
=> app.VersionOption("--version", GetInformationalVersion(assembly));

View File

@ -8,7 +8,7 @@ namespace Microsoft.Extensions.Tools.Internal
{
public class ConsoleReporter : IReporter
{
private object _writeLock = new object();
private readonly object _writeLock = new object();
public ConsoleReporter(IConsole console)
: 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.
using System;
@ -6,7 +6,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -19,6 +20,10 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
private readonly ProjectOptions _projectOptions = new ProjectOptions();
private IList<string> _args;
public InvokeCommand(IConsole console) : base(console)
{
}
public override void Configure(CommandLineApplication command)
{
base.Configure(command);
@ -75,7 +80,7 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
targetFramework.Version));
}
executable = "dotnet";
executable = DotNetMuxer.MuxerPathOrDefault();
toolsDirectory = Path.Combine(thisPath, "netcoreapp2.1");
args.Add("exec");
@ -133,22 +138,22 @@ namespace Microsoft.Extensions.ApiDescription.Tool.Commands
args.Add("--tools-directory");
args.Add(toolsDirectory);
if (Reporter.IsVerbose)
{
args.Add("--verbose");
}
if (Reporter.NoColor)
{
args.Add("--no-color");
}
if (Reporter.PrefixOutput)
if (ReporterExtensions.PrefixOutput)
{
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
{

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.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;
namespace Microsoft.Extensions.ApiDescription.Tool
{
@ -13,12 +14,13 @@ namespace Microsoft.Extensions.ApiDescription.Tool
public static int Run(
string executable,
IReadOnlyList<string> args,
IReporter reporter,
string workingDirectory = null,
bool interceptOutput = false)
{
var arguments = ToArguments(args);
var arguments = ArgumentEscaper.EscapeAndConcatenate(args);
Reporter.WriteVerbose(executable + " " + arguments);
reporter.WriteVerbose(executable + " " + arguments);
var startInfo = new ProcessStartInfo
{
@ -32,100 +34,30 @@ namespace Microsoft.Extensions.ApiDescription.Tool
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;
while ((line = process.StandardOutput.ReadLine()) != null)
{
Reporter.WriteVerbose(line);
}
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.
using System;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.ApiDescription.Tool.Commands;
using Microsoft.Extensions.Tools.Internal;
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)
{
var app = new CommandLineApplication(throwOnUnexpectedArg: false)
{
FullName = Resources.CommandFullName,
Name = Resources.CommandFullName,
};
DebugHelper.HandleDebugSwitch(ref args);
new InvokeCommand().Configure(app);
var console = GetConsole();
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;
}
return new Program(console).Run(args, new InvokeCommand(console), throwOnUnexpectedArg: false);
}
}
}

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.
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.Extensions.ApiDescription.Tool
{

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@ -129,9 +129,6 @@
<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>
</data>
<data name="NoColorDescription" xml:space="preserve">
<value>Do not colorize console output.</value>
</data>
<data name="PrefixDescription" xml:space="preserve">
<value>Prefix console output with logging level.</value>
</data>
@ -159,4 +156,10 @@
<data name="AssemblyDescription" xml:space="preserve">
<value>The assembly path to use. Required.</value>
</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>

View File

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