30 Commits
2.0 ... 2.0.4

Author SHA1 Message Date
Tyrrrz
f2b4e53615 Update version 2021-04-24 20:59:10 +03:00
Tyrrrz
2d519ab190 Remove the usage of ConsoleColor.DarkGray because it looks bad in some terminals
Fixes #104
2021-04-24 20:48:06 +03:00
Tyrrrz
2d479c9cb6 Refactor 2021-04-24 20:43:35 +03:00
Tyrrrz
2bb7e13e51 Use issue forms 2021-04-22 22:11:39 +03:00
Tyrrrz
6e1dfdcdd4 Update readme 2021-04-22 21:08:16 +03:00
Tyrrrz
5ba647e5c1 Update readme 2021-04-22 21:05:35 +03:00
Tyrrrz
853492695f Update readme 2021-04-22 21:04:36 +03:00
Robert Dailey
d5d72c7c50 Show choices for nullable enums in enumerable (#105) 2021-04-22 15:28:33 +03:00
Tyrrrz
d676b5832e Fix discrepancies in unicode handling between ConsoleWriter and Console.Write(...) 2021-04-21 03:16:18 +03:00
Tyrrrz
28097afc1e Update NuGet packages 2021-04-18 19:38:35 +03:00
Tyrrrz
fda96586f3 Update NuGet.config 2021-04-17 21:23:19 +03:00
Tyrrrz
fc5af8dbbc Don't write default value in help text for types that don't override ToString() 2021-04-16 23:28:39 +03:00
Tyrrrz
4835e64388 Remove GHA workarounds 2021-04-13 22:29:15 +03:00
Tyrrrz
0999c33f93 Add NuGet.config 2021-04-13 22:20:07 +03:00
Tyrrrz
595805255a Update version 2021-04-09 22:24:06 +03:00
Tyrrrz
65eaa912cf Refactor 2021-04-08 20:53:48 +03:00
Robert Dailey
038f48b78e Show choices on non-scalar enum parameters and options (#102) 2021-04-08 20:51:17 +03:00
Tyrrrz
d7460244b7 Update version 2021-03-31 12:11:50 +03:00
Tyrrrz
02766868fc Streamline analyzer packaging 2021-03-31 12:11:36 +03:00
Tyrrrz
8d7d25a144 Cleanup 2021-03-31 11:40:37 +03:00
Tyrrrz
17ded54e24 Update readme 2021-03-31 11:29:31 +03:00
Tyrrrz
54a4c32ddf Fix nullref in SystemConsoleShouldBeAvoidedAnalyzer 2021-03-31 11:28:48 +03:00
Tyrrrz
6d46e82145 Add test for SystemConsoleShouldBeAvoidedAnalyzer for when System.Console isn't used 2021-03-31 00:11:36 +03:00
Tyrrrz
fd4a2a18fe Improve comment on IConsole.RegisterCancellationHandler() 2021-03-30 16:22:10 +03:00
Tyrrrz
bfe99d620e Refactor IConsole.WithColors(...) 2021-03-30 16:10:11 +03:00
Tyrrrz
c5a111207f Seal attributes 2021-03-25 06:01:46 +02:00
Tyrrrz
544945c0e6 Don't reference analyzer assembly from main assembly 2021-03-24 02:42:03 +02:00
Tyrrrz
c616cdd750 Update version 2021-03-24 02:40:10 +02:00
Tyrrrz
d3c396956d Fix StackFrame.ParseMany(...) being too paranoid about its own failure 2021-03-24 02:34:36 +02:00
Tyrrrz
d0cbbc6d9a Don't highlight valid values in help text 2021-03-23 01:56:28 +02:00
45 changed files with 470 additions and 182 deletions

30
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: "\U0001F41E Bug report"
description: Report broken functionality.
labels: [bug]
body:
- type: markdown
attributes:
value: |
- Please check existing issues (both opened and closed) to ensure that this bug hasn't been reported before.
- If you want to ask a question instead of reporting a bug, use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
- type: input
attributes:
label: Version
description: "Which version(s) of CliFx does this bug affect?"
validations:
required: true
- type: textarea
attributes:
label: Details
description: "Clear and thorough explanation of the bug. If relevant, include screenshots or screen recordings."
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: "Minimum steps or code required to reproduce the bug."
validations:
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: "\U0001F5E8 Ask a question"
url: https://github.com/Tyrrrz/CliFx/discussions/new
about: Please ask and answer questions here.

View File

@@ -0,0 +1,16 @@
name: "\U00002728 Feature request"
description: Request a new feature.
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
- Please check existing issues (both opened and closed) to ensure that this feature hasn't been requested before.
- If you want to ask a question instead of requesting a feature, use [**discussions**](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
- type: textarea
attributes:
label: Details
description: "Clear and thorough explanation of the feature you have in mind."
validations:
required: true

View File

@@ -19,9 +19,7 @@ jobs:
dotnet-version: 5.0.x
- name: Pack
run: |
dotnet nuget locals all --clear
dotnet pack CliFx --configuration Release
run: dotnet pack CliFx --configuration Release
- name: Deploy
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}

View File

@@ -20,9 +20,7 @@ jobs:
dotnet-version: 5.0.x
- name: Build & test
run: |
dotnet nuget locals all --clear
dotnet test --configuration Release --logger GitHubActions
run: dotnet test --configuration Release --logger GitHubActions
- name: Upload coverage
uses: codecov/codecov-action@v1.0.5

View File

@@ -1,3 +1,24 @@
### v2.0.4 (24-Apr-2021)
- Fixed an issue where output and error streams in `SystemConsole` defaulted to UTF8 encoding with BOM when the application was running with UTF8 codepage. `ConsoleWriter` will now discard preamble from the specified encoding. This fix brings the behavior of `SystemConsole` in line with .NET's own `System.Console` which also discards preamble for output and error streams.
- Fixed an issue where help text tried to show default values for parameters and options whose type does not override `ToString()` method.
- Fixed an issue where help text didn't show default values for parameters and options whose type is an enumerable of nullable enums. (Thanks [@Robert Dailey](https://github.com/rcdailey))
- Fixed an issue where specific parts of the help text weren't legible in some terminals due to low color resolution. Removed the usage of `ConsoleColor.DarkGray` in help text.
### v2.0.3 (09-Apr-2021)
- Improved help text by showing valid values for non-scalar enum parameters and options. (Thanks [@Robert Dailey](https://github.com/rcdailey))
### v2.0.2 (31-Mar-2021)
- Fixed an issue where having a transitive reference to CliFx sometimes resulted in `SystemConsoleShouldBeAvoidedAnalyzer` throwing `NullReferenceException` during build.
- Fixed some documentation typos and inconsistencies.
### v2.0.1 (24-Mar-2021)
- Fixed an issue where some exceptions with async stack traces generated on .NET 3.1 or earlier were not parsed and formatted correctly.
- Fixed an issue where help text applied slightly incorrect formatting when displaying choices for enum-based parameters and properties.
### v2.0 (21-Mar-2021)
> Note: this major release includes many breaking changes.

View File

@@ -15,7 +15,7 @@
<ItemGroup>
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />

View File

@@ -104,5 +104,24 @@ public class MyCommand : ICommand
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
[Fact]
public void Analyzer_does_not_report_an_error_if_a_command_does_not_access_SystemConsole()
{
// Arrange
// language=cs
const string code = @"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
return default;
}
}";
// Act & assert
Analyzer.Should().NotProduceDiagnostics(code);
}
}
}

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;

