mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51cca36d2a | ||
|
|
84672c92f6 | ||
|
|
b1d01898b6 | ||
|
|
441a47a1a8 | ||
|
|
8abd7219a1 | ||
|
|
df73a0bfe8 | ||
|
|
55d12dc721 | ||
|
|
a6ee44c1bb | ||
|
|
76816e22f1 | ||
|
|
daf25e59d6 | ||
|
|
f2b4e53615 | ||
|
|
2d519ab190 | ||
|
|
2d479c9cb6 | ||
|
|
2bb7e13e51 | ||
|
|
6e1dfdcdd4 | ||
|
|
5ba647e5c1 | ||
|
|
853492695f | ||
|
|
d5d72c7c50 | ||
|
|
d676b5832e | ||
|
|
28097afc1e | ||
|
|
fda96586f3 | ||
|
|
fc5af8dbbc | ||
|
|
4835e64388 | ||
|
|
0999c33f93 | ||
|
|
595805255a | ||
|
|
65eaa912cf | ||
|
|
038f48b78e | ||
|
|
d7460244b7 | ||
|
|
02766868fc | ||
|
|
8d7d25a144 | ||
|
|
17ded54e24 | ||
|
|
54a4c32ddf | ||
|
|
6d46e82145 | ||
|
|
fd4a2a18fe | ||
|
|
bfe99d620e | ||
|
|
c5a111207f | ||
|
|
544945c0e6 | ||
|
|
c616cdd750 | ||
|
|
d3c396956d | ||
|
|
d0cbbc6d9a |
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report broken functionality.
|
||||||
|
labels: [bug]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
🧐 **Guidelines:**
|
||||||
|
|
||||||
|
- Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this bug has not been reported before.
|
||||||
|
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Something's not working" or "A couple of problems".
|
||||||
|
- Keep your issue focused on one single problem. If you have multiple bug reports, please create separate issues for each of them.
|
||||||
|
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
|
||||||
|
- If you want to ask a question instead of reporting a bug, please use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
description: Which version of CliFx does this bug affect?
|
||||||
|
placeholder: ver X.Y.Z
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Details
|
||||||
|
description: Clear and thorough explanation of the bug.
|
||||||
|
placeholder: I was doing X expecting Y to happen, but Z happened instead.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Minimum steps required to reproduce the bug.
|
||||||
|
placeholder: |
|
||||||
|
- Step 1
|
||||||
|
- Step 2
|
||||||
|
- Step 3
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/Tyrrrz/CliFx/discussions/new
|
||||||
|
about: Ask and answer questions.
|
||||||
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: ✨ Feature request
|
||||||
|
description: Request a new feature.
|
||||||
|
labels: [enhancement]
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
🧐 **Guidelines:**
|
||||||
|
|
||||||
|
- Search through [existing issues](https://github.com/Tyrrrz/CliFx/issues?q=is%3Aissue) first to ensure that this feature has not been requested before.
|
||||||
|
- Write a descriptive title for your issue. Avoid generic or vague titles such as "Some suggestions" or "Ideas for improvement".
|
||||||
|
- Keep your issue focused on one single problem. If you have multiple feature requests, please create separate issues for each of them.
|
||||||
|
- Provide as much context as possible in the details section. Include screenshots, screen recordings, links, references, or anything else you may consider relevant.
|
||||||
|
- If you want to ask a question instead of requesting a feature, please 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
|
||||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
|
||||||
|
Please make sure that there is an existing issue that describes the problem solved by your pull request. If there isn't one, consider creating it first.
|
||||||
|
An open issue offers a good place to iron out requirements, discuss possible solutions, and ask questions.
|
||||||
|
|
||||||
|
Remember to also:
|
||||||
|
|
||||||
|
- Keep your pull request focused and as small as possible. If you want to contribute multiple unrelated changes, please create separate pull requests for them.
|
||||||
|
- Follow the coding style and conventions already established by the project. When in doubt about which style to use, ask in the comments to your pull request.
|
||||||
|
- If you want to start a discussion regarding a specific change you've made, add a review comment to your own code. This can be used to highlight something important or to seek further input from others.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Please specify the issue addressed by this pull request -->
|
||||||
|
Closes #ISSUE_NUMBER
|
||||||
4
.github/workflows/CD.yml
vendored
4
.github/workflows/CD.yml
vendored
@@ -19,9 +19,7 @@ jobs:
|
|||||||
dotnet-version: 5.0.x
|
dotnet-version: 5.0.x
|
||||||
|
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: |
|
run: dotnet pack CliFx --configuration Release
|
||||||
dotnet nuget locals all --clear
|
|
||||||
dotnet pack CliFx --configuration Release
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}
|
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}
|
||||||
|
|||||||
4
.github/workflows/CI.yml
vendored
4
.github/workflows/CI.yml
vendored
@@ -20,9 +20,7 @@ jobs:
|
|||||||
dotnet-version: 5.0.x
|
dotnet-version: 5.0.x
|
||||||
|
|
||||||
- name: Build & test
|
- name: Build & test
|
||||||
run: |
|
run: dotnet test --configuration Release --logger GitHubActions
|
||||||
dotnet nuget locals all --clear
|
|
||||||
dotnet test --configuration Release --logger GitHubActions
|
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1.0.5
|
uses: codecov/codecov-action@v1.0.5
|
||||||
|
|||||||
30
Changelog.md
30
Changelog.md
@@ -1,3 +1,33 @@
|
|||||||
|
### v2.0.6 (17-Jul-2021)
|
||||||
|
|
||||||
|
- Fixed an issue where an exception thrown via reflection during parameter or option binding resulted in `Exception has been thrown by the target of an invocation` error instead of a more useful message. Such exceptions will now be unwrapped to provide better user experience.
|
||||||
|
|
||||||
|
### v2.0.5 (09-Jul-2021)
|
||||||
|
|
||||||
|
- Fixed an issue where calling `IConsole.Output.Encoding.EncodingName` and some other members threw an exception.
|
||||||
|
- Added readme file to the package.
|
||||||
|
|
||||||
|
### 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)
|
### v2.0 (21-Mar-2021)
|
||||||
|
|
||||||
> Note: this major release includes many breaking changes.
|
> Note: this major release includes many breaking changes.
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Basic.Reference.Assemblies" Version="1.1.2" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<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="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.0" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
||||||
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
|
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -104,5 +104,24 @@ public class MyCommand : ICommand
|
|||||||
// Act & assert
|
// Act & assert
|
||||||
Analyzer.Should().NotProduceDiagnostics(code);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Basic.Reference.Assemblies;
|
||||||
using FluentAssertions.Execution;
|
using FluentAssertions.Execution;
|
||||||
using FluentAssertions.Primitives;
|
using FluentAssertions.Primitives;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
@@ -58,14 +58,8 @@ namespace CliFx.Analyzers.Tests.Utils
|
|||||||
var compilation = CSharpCompilation.Create(
|
var compilation = CSharpCompilation.Create(
|
||||||
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
||||||
new[] {ast},
|
new[] {ast},
|
||||||
new[]
|
ReferenceAssemblies.Net50
|
||||||
{
|
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
|
||||||
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
|
|
||||||
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)
|
|
||||||
},
|
|
||||||
// DLL to avoid having to define the Main() method
|
// DLL to avoid having to define the Main() method
|
||||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (property.ContainingType.IsAbstract)
|
if (property.ContainingType.IsAbstract)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
if (option is null)
|
if (option is null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
var option = CommandOptionSymbol.TryResolve(property);
|
var option = CommandOptionSymbol.TryResolve(property);
|
||||||
if (option is null)
|
if (option is null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (property.ContainingType.IsAbstract)
|
if (property.ContainingType.IsAbstract)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (IsScalar(property.Type))
|
if (IsScalar(property.Type))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!CommandParameterSymbol.IsParameterProperty(property))
|
if (!CommandParameterSymbol.IsParameterProperty(property))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
if (parameter is null)
|
if (parameter is null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
|||||||
PropertyDeclarationSyntax propertyDeclaration,
|
PropertyDeclarationSyntax propertyDeclaration,
|
||||||
IPropertySymbol property)
|
IPropertySymbol property)
|
||||||
{
|
{
|
||||||
|
if (property.ContainingType is null)
|
||||||
|
return;
|
||||||
|
|
||||||
var parameter = CommandParameterSymbol.TryResolve(property);
|
var parameter = CommandParameterSymbol.TryResolve(property);
|
||||||
if (parameter is null)
|
if (parameter is null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
while (currentNode is MemberAccessExpressionSyntax memberAccess)
|
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;
|
return memberAccess;
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,8 @@ namespace CliFx.Analyzers
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Check if IConsole is available in scope as an alternative to System.Console
|
// Check if IConsole is available in scope as an alternative to System.Console
|
||||||
var isConsoleInterfaceAvailable = context.Node
|
var isConsoleInterfaceAvailable = context
|
||||||
|
.Node
|
||||||
.Ancestors()
|
.Ancestors()
|
||||||
.OfType<MethodDeclarationSyntax>()
|
.OfType<MethodDeclarationSyntax>()
|
||||||
.SelectMany(m => m.ParameterList.Parameters)
|
.SelectMany(m => m.ParameterList.Parameters)
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ namespace CliFx.Analyzers.Utils.Extensions
|
|||||||
internal static class RoslynExtensions
|
internal static class RoslynExtensions
|
||||||
{
|
{
|
||||||
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
|
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(
|
public static void HandleClassDeclaration(
|
||||||
this AnalysisContext analysisContext,
|
this AnalysisContext analysisContext,
|
||||||
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> handler)
|
Action<SyntaxNodeAnalysisContext, ClassDeclarationSyntax, ITypeSymbol> analyze)
|
||||||
{
|
{
|
||||||
analysisContext.RegisterSyntaxNodeAction(ctx =>
|
analysisContext.RegisterSyntaxNodeAction(ctx =>
|
||||||
{
|
{
|
||||||
@@ -24,13 +29,13 @@ namespace CliFx.Analyzers.Utils.Extensions
|
|||||||
if (type is null)
|
if (type is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
handler(ctx, classDeclaration, type);
|
analyze(ctx, classDeclaration, type);
|
||||||
}, SyntaxKind.ClassDeclaration);
|
}, SyntaxKind.ClassDeclaration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void HandlePropertyDeclaration(
|
public static void HandlePropertyDeclaration(
|
||||||
this AnalysisContext analysisContext,
|
this AnalysisContext analysisContext,
|
||||||
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> handler)
|
Action<SyntaxNodeAnalysisContext, PropertyDeclarationSyntax, IPropertySymbol> analyze)
|
||||||
{
|
{
|
||||||
analysisContext.RegisterSyntaxNodeAction(ctx =>
|
analysisContext.RegisterSyntaxNodeAction(ctx =>
|
||||||
{
|
{
|
||||||
@@ -41,7 +46,7 @@ namespace CliFx.Analyzers.Utils.Extensions
|
|||||||
if (property is null)
|
if (property is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
handler(ctx, propertyDeclaration, property);
|
analyze(ctx, propertyDeclaration, property);
|
||||||
}, SyntaxKind.PropertyDeclaration);
|
}, SyntaxKind.PropertyDeclaration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace CliFx.Benchmarks
|
|||||||
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
|
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
|
||||||
DefaultConfig
|
DefaultConfig
|
||||||
.Instance
|
.Instance
|
||||||
.With(ConfigOptions.DisableOptimizationsValidator)
|
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||||
<PackageReference Include="clipr" Version="1.6.1" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="Cocona" Version="1.5.0" />
|
<PackageReference Include="Cocona" Version="1.5.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace CliFx.Demo.Domain
|
|||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
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);
|
public Book? TryGetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="3.3.1" />
|
<PackageReference Include="Basic.Reference.Assemblies" Version="1.1.2" />
|
||||||
|
<PackageReference Include="CliWrap" Version="3.3.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.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" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
||||||
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
|
<PackageReference Include="coverlet.msbuild" Version="3.0.3" PrivateAssets="all" />
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CliFx.Infrastructure;
|
||||||
using CliFx.Tests.Utils;
|
using CliFx.Tests.Utils;
|
||||||
using CliWrap;
|
using CliWrap;
|
||||||
using CliWrap.Buffered;
|
using CliWrap.Buffered;
|
||||||
@@ -135,5 +138,22 @@ public class Command : ICommand
|
|||||||
stdOut.Trim().Should().Be("Hello world");
|
stdOut.Trim().Should().Be("Hello world");
|
||||||
stdErr.Trim().Should().Be("Hello world");
|
stdErr.Trim().Should().Be("Hello world");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Console_does_not_emit_preamble_when_used_with_encoding_that_has_it()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using var buffer = new MemoryStream();
|
||||||
|
using var consoleWriter = new ConsoleWriter(FakeConsole, buffer, Encoding.UTF8);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
consoleWriter.Write("Hello world");
|
||||||
|
consoleWriter.Flush();
|
||||||
|
|
||||||
|
var output = consoleWriter.Encoding.GetString(buffer.ToArray());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
output.Should().Be("Hello world");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -946,5 +946,48 @@ public class Command : ICommand
|
|||||||
exitCode.Should().NotBe(0);
|
exitCode.Should().NotBe(0);
|
||||||
stdErr.Should().Contain("Hello world");
|
stdErr.Should().Contain("Hello world");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Parameter_or_option_value_conversion_fails_if_the_static_parse_method_throws()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// language=cs
|
||||||
|
@"
|
||||||
|
public class CustomType
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
private CustomType(string value) => Value = value;
|
||||||
|
|
||||||
|
public static CustomType Parse(string value) => throw new Exception(""Hello world"");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class Command : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption('f')]
|
||||||
|
public CustomType Foo { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
|
}
|
||||||
|
");
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand(commandType)
|
||||||
|
.UseConsole(FakeConsole)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(
|
||||||
|
new[] {"-f", "bar"},
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
var stdErr = FakeConsole.ReadErrorString();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdErr.Should().Contain("Hello world");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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]
|
[Fact]
|
||||||
public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback()
|
public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using Basic.Reference.Assemblies;
|
||||||
using Microsoft.CodeAnalysis;
|
using Microsoft.CodeAnalysis;
|
||||||
using Microsoft.CodeAnalysis.CSharp;
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
using Microsoft.CodeAnalysis.Text;
|
using Microsoft.CodeAnalysis.Text;
|
||||||
@@ -60,16 +61,9 @@ namespace CliFx.Tests.Utils
|
|||||||
var compilation = CSharpCompilation.Create(
|
var compilation = CSharpCompilation.Create(
|
||||||
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
||||||
new[] {ast},
|
new[] {ast},
|
||||||
new[]
|
ReferenceAssemblies.Net50
|
||||||
{
|
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
|
||||||
MetadataReference.CreateFromFile(Assembly.Load("netstandard").Location),
|
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)),
|
||||||
MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location),
|
|
||||||
MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)
|
|
||||||
},
|
|
||||||
// DLL to avoid having to define the Main() method
|
// DLL to avoid having to define the Main() method
|
||||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
public string ExecutableName { get; }
|
public string ExecutableName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Application version text.
|
/// Application version.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Version { get; }
|
public string Version { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace CliFx.Attributes
|
|||||||
/// Annotates a type that defines a command.
|
/// Annotates a type that defines a command.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||||
public class CommandAttribute : Attribute
|
public sealed class CommandAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Command's name.
|
/// Command's name.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace CliFx.Attributes
|
|||||||
/// Annotates a property that defines a command option.
|
/// Annotates a property that defines a command option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandOptionAttribute : Attribute
|
public sealed class CommandOptionAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option name.
|
/// Option name.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace CliFx.Attributes
|
|||||||
/// Annotates a property that defines a command parameter.
|
/// Annotates a property that defines a command parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandParameterAttribute : Attribute
|
public sealed class CommandParameterAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parameter order.
|
/// Parameter order.
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ namespace CliFx
|
|||||||
// Handle preview directive
|
// Handle preview directive
|
||||||
if (IsPreviewModeEnabled(commandInput))
|
if (IsPreviewModeEnabled(commandInput))
|
||||||
{
|
{
|
||||||
_console.WriteCommandInput(commandInput);
|
_console.Output.WriteCommandInput(commandInput);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ namespace CliFx
|
|||||||
// Handle help option
|
// Handle help option
|
||||||
if (ShouldShowHelpText(commandSchema, commandInput))
|
if (ShouldShowHelpText(commandSchema, commandInput))
|
||||||
{
|
{
|
||||||
_console.WriteHelpText(helpContext);
|
_console.Output.WriteHelpText(helpContext);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +150,12 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
catch (CliFxException ex)
|
catch (CliFxException ex)
|
||||||
{
|
{
|
||||||
_console.WriteException(ex);
|
_console.Error.WriteException(ex);
|
||||||
|
|
||||||
if (ex.ShowHelp)
|
if (ex.ShowHelp)
|
||||||
{
|
{
|
||||||
_console.Output.WriteLine();
|
_console.Output.WriteLine();
|
||||||
_console.WriteHelpText(helpContext);
|
_console.Output.WriteHelpText(helpContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ex.ExitCode;
|
return ex.ExitCode;
|
||||||
@@ -200,7 +200,7 @@ namespace CliFx
|
|||||||
// developer, so we don't swallow them in that case.
|
// developer, so we don't swallow them in that case.
|
||||||
catch (Exception ex) when (!Debugger.IsAttached)
|
catch (Exception ex) when (!Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
_console.WriteException(ex);
|
_console.Error.WriteException(ex);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a command the application.
|
/// Adds a command to the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
|
public CliApplicationBuilder AddCommand<TCommand>() where TCommand : ICommand =>
|
||||||
AddCommand(typeof(TCommand));
|
AddCommand(typeof(TCommand));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags>
|
<PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags>
|
||||||
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
|
||||||
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
|
||||||
|
<PackageReadmeFile>Readme.md</PackageReadmeFile>
|
||||||
<PackageIcon>favicon.png</PackageIcon>
|
<PackageIcon>favicon.png</PackageIcon>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
@@ -16,7 +17,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="../favicon.png" Pack="true" PackagePath="" />
|
<None Include="../Readme.md" Pack="true" PackagePath="" Visible="false" />
|
||||||
|
<None Include="../favicon.png" Pack="true" PackagePath="" Visible="false" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -28,20 +30,10 @@
|
|||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- The following elements are responsible for embedding the analyzer assembly within the output NuGet package -->
|
<!-- Pack the analyzer assembly inside the package -->
|
||||||
|
|
||||||
<ItemGroup>
|
<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>
|
</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>
|
</Project>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Extensibility;
|
using CliFx.Extensibility;
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
@@ -161,12 +162,18 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
|
catch (Exception ex) when (ex is not CliFxException) // don't wrap CliFxException
|
||||||
{
|
{
|
||||||
|
// We use reflection-based invocation which can throw TargetInvocationException.
|
||||||
|
// Unwrap these exceptions to provide a more user-friendly error message.
|
||||||
|
var errorMessage = ex is TargetInvocationException invokeEx
|
||||||
|
? invokeEx.InnerException?.Message ?? invokeEx.Message
|
||||||
|
: ex.Message;
|
||||||
|
|
||||||
throw CliFxException.UserError(
|
throw CliFxException.UserError(
|
||||||
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" +
|
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" +
|
||||||
Environment.NewLine +
|
Environment.NewLine +
|
||||||
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
|
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
|
||||||
Environment.NewLine +
|
Environment.NewLine +
|
||||||
$"Error: {ex.Message}",
|
$"Error: {errorMessage}",
|
||||||
ex
|
ex
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Infrastructure;
|
using CliFx.Infrastructure;
|
||||||
using CliFx.Input;
|
using CliFx.Input;
|
||||||
|
|
||||||
@@ -47,9 +46,9 @@ namespace CliFx.Formatting
|
|||||||
foreach (var value in optionInput.Values)
|
foreach (var value in optionInput.Values)
|
||||||
{
|
{
|
||||||
Write(' ');
|
Write(' ');
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(value);
|
Write(value);
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
Write(']');
|
Write(']');
|
||||||
@@ -75,9 +74,9 @@ namespace CliFx.Formatting
|
|||||||
Write('=');
|
Write('=');
|
||||||
|
|
||||||
// Value
|
// Value
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(environmentVariableInput.Value);
|
Write(environmentVariableInput.Value);
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
|
|
||||||
WriteLine();
|
WriteLine();
|
||||||
}
|
}
|
||||||
@@ -93,7 +92,7 @@ namespace CliFx.Formatting
|
|||||||
|
|
||||||
internal static class CommandInputConsoleFormatterExtensions
|
internal static class CommandInputConsoleFormatterExtensions
|
||||||
{
|
{
|
||||||
public static void WriteCommandInput(this IConsole console, CommandInput commandInput) =>
|
public static void WriteCommandInput(this ConsoleWriter consoleWriter, CommandInput commandInput) =>
|
||||||
new CommandInputConsoleFormatter(console.Output).WriteCommandInput(commandInput);
|
new CommandInputConsoleFormatter(consoleWriter).WriteCommandInput(commandInput);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ namespace CliFx.Formatting
|
|||||||
Write("at ");
|
Write("at ");
|
||||||
|
|
||||||
// Fully qualified method name
|
// Fully qualified method name
|
||||||
Write(ConsoleColor.DarkGray, stackFrame.ParentType + '.');
|
Write(stackFrame.ParentType + '.');
|
||||||
Write(ConsoleColor.Yellow, stackFrame.MethodName);
|
Write(ConsoleColor.Yellow, stackFrame.MethodName);
|
||||||
|
|
||||||
// Method parameters
|
// Method parameters
|
||||||
@@ -60,7 +60,7 @@ namespace CliFx.Formatting
|
|||||||
Write("in ");
|
Write("in ");
|
||||||
|
|
||||||
// File path
|
// File path
|
||||||
Write(ConsoleColor.DarkGray, stackFrameDirectoryPath);
|
Write(stackFrameDirectoryPath);
|
||||||
Write(ConsoleColor.Yellow, stackFrameFileName);
|
Write(ConsoleColor.Yellow, stackFrameFileName);
|
||||||
|
|
||||||
// Source position
|
// Source position
|
||||||
@@ -80,7 +80,7 @@ namespace CliFx.Formatting
|
|||||||
|
|
||||||
// Fully qualified exception type
|
// Fully qualified exception type
|
||||||
var exceptionType = exception.GetType();
|
var exceptionType = exception.GetType();
|
||||||
Write(ConsoleColor.DarkGray, exceptionType.Namespace + '.');
|
Write(exceptionType.Namespace + '.');
|
||||||
Write(ConsoleColor.White, exceptionType.Name);
|
Write(ConsoleColor.White, exceptionType.Name);
|
||||||
Write(": ");
|
Write(": ");
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ namespace CliFx.Formatting
|
|||||||
|
|
||||||
internal static class ExceptionConsoleFormatterExtensions
|
internal static class ExceptionConsoleFormatterExtensions
|
||||||
{
|
{
|
||||||
public static void WriteException(this IConsole console, Exception exception) =>
|
public static void WriteException(this ConsoleWriter consoleWriter, Exception exception) =>
|
||||||
new ExceptionConsoleFormatter(console.Error).WriteException(exception);
|
new ExceptionConsoleFormatter(consoleWriter).WriteException(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ namespace CliFx.Formatting
|
|||||||
|
|
||||||
private void WriteCommandInvocation()
|
private void WriteCommandInvocation()
|
||||||
{
|
{
|
||||||
Write(ConsoleColor.DarkGray, _context.ApplicationMetadata.ExecutableName);
|
Write(_context.ApplicationMetadata.ExecutableName);
|
||||||
|
|
||||||
// Command name
|
// Command name
|
||||||
if (!string.IsNullOrWhiteSpace(_context.CommandSchema.Name))
|
if (!string.IsNullOrWhiteSpace(_context.CommandSchema.Name))
|
||||||
@@ -190,9 +190,9 @@ namespace CliFx.Formatting
|
|||||||
Write(", ");
|
Write(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(ConsoleColor.White, validValue.ToString());
|
Write(validValue.ToString());
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
Write('.');
|
Write('.');
|
||||||
@@ -269,9 +269,9 @@ namespace CliFx.Formatting
|
|||||||
Write(", ");
|
Write(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(ConsoleColor.White, validValue.ToString());
|
Write(validValue.ToString());
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
Write('.');
|
Write('.');
|
||||||
@@ -317,10 +317,12 @@ namespace CliFx.Formatting
|
|||||||
Write(", ");
|
Write(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(element.ToString(CultureInfo.InvariantCulture));
|
Write(element.ToString(CultureInfo.InvariantCulture));
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Write('.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -329,13 +331,12 @@ namespace CliFx.Formatting
|
|||||||
{
|
{
|
||||||
Write(ConsoleColor.White, "Default: ");
|
Write(ConsoleColor.White, "Default: ");
|
||||||
|
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
Write(defaultValue.ToString(CultureInfo.InvariantCulture));
|
Write(defaultValue.ToString(CultureInfo.InvariantCulture));
|
||||||
Write(ConsoleColor.DarkGray, '"');
|
Write('"');
|
||||||
|
Write('.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Write('.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +444,7 @@ namespace CliFx.Formatting
|
|||||||
|
|
||||||
internal static class HelpConsoleFormatterExtensions
|
internal static class HelpConsoleFormatterExtensions
|
||||||
{
|
{
|
||||||
public static void WriteHelpText(this IConsole console, HelpContext context) =>
|
public static void WriteHelpText(this ConsoleWriter consoleWriter, HelpContext context) =>
|
||||||
new HelpConsoleFormatter(console.Output, context).WriteHelpText();
|
new HelpConsoleFormatter(consoleWriter, context).WriteHelpText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace CliFx.Infrastructure
|
|||||||
/// Initializes an instance of <see cref="ConsoleReader"/>.
|
/// Initializes an instance of <see cref="ConsoleReader"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ConsoleReader(IConsole console, Stream stream, Encoding encoding)
|
public ConsoleReader(IConsole console, Stream stream, Encoding encoding)
|
||||||
: base(stream, encoding, false)
|
: base(stream, encoding, false, 4096)
|
||||||
{
|
{
|
||||||
Console = console;
|
Console = console;
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,11 @@ namespace CliFx.Infrastructure
|
|||||||
|
|
||||||
public partial class ConsoleReader
|
public partial class ConsoleReader
|
||||||
{
|
{
|
||||||
internal static ConsoleReader Create(IConsole console, Stream? stream) =>
|
internal static ConsoleReader Create(IConsole console, Stream? stream) => new(
|
||||||
new(console, stream is not null ? Stream.Synchronized(stream) : Stream.Null);
|
console,
|
||||||
|
stream is not null
|
||||||
|
? Stream.Synchronized(stream)
|
||||||
|
: Stream.Null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using CliFx.Utils;
|
||||||
|
|
||||||
namespace CliFx.Infrastructure
|
namespace CliFx.Infrastructure
|
||||||
{
|
{
|
||||||
@@ -17,7 +18,7 @@ namespace CliFx.Infrastructure
|
|||||||
/// Initializes an instance of <see cref="ConsoleWriter"/>.
|
/// Initializes an instance of <see cref="ConsoleWriter"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ConsoleWriter(IConsole console, Stream stream, Encoding encoding)
|
public ConsoleWriter(IConsole console, Stream stream, Encoding encoding)
|
||||||
: base(stream, encoding)
|
: base(stream, encoding.WithoutPreamble(), 256)
|
||||||
{
|
{
|
||||||
Console = console;
|
Console = console;
|
||||||
}
|
}
|
||||||
@@ -33,7 +34,11 @@ namespace CliFx.Infrastructure
|
|||||||
|
|
||||||
public partial class ConsoleWriter
|
public partial class ConsoleWriter
|
||||||
{
|
{
|
||||||
internal static ConsoleWriter Create(IConsole console, Stream? stream) =>
|
internal static ConsoleWriter Create(IConsole console, Stream? stream) => new(
|
||||||
new(console, stream is not null ? Stream.Synchronized(stream) : Stream.Null) {AutoFlush = true};
|
console,
|
||||||
|
stream is not null
|
||||||
|
? Stream.Synchronized(stream)
|
||||||
|
: Stream.Null
|
||||||
|
) {AutoFlush = true};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,12 +70,16 @@ namespace CliFx.Infrastructure
|
|||||||
/// Subsequent calls to this method have no side-effects and return the same token.
|
/// Subsequent calls to this method have no side-effects and return the same token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
/// Calling this method effectively makes the command cancellation-aware, which
|
/// 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.
|
/// but will instead trigger a token that the command can use to exit more gracefully.
|
||||||
///
|
/// </para>
|
||||||
/// If the user sends a second interrupt signal after the first one, the application
|
/// <para>
|
||||||
/// will terminate immediately.
|
/// 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>
|
/// </remarks>
|
||||||
CancellationToken RegisterCancellationHandler();
|
CancellationToken RegisterCancellationHandler();
|
||||||
}
|
}
|
||||||
@@ -116,16 +120,10 @@ namespace CliFx.Infrastructure
|
|||||||
public static IDisposable WithColors(
|
public static IDisposable WithColors(
|
||||||
this IConsole console,
|
this IConsole console,
|
||||||
ConsoleColor foregroundColor,
|
ConsoleColor foregroundColor,
|
||||||
ConsoleColor backgroundColor)
|
ConsoleColor backgroundColor) =>
|
||||||
{
|
Disposable.Merge(
|
||||||
var foregroundColorRegistration = console.WithForegroundColor(foregroundColor);
|
console.WithForegroundColor(foregroundColor),
|
||||||
var backgroundColorRegistration = console.WithBackgroundColor(backgroundColor);
|
console.WithBackgroundColor(backgroundColor)
|
||||||
|
);
|
||||||
return Disposable.Create(() =>
|
|
||||||
{
|
|
||||||
foregroundColorRegistration.Dispose();
|
|
||||||
backgroundColorRegistration.Dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,20 @@ namespace CliFx.Schema
|
|||||||
|
|
||||||
public IReadOnlyList<object?> GetValidValues()
|
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
|
// We can only get valid values for enums
|
||||||
if (underlyingType.IsEnum)
|
if (underlyingType.IsEnum)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CliFx.Utils
|
namespace CliFx.Utils
|
||||||
{
|
{
|
||||||
@@ -14,5 +15,14 @@ namespace CliFx.Utils
|
|||||||
internal partial class Disposable
|
internal partial class Disposable
|
||||||
{
|
{
|
||||||
public static IDisposable Create(Action dispose) => new Disposable(dispose);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,9 +54,11 @@ namespace CliFx.Utils.Extensions
|
|||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsToStringOverriden(this Type type) =>
|
public static bool IsToStringOverriden(this Type type)
|
||||||
type.GetMethod(nameof(ToString), Type.EmptyTypes) !=
|
{
|
||||||
typeof(object).GetMethod(nameof(ToString), Type.EmptyTypes);
|
var toStringMethod = type.GetMethod(nameof(ToString), Type.EmptyTypes);
|
||||||
|
return toStringMethod?.GetBaseDefinition()?.DeclaringType != toStringMethod?.DeclaringType;
|
||||||
|
}
|
||||||
|
|
||||||
// Types supported by `Convert.ChangeType(...)`
|
// Types supported by `Convert.ChangeType(...)`
|
||||||
private static readonly HashSet<Type> ConvertibleTypes = new()
|
private static readonly HashSet<Type> ConvertibleTypes = new()
|
||||||
|
|||||||
142
CliFx/Utils/NoPreambleEncoding.cs
Normal file
142
CliFx/Utils/NoPreambleEncoding.cs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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
|
||||||
|
// Also see:
|
||||||
|
// https://source.dot.net/#System.Console/ConsoleEncoding.cs,5eedd083a4a4f4a2
|
||||||
|
internal class NoPreambleEncoding : Encoding
|
||||||
|
{
|
||||||
|
private readonly Encoding _underlyingEncoding;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string EncodingName => _underlyingEncoding.EncodingName;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string BodyName => _underlyingEncoding.BodyName;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int CodePage => _underlyingEncoding.CodePage;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int WindowsCodePage => _underlyingEncoding.WindowsCodePage;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string HeaderName => _underlyingEncoding.HeaderName;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string WebName => _underlyingEncoding.WebName;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsBrowserDisplay => _underlyingEncoding.IsBrowserDisplay;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsBrowserSave => _underlyingEncoding.IsBrowserSave;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsSingleByte => _underlyingEncoding.IsSingleByte;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsMailNewsDisplay => _underlyingEncoding.IsMailNewsDisplay;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsMailNewsSave => _underlyingEncoding.IsMailNewsSave;
|
||||||
|
|
||||||
|
public NoPreambleEncoding(Encoding underlyingEncoding)
|
||||||
|
: base(
|
||||||
|
underlyingEncoding.CodePage,
|
||||||
|
underlyingEncoding.EncoderFallback,
|
||||||
|
underlyingEncoding.DecoderFallback
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_underlyingEncoding = underlyingEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the only part that changes
|
||||||
|
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 GetByteCount(char[] chars) => _underlyingEncoding.GetByteCount(chars);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetByteCount(string s) => _underlyingEncoding.GetByteCount(s);
|
||||||
|
|
||||||
|
[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 byte[] GetBytes(char[] chars) => _underlyingEncoding.GetBytes(chars);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override byte[] GetBytes(char[] chars, int index, int count) =>
|
||||||
|
_underlyingEncoding.GetBytes(chars, index, count);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override byte[] GetBytes(string s) => _underlyingEncoding.GetBytes(s);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetBytes(string s, int charIndex, int charCount, byte[] bytes, int byteIndex) =>
|
||||||
|
_underlyingEncoding.GetBytes(s, charIndex, charCount, bytes, byteIndex);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetCharCount(byte[] bytes, int index, int count) =>
|
||||||
|
_underlyingEncoding.GetCharCount(bytes, index, count);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetCharCount(byte[] bytes) => _underlyingEncoding.GetCharCount(bytes);
|
||||||
|
|
||||||
|
[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 char[] GetChars(byte[] bytes) => _underlyingEncoding.GetChars(bytes);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override char[] GetChars(byte[] bytes, int index, int count) =>
|
||||||
|
_underlyingEncoding.GetChars(bytes, index, count);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string GetString(byte[] bytes) => _underlyingEncoding.GetString(bytes);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override string GetString(byte[] bytes, int index, int count) =>
|
||||||
|
_underlyingEncoding.GetString(bytes, index, count);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetMaxByteCount(int charCount) =>
|
||||||
|
_underlyingEncoding.GetMaxByteCount(charCount);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override int GetMaxCharCount(int byteCount) =>
|
||||||
|
_underlyingEncoding.GetMaxCharCount(byteCount);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override bool IsAlwaysNormalized(NormalizationForm form) => _underlyingEncoding.IsAlwaysNormalized(form);
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override Encoder GetEncoder() => _underlyingEncoding.GetEncoder();
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override Decoder GetDecoder() => _underlyingEncoding.GetDecoder();
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public override object Clone() => new NoPreambleEncoding((Encoding) base.Clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class NoPreambleEncodingExtensions
|
||||||
|
{
|
||||||
|
public static Encoding WithoutPreamble(this Encoding encoding) =>
|
||||||
|
encoding.GetPreamble().Length > 0
|
||||||
|
? new NoPreambleEncoding(encoding)
|
||||||
|
: encoding;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,9 +24,6 @@ internal static partial class PolyfillExtensions
|
|||||||
key = pair.Key;
|
key = pair.Key;
|
||||||
value = pair.Value;
|
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
|
internal static partial class PolyfillExtensions
|
||||||
@@ -44,4 +41,13 @@ namespace System.Linq
|
|||||||
new(source, comparer);
|
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
|
#endif
|
||||||
@@ -91,12 +91,7 @@ namespace CliFx.Utils
|
|||||||
{
|
{
|
||||||
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
|
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
|
||||||
|
|
||||||
// Ensure success (all lines should be parsed)
|
if (matches.Length <= 0 || matches.Any(m => !m.Success))
|
||||||
var isSuccess =
|
|
||||||
matches.Length ==
|
|
||||||
stackTrace.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length;
|
|
||||||
|
|
||||||
if (!isSuccess)
|
|
||||||
{
|
{
|
||||||
// If parsing fails, we include the original stacktrace in the
|
// If parsing fails, we include the original stacktrace in the
|
||||||
// exception so that it's shown to the user.
|
// exception so that it's shown to the user.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>2.0</Version>
|
<Version>2.0.6</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
|
|||||||
7
NuGet.config
Normal file
7
NuGet.config
Normal 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>
|
||||||
148
Readme.md
148
Readme.md
@@ -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.
|
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.
|
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
|
MyApp v1.0
|
||||||
|
|
||||||
Usage
|
USAGE
|
||||||
dotnet myapp.dll [options]
|
dotnet myapp.dll [options]
|
||||||
|
|
||||||
Options
|
OPTIONS
|
||||||
-h|--help Shows help text.
|
-h|--help Shows help text.
|
||||||
--version Shows version information.
|
--version Shows version information.
|
||||||
```
|
```
|
||||||
@@ -129,7 +129,8 @@ public class LogCommand : ICommand
|
|||||||
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
|
[CommandParameter(0, Description = "Value whose logarithm is to be found.")]
|
||||||
public double Value { get; init; }
|
public double Value { get; init; }
|
||||||
|
|
||||||
// Name: --base | Short name: -b
|
// Name: --base
|
||||||
|
// Short name: -b
|
||||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||||
public double Base { get; init; } = 10;
|
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
|
MyApp v1.0
|
||||||
|
|
||||||
Usage
|
USAGE
|
||||||
dotnet myapp.dll <value> [options]
|
dotnet myapp.dll <value> [options]
|
||||||
|
|
||||||
Parameters
|
PARAMETERS
|
||||||
* value Value whose logarithm is to be found.
|
* value Value whose logarithm is to be found.
|
||||||
|
|
||||||
Options
|
OPTIONS
|
||||||
-b|--base Logarithm base. Default: "10".
|
-b|--base Logarithm base. Default: "10".
|
||||||
-h|--help Shows help text.
|
-h|--help Shows help text.
|
||||||
--version Shows version information.
|
--version Shows version information.
|
||||||
@@ -244,67 +245,15 @@ Parameters and options can have the following underlying types:
|
|||||||
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
|
- Types that are assignable from arrays (`IReadOnlyList<T>`, `ICollection<T>`, etc.)
|
||||||
- Types with a constructor accepting an array (`List<T>`, `HashSet<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
|
Here's an example of a command with an array-backed parameter:
|
||||||
// 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:
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
[Command]
|
[Command]
|
||||||
public class FileSizeCalculatorCommand : ICommand
|
public class FileSizeCalculatorCommand : ICommand
|
||||||
{
|
{
|
||||||
// FileInfo is string-initializable and IReadOnlyList<T> can be assgined from an array,
|
// FileInfo is string-initializable and IReadOnlyList<T> can be assigned from an array,
|
||||||
// so the value of this property can be mapped from a sequence of arguments.
|
// so the value of this property can be mapped from a sequence of arguments.
|
||||||
[CommandParameter(0)]
|
[CommandParameter(0)]
|
||||||
public IReadOnlyList<FileInfo> Files { get; init; }
|
public IReadOnlyList<FileInfo> Files { get; init; }
|
||||||
@@ -352,6 +301,56 @@ public class FileSizeCalculatorCommand : ICommand
|
|||||||
Total file size: 186368 bytes
|
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
|
### 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.
|
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
|
MyApp v1.0
|
||||||
|
|
||||||
Usage
|
USAGE
|
||||||
dotnet myapp.dll [options]
|
dotnet myapp.dll [options]
|
||||||
dotnet myapp.dll [command] [...]
|
dotnet myapp.dll [command] [...]
|
||||||
|
|
||||||
Options
|
OPTIONS
|
||||||
-h|--help Shows help text.
|
-h|--help Shows help text.
|
||||||
--version Shows version information.
|
--version Shows version information.
|
||||||
|
|
||||||
Commands
|
COMMANDS
|
||||||
cmd1 Subcommands: cmd1 sub.
|
cmd1 Subcommands: cmd1 sub.
|
||||||
cmd2
|
cmd2
|
||||||
|
|
||||||
@@ -420,21 +419,21 @@ The user can also refine their help request by querying it on a specific command
|
|||||||
```sh
|
```sh
|
||||||
> dotnet myapp.dll cmd1 --help
|
> dotnet myapp.dll cmd1 --help
|
||||||
|
|
||||||
Usage
|
USAGE
|
||||||
dotnet myapp.dll cmd1 [options]
|
dotnet myapp.dll cmd1 [options]
|
||||||
dotnet myapp.dll cmd1 [command] [...]
|
dotnet myapp.dll cmd1 [command] [...]
|
||||||
|
|
||||||
Options
|
OPTIONS
|
||||||
-h|--help Shows help text.
|
-h|--help Shows help text.
|
||||||
|
|
||||||
Commands
|
COMMANDS
|
||||||
sub
|
sub
|
||||||
|
|
||||||
You can run `dotnet myapp.dll cmd1 [command] --help` to show help on a specific command.
|
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.
|
> 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
|
### Reporting errors
|
||||||
|
|
||||||
@@ -478,7 +477,8 @@ Division by zero is not supported.
|
|||||||
133
|
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
|
### Graceful cancellation
|
||||||
|
|
||||||
@@ -511,7 +511,7 @@ public class CancellableCommand : ICommand
|
|||||||
```
|
```
|
||||||
|
|
||||||
> Note that a command may use this approach to delay cancellation only once.
|
> 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
|
### 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.
|
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.
|
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
|
```csharp
|
||||||
public static class Program
|
public static class Program
|
||||||
@@ -632,7 +632,7 @@ public async Task ConcatCommand_executes_successfully()
|
|||||||
### Debug and preview mode
|
### Debug and preview mode
|
||||||
|
|
||||||
When troubleshooting issues, you may find it useful to run your app in debug or 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.
|
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:
|
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
|
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).
|
In such case, the values of the environment variable will be split by `Path.PathSeparator` (`;` on Windows, `:` on Linux).
|
||||||
|
|
||||||
## Etymology
|
## Etymology
|
||||||
|
|||||||
Reference in New Issue
Block a user