10 Commits
2.0.4 ... 2.0.6

Author SHA1 Message Date
Tyrrrz
51cca36d2a Update version 2021-07-17 21:37:32 +03:00
Tyrrrz
84672c92f6 Unwrap TargetInvocationException to provide more user-friendly errors when binding fails 2021-07-17 21:32:15 +03:00
Tyrrrz
b1d01898b6 Add test for preamble omission 2021-07-10 19:43:21 +03:00
Tyrrrz
441a47a1a8 Update version 2021-07-09 22:23:46 +03:00
Tyrrrz
8abd7219a1 Better shimming in NoPreambleEncoding 2021-07-09 22:00:31 +03:00
Tyrrrz
df73a0bfe8 Update GitHib issue forms 2021-06-24 21:40:08 +03:00
Tyrrrz
55d12dc721 Add readme to package 2021-06-17 20:36:35 +03:00
Alexey Golub
a6ee44c1bb Fix typo in readme 2021-06-13 06:19:26 -07:00
Tyrrrz
76816e22f1 Use Basic.Reference.Assemblies to simplify reference resolving for dynamic assemblies in tests
Note: bumped `Microsoft.CodeAnalysis.CSharp` in test projects, but didn't touch the one in CliFx.Analyzers as it may have unintended side-effects.
2021-05-10 21:10:42 +03:00
Tyrrrz
daf25e59d6 Fix deprecation warning 2021-05-10 21:06:36 +03:00
18 changed files with 243 additions and 46 deletions

View File

@@ -1,30 +1,42 @@
name: "\U0001F41E Bug report"
name: 🐞 Bug report
description: Report broken functionality.
labels: [bug]
body:
- type: markdown
attributes:
value: |
- Please check existing issues (both opened and closed) to ensure that this bug hasn't been reported before.
- If you want to ask a question instead of reporting a bug, use [discussions](https://github.com/Tyrrrz/CliFx/discussions/new) instead.
🧐 **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(s) of CliFx does this bug affect?"
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. If relevant, include screenshots or screen recordings."
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 or code required to reproduce the bug."
description: Minimum steps required to reproduce the bug.
placeholder: |
- Step 1
- Step 2
- Step 3
validations:
required: true

View File

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

View File

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

17
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View 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

View File

@@ -1,3 +1,12 @@
### 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.
@@ -103,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.

View File

@@ -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.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.4.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<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>

View File

@@ -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)
);

View File

@@ -14,7 +14,7 @@ namespace CliFx.Benchmarks
public static void Main() => BenchmarkRunner.Run<Benchmarks>(
DefaultConfig
.Instance
.With(ConfigOptions.DisableOptimizationsValidator)
.WithOptions(ConfigOptions.DisableOptimizationsValidator)
);
}
}

View File

@@ -13,10 +13,11 @@
</ItemGroup>
<ItemGroup>
<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.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" />

View File

@@ -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");
}
}
}

View File

@@ -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");
}
}
}

View File

@@ -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)
);

View File

@@ -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>

View File

@@ -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
);
}

View File

@@ -18,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, 256)
: base(stream, encoding.WithoutPreamble(), 256)
{
Console = console;
}
@@ -27,7 +27,7 @@ namespace CliFx.Infrastructure
/// Initializes an instance of <see cref="ConsoleWriter"/>.
/// </summary>
public ConsoleWriter(IConsole console, Stream stream)
: this(console, stream, System.Console.OutputEncoding.WithoutPreamble())
: this(console, stream, System.Console.OutputEncoding)
{
}
}

View File

@@ -6,32 +6,111 @@ 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;
public NoPreambleEncoding(Encoding underlyingEncoding) =>
_underlyingEncoding = underlyingEncoding;
[ExcludeFromCodeCoverage]
public override string EncodingName => _underlyingEncoding.EncodingName;
public override byte[] GetPreamble() =>
Array.Empty<byte>();
[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);
@@ -39,6 +118,18 @@ namespace CliFx.Utils
[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

View File

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

View File

@@ -253,7 +253,7 @@ Here's an example of a command with an array-backed parameter:
[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; }