View File

@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var option = CommandOptionSymbol.TryResolve(property);
if (option is null)
return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (property.ContainingType.IsAbstract)
return;

View File

@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (IsScalar(property.Type))
return;

View File

@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
if (!CommandParameterSymbol.IsParameterProperty(property))
return;

View File

@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;

View File

@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
PropertyDeclarationSyntax propertyDeclaration,
IPropertySymbol property)
{
if (property.ContainingType is null)
return;
var parameter = CommandParameterSymbol.TryResolve(property);
if (parameter is null)
return;

View File

@@ -27,9 +27,9 @@ namespace CliFx.Analyzers
while (currentNode is MemberAccessExpressionSyntax memberAccess)
{
var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
var member = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
if (symbol is not null && symbol.ContainingType.DisplayNameMatches("System.Console"))
if (member?.ContainingType?.DisplayNameMatches("System.Console") == true)
{
return memberAccess;
}
@@ -53,7 +53,8 @@ namespace CliFx.Analyzers
return;
// Check if IConsole is available in scope as an alternative to System.Console
var isConsoleInterfaceAvailable = context.Node
var isConsoleInterfaceAvailable = context
.Node
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.SelectMany(m => m.ParameterList.Parameters)

View File

@@ -9,11 +9,16 @@ namespace CliFx.Analyzers.Utils.Extensions
internal static class RoslynExtensions
{
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
string.Equals(symbol.ToDisplayString(), name, StringComparison.Ordinal);
string.Equals(
// Fully qualified name, without `global::`
symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
name,
StringComparison.Ordinal
);
public static void HandleClassDeclaration(
this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> handler)
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze)
{
analysisContext.RegisterSyntaxNodeAction(ctx =>
{
@@ -24,13 +29,13 @@ namespace CliFx.Analyzers.Utils.Extensions
if (type is null)
return;
handler(ctx, classDeclaration, type);
analyze(ctx, classDeclaration, type);
}, SyntaxKind.ClassDeclaration);
}
public static void HandlePropertyDeclaration(
this AnalysisContext analysisContext,
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> handler)
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze)
{
analysisContext.RegisterSyntaxNodeAction(ctx =>
{
@@ -41,7 +46,7 @@ namespace CliFx.Analyzers.Utils.Extensions
if (property is null)
return;
handler(ctx, propertyDeclaration, property);
analyze(ctx, propertyDeclaration, property);
}, SyntaxKind.PropertyDeclaration);
}
}

