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
|
||||
|
||||
- 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 }}
|
||||
|
||||
4
.github/workflows/CI.yml
vendored
4
.github/workflows/CI.yml
vendored
@@ -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
|
||||
|
||||
32
Changelog.md
32
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)
|
||||
|
||||
> Note: this major release includes many breaking changes.
|
||||
@@ -82,4 +112,4 @@ Please refer to the readme to find updated instructions and usage examples.
|
||||
|
||||
- Changed `IConsole` interface (and as a result, `SystemConsole` and `VirtualConsole`) to support writing binary data. Instead of `TextReader`/`TextWriter` instances, the streams are now exposed as `StreamReader`/`StreamWriter` which provide the `BaseStream` property that allows raw access. Existing usages inside commands should remain the same because `StreamReader`/`StreamWriter` are compatible with their base classes `TextReader`/`TextWriter`, but if you were using `VirtualConsole` in tests, you may have to update it to the new API. Refer to the readme for more info.
|
||||
- Changed argument binding behavior so that an error is produced if the user provides an argument that doesn't match with any parameter or option. This is done in order to improve user experience, as otherwise the user may make a typo without knowing that their input wasn't taken into account.
|
||||
- Changed argument binding behavior so that options can be set to multiple argument values while specifying them with mixed naming. For example, `--option value1 -o value2 --option value3` would result in the option being set to corresponding three values, assuming `--option` and `-o` match with the same option.
|
||||
- Changed argument binding behavior so that options can be set to multiple argument values while specifying them with mixed naming. For example, `--option value1 -o value2 --option value3` would result in the option being set to corresponding three values, assuming `--option` and `-o` match with the same option.
|
||||
@@ -13,11 +13,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Basic.Reference.Assemblies" Version="1.1.2" />
|
||||
<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.CodeAnalysis.CSharp" Version="3.4.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
|
||||
<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" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Basic.Reference.Assemblies;
|
||||
using FluentAssertions.Execution;
|
||||
using FluentAssertions.Primitives;
|
||||
using Microsoft.CodeAnalysis;
|
||||
@@ -58,14 +58,8 @@ namespace CliFx.Analyzers.Tests.Utils
|
||||
var compilation = CSharpCompilation.Create(
|
||||
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
||||
new[] {ast},
|
||||
new[]
|
||||
{
|
||||
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)
|
||||
},
|
||||
ReferenceAssemblies.Net50
|
||||
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)),
|
||||
// DLL to avoid having to define the Main() method
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
);
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
IPropertySymbol property)
|
||||
{
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (property.ContainingType.IsAbstract)
|
||||
return;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace CliFx.Analyzers
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
IPropertySymbol property)
|
||||
{
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (property.ContainingType.IsAbstract)
|
||||
return;
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
IPropertySymbol property)
|
||||
{
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (IsScalar(property.Type))
|
||||
return;
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace CliFx.Analyzers
|
||||
PropertyDeclarationSyntax propertyDeclaration,
|
||||
IPropertySymbol property)
|
||||
{
|
||||
if (property.ContainingType is null)
|
||||
return;
|
||||
|
||||
if (!CommandParameterSymbol.IsParameterProperty(property))
|
||||
return;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace CliFx.Benchmarks
|
||||
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
|
||||
DefaultConfig
|
||||
.Instance
|
||||
.With(ConfigOptions.DisableOptimizationsValidator)
|
||||
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
</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="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.CodeAnalysis.CSharp" Version="3.9.0" />
|
||||
<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" />
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Infrastructure;
|
||||
using CliFx.Tests.Utils;
|
||||
using CliWrap;
|
||||
using CliWrap.Buffered;
|
||||
@@ -135,5 +138,22 @@ public class Command : ICommand
|
||||
stdOut.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);
|
||||
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]
|
||||
public async Task Help_text_shows_environment_variables_for_options_that_have_them_configured_as_fallback()
|
||||
{
|
||||
@@ -875,4 +965,4 @@ public class SecondCommandSecondChildCommand : ICommand
|
||||
stdOut.Trim().Should().Be("v6.9");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Basic.Reference.Assemblies;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
@@ -60,16 +61,9 @@ namespace CliFx.Tests.Utils
|
||||
var compilation = CSharpCompilation.Create(
|
||||
"CliFxTests_DynamicAssembly_" + Guid.NewGuid(),
|
||||
new[] {ast},
|
||||
new[]
|
||||
{
|
||||
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(Enumerable).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location)
|
||||
},
|
||||
ReferenceAssemblies.Net50
|
||||
.Append(MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location))
|
||||
.Append(MetadataReference.CreateFromFile(typeof(DynamicCommandBuilder).Assembly.Location)),
|
||||
// DLL to avoid having to define the Main() method
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
public string ExecutableName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Application version text.
|
||||
/// Application version.
|
||||
/// </summary>
|
||||
public string Version { get; }
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl>
|
||||
<PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes>
|
||||
<PackageReadmeFile>Readme.md</PackageReadmeFile>
|
||||
<PackageIcon>favicon.png</PackageIcon>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -16,7 +17,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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>
|
||||
@@ -28,20 +30,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>
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Extensibility;
|
||||
using CliFx.Infrastructure;
|
||||
@@ -161,12 +162,18 @@ namespace CliFx
|
||||
}
|
||||
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(
|
||||
$"{memberSchema.GetKind()} {memberSchema.GetFormattedIdentifier()} cannot be set from provided argument(s):" +
|
||||
Environment.NewLine +
|
||||
rawValues.Select(v => '<' + v + '>').JoinToString(" ") +
|
||||
Environment.NewLine +
|
||||
$"Error: {ex.Message}",
|
||||
$"Error: {errorMessage}",
|
||||
ex
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.WithoutPreamble(), 256)
|
||||
{
|
||||
Console = console;
|
||||
}
|
||||
@@ -33,7 +34,11 @@ namespace CliFx.Infrastructure
|
||||
|
||||
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};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -30,4 +43,4 @@ namespace CliFx.Schema
|
||||
return Array.Empty<object?>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
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;
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>2.0</Version>
|
||||
<Version>2.0.6</Version>
|
||||
<Company>Tyrrrz</Company>
|
||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||
<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>
|
||||
150
Readme.md
150
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.
|
||||
|
||||
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.
|
||||
@@ -214,7 +215,7 @@ Here are some examples of how it works:
|
||||
- `myapp cmd abc -o` routes to command `cmd` (assuming it's an existing command) with parameter `abc` and sets option `'o'` without value
|
||||
|
||||
Additionally, argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result regardless of the application configuration.
|
||||
In fact, the only context-sensitive part in the parser is the command name resolution, which needs to know the list of available commands in order to discern between arguments that correspond to command name and arguments which map as parameters.
|
||||
In fact, the only context-sensitive part in the parser is the command name resolution, which needs to know the list of available commands in order to discern between arguments that correspond to command name and arguments which map as parameters.
|
||||
|
||||
The parser's context-free nature has several implications on how it consumes arguments.
|
||||
For example, passing `myapp -i file1.txt file2.txt` will always be parsed as an option with multiple values, regardless of the arity of the underlying property it's bound to.
|
||||
@@ -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 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]
|
||||
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.
|
||||
[CommandParameter(0)]
|
||||
public IReadOnlyList<FileInfo> Files { get; init; }
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user