View File

@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
<PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="Cocona" Version="1.5.0" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />

View File

@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -21,7 +21,7 @@ namespace CliFx.Demo.Domain
var data = File.ReadAllText(StorageFilePath);
return JsonConvert.DeserializeObject<Library>(data);
return JsonConvert.DeserializeObject<Library>(data) ?? Library.Empty;
}
public Book? TryGetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);

View File

@@ -13,11 +13,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.3.1" />
<PackageReference Include="CliWrap" Version="3.3.2" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />

View File

@@ -577,6 +577,96 @@ public class Command : ICommand
);
}
[Fact]
public async Task Help_text_shows_all_valid_values_for_non_scalar_enum_parameters_and_options()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
public enum CustomEnum { One, Two, Three }
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public IReadOnlyList<CustomEnum> Foo { get; set; }
[CommandOption(""bar"")]
public IReadOnlyList<CustomEnum> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"--help"},
new Dictionary<string, string>()
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Should().ContainAllInOrder(
"PARAMETERS",
"foo", "Choices:", "One", "Two", "Three",
"OPTIONS",
"--bar", "Choices:", "One", "Two", "Three"
);
}
[Fact]
public async Task Help_text_shows_all_valid_values_for_nullable_enum_parameters_and_options()
{
// Arrange
var commandType = DynamicCommandBuilder.Compile(
// language=cs
@"
public enum CustomEnum { One, Two, Three }
[Command]
public class Command : ICommand
{
[CommandParameter(0)]
public CustomEnum? Foo { get; set; }
[CommandOption(""bar"")]
public IReadOnlyList<CustomEnum?> Bar { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}
");
var application = new CliApplicationBuilder()
.AddCommand(commandType)
.UseConsole(FakeConsole)
.Build();
// Act
var exitCode = await application.RunAsync(
new[] {"--help"},
new Dictionary<string, string>()
);
var stdOut = FakeConsole.ReadOutputString();
// Assert
exitCode.Should().Be(0);
stdOut.Should().ContainAllInOrder(
"PARAMETERS",
"foo", "Choices:", "One", "Two", "Three",
"OPTIONS",
"--bar", "Choices:", "One", "Two", "Three"
);
}
[Fact]
public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback()
{

View File

@@ -16,7 +16,7 @@
public string ExecutableName { get; }
/// <summary>
/// Application version text.
/// Application version.
/// </summary>
public string Version { get; }

View File

@@ -6,7 +6,7 @@ namespace CliFx.Attributes
/// Annotates a type that defines a command.
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class CommandAttribute : Attribute
public sealed class CommandAttribute : Attribute
{
/// <summary>
/// Command's name.

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command option.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandOptionAttribute : Attribute
public sealed class CommandOptionAttribute : Attribute
{
/// <summary>
/// Option name.

View File

@@ -7,7 +7,7 @@ namespace CliFx.Attributes
/// Annotates a property that defines a command parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class CommandParameterAttribute : Attribute
public sealed class CommandParameterAttribute : Attribute
{
/// <summary>
/// Parameter order.

View File

@@ -99,7 +99,7 @@ namespace CliFx
// Handle preview directive
if (IsPreviewModeEnabled(commandInput))
{
_console.WriteCommandInput(commandInput);
_console.Output.WriteCommandInput(commandInput);
return 0;
}
@@ -125,7 +125,7 @@ namespace CliFx
// Handle help option
if (ShouldShowHelpText(commandSchema, commandInput))
{
_console.WriteHelpText(helpContext);
_console.Output.WriteHelpText(helpContext);
return 0;
}
@@ -150,12 +150,12 @@ namespace CliFx
}
catch (CliFxException ex)
{
_console.WriteException(ex);
_console.Error.WriteException(ex);
if (ex.ShowHelp)
{
_console.Output.WriteLine();
_console.WriteHelpText(helpContext);
_console.Output.WriteHelpText(helpContext);
}
return ex.ExitCode;
@@ -200,7 +200,7 @@ namespace CliFx
// developer, so we don't swallow them in that case.
catch (Exception ex) when (!Debugger.IsAttached)
{
_console.WriteException(ex);
_console.Error.WriteException(ex);
return 1;
}
}

View File

@@ -37,7 +37,7 @@ namespace CliFx
}
/// <summary>
/// Adds a command the application.
/// Adds a command to the application.
/// </summary>
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
AddCommand(typeof(TCommand));

View File

@@ -28,20 +28,10 @@
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
</ItemGroup>
<!-- The following elements are responsible for embedding the analyzer assembly within the output NuGet package -->
<!-- Pack the analyzer assembly inside the package -->
<ItemGroup>
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="true" IncludeAssets="CliFx.Analyzers.dll" />
<ProjectReference Include="../CliFx.Analyzers/CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
<None Include="../CliFx.Analyzers/bin/$(Configuration)/netstandard2.0/CliFx.Analyzers.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<PropertyGroup>
<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);CopyAnalyzerToPackage</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>
<Target Name="CopyAnalyzerToPackage">
<ItemGroup>
<TfmSpecificPackageFile Include="$(OutDir)/CliFx.Analyzers.dll" PackagePath="analyzers/dotnet/cs" BuildAction="none" />
</ItemGroup>
</Target>
</Project>

View File

@@ -1,5 +1,4 @@
using System;
using System.Linq;
using CliFx.Infrastructure;
using CliFx.Input;
@@ -47,9 +46,9 @@ namespace CliFx.Formatting
foreach (var value in optionInput.Values)
{
Write(' ');
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(value);
Write(ConsoleColor.DarkGray, '"');
Write('"');
}
Write(']');
@@ -75,9 +74,9 @@ namespace CliFx.Formatting
Write('=');
// Value
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(environmentVariableInput.Value);
Write(ConsoleColor.DarkGray, '"');
Write('"');
WriteLine();
}
@@ -93,7 +92,7 @@ namespace CliFx.Formatting
internal static class CommandInputConsoleFormatterExtensions
{
public static void WriteCommandInput(this IConsole console, CommandInput commandInput) =>
new CommandInputConsoleFormatter(console.Output).WriteCommandInput(commandInput);
public static void WriteCommandInput(this ConsoleWriter consoleWriter, CommandInput commandInput) =>
new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput);
}
}

View File

@@ -19,7 +19,7 @@ namespace CliFx.Formatting
Write("at ");
// Fully qualified method name
Write(ConsoleColor.DarkGray, stackFrame.ParentType + '.');
Write(stackFrame.ParentType + '.');
Write(ConsoleColor.Yellow, stackFrame.MethodName);
// Method parameters
@@ -60,7 +60,7 @@ namespace CliFx.Formatting
Write("in ");
// File path
Write(ConsoleColor.DarkGray, stackFrameDirectoryPath);
Write(stackFrameDirectoryPath);
Write(ConsoleColor.Yellow, stackFrameFileName);
// Source position
@@ -80,7 +80,7 @@ namespace CliFx.Formatting
// Fully qualified exception type
var exceptionType = exception.GetType();
Write(ConsoleColor.DarkGray, exceptionType.Namespace + '.');
Write(exceptionType.Namespace + '.');
Write(ConsoleColor.White, exceptionType.Name);
Write(": ");
@@ -129,7 +129,7 @@ namespace CliFx.Formatting
internal static class ExceptionConsoleFormatterExtensions
{
public static void WriteException(this IConsole console, Exception exception) =>
new ExceptionConsoleFormatter(console.Error).WriteException(exception);
public static void WriteException(this ConsoleWriter consoleWriter, Exception exception) =>
new ExceptionConsoleFormatter(consoleWriter).WriteException(exception);
}
}

View File

@@ -27,7 +27,7 @@ namespace CliFx.Formatting
private void WriteCommandInvocation()
{
Write(ConsoleColor.DarkGray, _context.ApplicationMetadata.ExecutableName);
Write(_context.ApplicationMetadata.ExecutableName);
// Command name
if (!string.IsNullOrWhiteSpace(_context.CommandSchema.Name))
@@ -190,9 +190,9 @@ namespace CliFx.Formatting
Write(", ");
}
Write(ConsoleColor.DarkGray, '"');
Write(ConsoleColor.White, validValue.ToString());
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
@@ -269,9 +269,9 @@ namespace CliFx.Formatting
Write(", ");
}
Write(ConsoleColor.DarkGray, '"');
Write(ConsoleColor.White, validValue.ToString());
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(validValue.ToString());
Write('"');
}
Write('.');
@@ -317,10 +317,12 @@ namespace CliFx.Formatting
Write(", ");
}
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(element.ToString(CultureInfo.InvariantCulture));
Write(ConsoleColor.DarkGray, '"');
Write('"');
}
Write('.');
}
}
else
@@ -329,13 +331,12 @@ namespace CliFx.Formatting
{
Write(ConsoleColor.White, "Default: ");
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write(defaultValue.ToString(CultureInfo.InvariantCulture));
Write(ConsoleColor.DarkGray, '"');
Write('"');
Write('.');
}
}
Write('.');
}
}
@@ -443,7 +444,7 @@ namespace CliFx.Formatting
internal static class HelpConsoleFormatterExtensions
{
public static void WriteHelpText(this IConsole console, HelpContext context) =>
new HelpConsoleFormatter(console.Output, context).WriteHelpText();
public static void WriteHelpText(this ConsoleWriter consoleWriter, HelpContext context) =>
new HelpConsoleFormatter(consoleWriter, context).WriteHelpText();
}
}

View File

@@ -17,7 +17,7 @@ namespace CliFx.Infrastructure
/// Initializes an instance of <see cref="ConsoleReader"/>.
/// </summary>
public ConsoleReader(IConsole console, Stream stream, Encoding encoding)
: base(stream, encoding, false)
: base(stream, encoding, false, 4096)
{
Console = console;
}
@@ -33,7 +33,11 @@ namespace CliFx.Infrastructure
public partial class ConsoleReader
{
internal static ConsoleReader Create(IConsole console, Stream? stream) =>
new(console, stream is not null ? Stream.Synchronized(stream) : Stream.Null);
internal static ConsoleReader Create(IConsole console, Stream? stream) => new(
console,
stream is not null
? Stream.Synchronized(stream)
: Stream.Null
);
}
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using System.Text;
using CliFx.Utils;
namespace CliFx.Infrastructure
{
@@ -17,7 +18,7 @@ namespace CliFx.Infrastructure
/// Initializes an instance of <see cref="ConsoleWriter"/>.
/// </summary>
public ConsoleWriter(IConsole console, Stream stream, Encoding encoding)
: base(stream, encoding)
: base(stream, encoding, 256)
{
Console = console;
}
@@ -26,14 +27,18 @@ namespace CliFx.Infrastructure
/// Initializes an instance of <see cref="ConsoleWriter"/>.
/// </summary>
public ConsoleWriter(IConsole console, Stream stream)
: this(console, stream, System.Console.OutputEncoding)
: this(console, stream, System.Console.OutputEncoding.WithoutPreamble())
{
}
}
public partial class ConsoleWriter
{
internal static ConsoleWriter Create(IConsole console, Stream? stream) =>
new(console, stream is not null ? Stream.Synchronized(stream) : Stream.Null) {AutoFlush = true};
internal static ConsoleWriter Create(IConsole console, Stream? stream) => new(
console,
stream is not null
? Stream.Synchronized(stream)
: Stream.Null
) {AutoFlush = true};
}
}

View File

@@ -70,12 +70,16 @@ namespace CliFx.Infrastructure
/// Subsequent calls to this method have no side-effects and return the same token.
/// </summary>
/// <remarks>
/// <para>
/// Calling this method effectively makes the command cancellation-aware, which
/// means that sending an interrupt signal won't immediately terminate the application,
/// means that sending the interrupt signal won't immediately terminate the application,
/// but will instead trigger a token that the command can use to exit more gracefully.
///
/// If the user sends a second interrupt signal after the first one, the application
/// will terminate immediately.
/// </para>
/// <para>
/// Note that the handler is only respected when the user sends the interrupt signal for the first time.
/// If the user decides to issue the signal again, the application will terminate immediately
/// regardless of whether the command is cancellation-aware.
/// </para>
/// </remarks>
CancellationToken RegisterCancellationHandler();
}
@@ -116,16 +120,10 @@ namespace CliFx.Infrastructure
public static IDisposable WithColors(
this IConsole console,
ConsoleColor foregroundColor,
ConsoleColor backgroundColor)
{
var foregroundColorRegistration = console.WithForegroundColor(foregroundColor);
var backgroundColorRegistration = console.WithBackgroundColor(backgroundColor);
return Disposable.Create(() =>
{
foregroundColorRegistration.Dispose();
backgroundColorRegistration.Dispose();
});
}
ConsoleColor backgroundColor) =>
Disposable.Merge(
console.WithForegroundColor(foregroundColor),
console.WithBackgroundColor(backgroundColor)
);
}
}

View File

@@ -21,7 +21,20 @@ namespace CliFx.Schema
public IReadOnlyList<object?> GetValidValues()
{
var underlyingType = Type.TryGetNullableUnderlyingType() ?? Type;
static Type GetUnderlyingType(Type type)
{
var enumerableUnderlyingType = type.TryGetEnumerableUnderlyingType();
if (enumerableUnderlyingType is not null)
return GetUnderlyingType(enumerableUnderlyingType);
var nullableUnderlyingType = type.TryGetNullableUnderlyingType();
if (nullableUnderlyingType is not null)
return GetUnderlyingType(nullableUnderlyingType);
return type;
}
var underlyingType = GetUnderlyingType(Type);
// We can only get valid values for enums
if (underlyingType.IsEnum)

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace CliFx.Utils
{
@@ -14,5 +15,14 @@ namespace CliFx.Utils
internal partial class Disposable
{
public static IDisposable Create(Action dispose) => new Disposable(dispose);
public static IDisposable Merge(IEnumerable<IDisposable> disposables) => Create(() =>
{
foreach (var disposable in disposables)
disposable.Dispose();
});
public static IDisposable Merge(params IDisposable[] disposables) =>
Merge((IEnumerable<IDisposable>) disposables);
}
}

View File

@@ -54,9 +54,11 @@ namespace CliFx.Utils.Extensions
return array;
}
public static bool IsToStringOverriden(this Type type) =>
type.GetMethod(nameof(ToString), Type.EmptyTypes) !=
typeof(object).GetMethod(nameof(ToString), Type.EmptyTypes);
public static bool IsToStringOverriden(this Type type)
{
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);
return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType;
}
// Types supported by `Convert.ChangeType(...)`
private static readonly HashSet<Type> ConvertibleTypes = new()

View File

@@ -0,0 +1,51 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace CliFx.Utils
{
// Adapted from:
// https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Common/src/System/Text/ConsoleEncoding.cs
internal class NoPreambleEncoding : Encoding
{
private readonly Encoding _underlyingEncoding;
public NoPreambleEncoding(Encoding underlyingEncoding) =>
_underlyingEncoding = underlyingEncoding;
public override byte[] GetPreamble() =>
Array.Empty<byte>();
[ExcludeFromCodeCoverage]
public override int GetByteCount(char[] chars, int index, int count) =>
_underlyingEncoding.GetByteCount(chars, index, count);
[ExcludeFromCodeCoverage]
public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex) =>
_underlyingEncoding.GetBytes(chars, charIndex, charCount, bytes, byteIndex);
[ExcludeFromCodeCoverage]
public override int GetCharCount(byte[] bytes, int index, int count) =>
_underlyingEncoding.GetCharCount(bytes, index, count);
[ExcludeFromCodeCoverage]
public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) =>
_underlyingEncoding.GetChars(bytes, byteIndex, byteCount, chars, charIndex);
[ExcludeFromCodeCoverage]
public override int GetMaxByteCount(int charCount) =>
_underlyingEncoding.GetMaxByteCount(charCount);
[ExcludeFromCodeCoverage]
public override int GetMaxCharCount(int byteCount) =>
_underlyingEncoding.GetMaxCharCount(byteCount);
}
internal static class NoPreambleEncodingExtensions
{
public static Encoding WithoutPreamble(this Encoding encoding) =>
encoding.GetPreamble().Length > 0
? new NoPreambleEncoding(encoding)
: encoding;
}
}

View File

@@ -24,9 +24,6 @@ internal static partial class PolyfillExtensions
key = pair.Key;
value = pair.Value;
}
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
dic.TryGetValue(key!, out var result) ? result! : default!;
}
internal static partial class PolyfillExtensions
@@ -44,4 +41,13 @@ namespace System.Linq
new(source, comparer);
}
}
namespace System.Collections.Generic
{
internal static class PolyfillExtensions
{
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
dic.TryGetValue(key!, out var result) ? result! : default!;
}
}
#endif

View File

@@ -91,12 +91,7 @@ namespace CliFx.Utils
{
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
// Ensure success (all lines should be parsed)
var isSuccess =
matches.Length ==
stackTrace.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length;
if (!isSuccess)
if (matches.Length <= 0 || matches.Any(m => !m.Success))
{
// If parsing fails, we include the original stacktrace in the
// exception so that it's shown to the user.

View File

@@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>2.0</Version>
<Version>2.0.4</Version>
<Company>Tyrrrz</Company>
<Copyright>Copyright (C) Alexey Golub</Copyright>
<LangVersion>latest</LangVersion>

7
NuGet.config Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>

146
Readme.md
View File

@@ -53,7 +53,7 @@ public static class Program
}
```
> Note: ensure that your `Main()` method returns the integer exit code provided by `CliApplication.RunAsync()`, as shown in the above example.
> ⚠️ Ensure that your `Main()` method returns the integer exit code provided by `CliApplication.RunAsync()`, as shown in the above example.
Exit code is used to communicate execution result to the parent process, so it's important that your program returns it.
The code above calls `AddCommandsFromThisAssembly()` to scan and resolve command types defined within the current assembly.
@@ -97,10 +97,10 @@ They can be used to show help text or application version respectively:
MyApp v1.0
Usage
USAGE
dotnet myapp.dll [options]
Options
OPTIONS
-h|--help Shows help text.
--version Shows version information.
```
@@ -129,7 +129,8 @@ public class LogCommand : ICommand
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
public double Value { get; init; }
// Name: --base | Short name: -b
// Name: --base
// Short name: -b
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; init; } = 10;
@@ -175,13 +176,13 @@ Available parameters and options are also listed in the command's help text, whi
MyApp v1.0
Usage
USAGE
dotnet myapp.dll <value> [options]
Parameters
PARAMETERS
* value Value whose logarithm is to be found.
Options
OPTIONS
-b|--base Logarithm base. Default: "10".
-h|--help Shows help text.
--version Shows version information.
@@ -244,61 +245,9 @@ Parameters and options can have the following underlying types:
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
- Types with a constructor accepting an array (`List<T>`, `HashSet<T>`, etc.)
- Example command with a custom converter:
#### Non-scalar parameters and options
```csharp
// Maps 2D vectors from AxB notation
public class VectorConverter : BindingConverter<Vector2>
{
public override Vector2 Convert(string? rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
return default;
var components = rawValue.Split('x', 'X', ';');
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
return new Vector2(x, y);
}
}
[Command]
public class SurfaceCalculatorCommand : ICommand
{
// Custom converter is used to map raw argument values
[CommandParameter(0, Converter = typeof(VectorConverter))]
public Vector2 PointA { get; init; }
[CommandParameter(1, Converter = typeof(VectorConverter))]
public Vector2 PointB { get; init; }
[CommandParameter(2, Converter = typeof(VectorConverter))]
public Vector2 PointC { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
var a = (PointB - PointA).Length();
var b = (PointC - PointB).Length();
var c = (PointA - PointC).Length();
var p = (a + b + c) / 2;
var surface = Math.Sqrt(p * (p - a) * (p - b) * (p - c));
console.Output.WriteLine($"Triangle surface area: {surface}");
return default;
}
}
```
```sh
> dotnet myapp.dll 0x0 0x18 24x0
Triangle surface area: 216
```
- Example command with an array-backed parameter:
Here's an example of a command with an array-backed parameter:
```csharp
[Command]
@@ -352,6 +301,56 @@ public class FileSizeCalculatorCommand : ICommand
Total file size: 186368 bytes
```
#### Custom conversion
To create a custom converter for a parameter or an option, define a class that inherits from `BindingConverter<T>` and specify it in the attribute:
```csharp
// Maps 2D vectors from AxB notation
public class VectorConverter : BindingConverter<Vector2>
{
public override Vector2 Convert(string? rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
return default;
var components = rawValue.Split('x', 'X', ';');
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
return new Vector2(x, y);
}
}
[Command]
public class SurfaceCalculatorCommand : ICommand
{
// Custom converter is used to map raw argument values
[CommandParameter(0, Converter = typeof(VectorConverter))]
public Vector2 PointA { get; init; }
[CommandParameter(1, Converter = typeof(VectorConverter))]
public Vector2 PointB { get; init; }
[CommandParameter(2, Converter = typeof(VectorConverter))]
public Vector2 PointC { get; init; }
public ValueTask ExecuteAsync(IConsole console)
{
var a = (PointB - PointA).Length();
var b = (PointC - PointB).Length();
var c = (PointA - PointC).Length();
var p = (a + b + c) / 2;
var surface = Math.Sqrt(p * (p - a) * (p - b) * (p - c));
console.Output.WriteLine($"Triangle surface area: {surface}");
return default;
}
}
```
### Multiple commands
In order to facilitate a variety of different workflows, command line applications may provide the user with more than just a single command.
@@ -400,15 +399,15 @@ Requesting help will show direct subcommands of the current command:
MyApp v1.0
Usage
USAGE
dotnet myapp.dll [options]
dotnet myapp.dll [command] [...]
Options
OPTIONS
-h|--help Shows help text.
--version Shows version information.
Commands
COMMANDS
cmd1 Subcommands: cmd1 sub.
cmd2
@@ -420,21 +419,21 @@ The user can also refine their help request by querying it on a specific command
```sh
> dotnet myapp.dll cmd1 --help
Usage
USAGE
dotnet myapp.dll cmd1 [options]
dotnet myapp.dll cmd1 [command] [...]
Options
OPTIONS
-h|--help Shows help text.
Commands
COMMANDS
sub
You can run `dotnet myapp.dll cmd1 [command] --help` to show help on a specific command.
```
> Note that defining a default (unnamed) command is not required.
In the even of its absence, running the application without specifying a command will just show root level help text.
If it's absent, running the application without specifying a command will just show root level help text.
### Reporting errors
@@ -478,7 +477,8 @@ Division by zero is not supported.
133
```
> Note that Unix systems rely on 8-bit unsigned integers to represent exit codes, which means that you can only use values between `1` and `255` to indicate an unsuccessful execution result.
> ⚠️ Even though exit codes are represented by 32-bit integers in .NET, using values outside of 8-bit unsigned range will cause an overflow on Unix systems.
To avoid unexpected results, use numbers between 1 and 255 for exit codes that indicate failure.
### Graceful cancellation
@@ -511,7 +511,7 @@ public class CancellableCommand : ICommand
```
> Note that a command may use this approach to delay cancellation only once.
If the user issues a second interrupt signal, the application will be immediately terminated.
If the user issues a second interrupt signal, the application will be terminated immediately.
### Type activation
@@ -521,7 +521,7 @@ To facilitate that, it uses an interface called `ITypeActivator` that determines
The default implementation of `ITypeActivator` only supports types that have public parameterless constructors, which is sufficient for majority of scenarios.
However, in some cases you may also want to define a custom initializer, for example when integrating with an external dependency container.
The following snippet shows how to configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) as the type activator:
The following example shows how to configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) as the type activator in CliFx:
```csharp
public static class Program
@@ -632,7 +632,7 @@ public async Task ConcatCommand_executes_successfully()
### Debug and preview mode
When troubleshooting issues, you may find it useful to run your app in debug or preview mode.
To do that, you need to pass pass the corresponding directive before any other arguments.
To do that, you need to pass the corresponding directive before any other arguments.
In order to run the application in debug mode, use the `[debug]` directive.
This will cause the program to launch in a suspended state, waiting for debugger to be attached to the process:
@@ -696,7 +696,7 @@ public class AuthCommand : ICommand
test
```
Environment variables can be configured for options of non-scalar types as well.
Environment variables can be configured for options of non-scalar types (arrays, lists, etc.) as well.
In such case, the values of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Linux).
## Etymology