Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ef0afc2
make multitarget possible
sensslen Feb 2, 2026
138b42a
only specify netframework on windows
sensslen Feb 3, 2026
0dc7007
use windows machines to build release
sensslen Feb 3, 2026
6aadb1e
fix non windows targetframeworks setting
sensslen Feb 3, 2026
9ac37dc
Initial plan
Copilot Feb 5, 2026
090a300
fix: disable strong naming for source generator project
Copilot Feb 5, 2026
f25d48f
use fixed versions of polysharp and remove langversion specification …
sensslen Feb 5, 2026
1a2678e
Merge pull request #1 from sensslen/copilot/fix-strong-naming-issue
sensslen Feb 5, 2026
c39bf5b
Merge pull request #2 from sensslen/simon/address-review-comments
sensslen Feb 5, 2026
a74ae70
Initial plan
Copilot Feb 5, 2026
62f9505
add some documentation and remove empty catch
sensslen Feb 5, 2026
79de02d
fix: disable strong name signing for test assemblies to resolve .NET …
Copilot Feb 5, 2026
b1bfee8
Fix hashCode generation
sensslen Feb 5, 2026
8ef3481
Revert "fix: disable strong naming for source generator project"
sensslen Feb 5, 2026
755749f
fix: add app.config to bypass strong name verification for .NET Frame…
Copilot Feb 5, 2026
e1cc18b
fix: restore InternalsVisibleTo public key requirement for strong-nam…
Copilot Feb 5, 2026
117a1e1
no need to ckeck for windows compilation for unit under test
sensslen Feb 5, 2026
100ca3a
Merge branch 'main' into copilot/fix-dotnet-framework-build-issue
sensslen Feb 5, 2026
8169c1d
Merge branch 'copilot/fix-dotnet-framework-build-issue' of https://gi…
sensslen Feb 5, 2026
901b681
Merge pull request #3 from sensslen/copilot/fix-dotnet-framework-buil…
sensslen Feb 5, 2026
fcdf6cb
address more issues
sensslen Feb 5, 2026
3620b7c
Merge branch 'main' of https://github.com/sensslen/CommandLineUtils
sensslen Feb 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
release:
if: ${{ github.event.inputs.release }}
needs: build
runs-on: ubuntu-latest
runs-on: windows-latest
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would generally prefer to keep linux runners. The windows agents are 5 to 10x slower (from my non-scientific testing). Is there any reason this needs to run on Windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not aware of a way to make linux compile .net framework. That's the reason for the change. I may be wrong here though.

permissions:
contents: write # for creating GitHub releases
id-token: write # for NuGet trusted publishing (OIDC)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
Expand Down
6 changes: 3 additions & 3 deletions src/CommandLineUtils/CommandLineApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Reflection;
Expand All @@ -13,6 +12,7 @@
using System.Threading.Tasks;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Conventions;
using McMaster.Extensions.CommandLineUtils.Extensions;
using McMaster.Extensions.CommandLineUtils.HelpText;
using McMaster.Extensions.CommandLineUtils.Internal;

Expand Down Expand Up @@ -221,7 +221,7 @@ public IEnumerable<string> Names
{
get
{
if (!string.IsNullOrEmpty(Name))
if (!Name.IsNullOrEmpty())
{
yield return Name;
}
Expand Down Expand Up @@ -499,7 +499,7 @@ internal CommandLineApplication AddSubcommand(string name, Type modelType, Sourc

private void AssertCommandNameIsUnique(string? name, CommandLineApplication? commandToIgnore)
{
if (string.IsNullOrEmpty(name))
if (name.IsNullOrEmpty())
{
return;
}
Expand Down
31 changes: 16 additions & 15 deletions src/CommandLineUtils/Conventions/OptionAttributeConvention.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using McMaster.Extensions.CommandLineUtils.Extensions;
using McMaster.Extensions.CommandLineUtils.SourceGeneration;

namespace McMaster.Extensions.CommandLineUtils.Conventions
Expand Down Expand Up @@ -32,33 +33,33 @@ public virtual void Apply(ConventionContext context)
var (template, shortName, longName) = GetOptionNames(optMeta);

// Check for same-class conflicts (options in the same provider with conflicting names)
if (!string.IsNullOrEmpty(shortName) && addedShortOptions.TryGetValue(shortName, out var existingShort))
if (!shortName.IsNullOrEmpty() && addedShortOptions.TryGetValue(shortName, out var existingShort))
{
throw new InvalidOperationException(
Strings.OptionNameIsAmbiguous(shortName, optMeta.PropertyName, optMeta.DeclaringType, existingShort.PropertyName, existingShort.DeclaringType));
}
if (!string.IsNullOrEmpty(longName) && addedLongOptions.TryGetValue(longName, out var existingLong))
if (!longName.IsNullOrEmpty() && addedLongOptions.TryGetValue(longName, out var existingLong))
{
throw new InvalidOperationException(
Strings.OptionNameIsAmbiguous(longName, optMeta.PropertyName, optMeta.DeclaringType, existingLong.PropertyName, existingLong.DeclaringType));
}

// Check if option already exists from parent command (inherited options)
if (!string.IsNullOrEmpty(shortName) && context.Application._shortOptions.ContainsKey(shortName))
if (!shortName.IsNullOrEmpty() && context.Application._shortOptions.ContainsKey(shortName))
{
continue; // Skip - option already registered by parent
}
if (!string.IsNullOrEmpty(longName) && context.Application._longOptions.ContainsKey(longName))
if (!longName.IsNullOrEmpty() && context.Application._longOptions.ContainsKey(longName))
{
continue; // Skip - option already registered by parent
}

// Track this option
if (!string.IsNullOrEmpty(shortName))
if (!shortName.IsNullOrEmpty())
{
addedShortOptions[shortName] = optMeta;
}
if (!string.IsNullOrEmpty(longName))
if (!longName.IsNullOrEmpty())
{
addedLongOptions[longName] = optMeta;
}
Expand All @@ -74,18 +75,18 @@ private static (string template, string? shortName, string? longName) GetOptionN
string? shortName = meta.ShortName;
string? longName = meta.LongName;

if (string.IsNullOrEmpty(template))
if (template.IsNullOrEmpty())
{
// Build template from ShortName/LongName
if (!string.IsNullOrEmpty(shortName) && !string.IsNullOrEmpty(longName))
if (!shortName.IsNullOrEmpty() && !longName.IsNullOrEmpty())
{
template = $"-{shortName}|--{longName}";
}
else if (!string.IsNullOrEmpty(longName))
else if (!longName.IsNullOrEmpty())
{
template = $"--{longName}";
}
else if (!string.IsNullOrEmpty(shortName))
else if (!shortName.IsNullOrEmpty())
{
template = $"-{shortName}";
}
Expand All @@ -99,17 +100,17 @@ private static (string template, string? shortName, string? longName) GetOptionN
else
{
// Parse short/long names from template if not already set
if (string.IsNullOrEmpty(shortName) || string.IsNullOrEmpty(longName))
if (shortName.IsNullOrEmpty() || longName.IsNullOrEmpty())
{
var parts = template.Split('|');
foreach (var part in parts)
{
var trimmed = part.Trim();
if (trimmed.StartsWith("--") && string.IsNullOrEmpty(longName))
if (trimmed.StartsWith("--") && longName.IsNullOrEmpty())
{
longName = trimmed.Substring(2).Split(' ', '<', ':', '=')[0];
}
else if (trimmed.StartsWith("-") && string.IsNullOrEmpty(shortName))
else if (trimmed.StartsWith("-") && shortName.IsNullOrEmpty())
{
shortName = trimmed.Substring(1).Split(' ', '<', ':', '=')[0];
}
Expand Down Expand Up @@ -175,12 +176,12 @@ private void AddOptionFromMetadata(ConventionContext context, CommandOption opti
}

// Register names for duplicate checking
if (!string.IsNullOrEmpty(option.ShortName))
if (!option.ShortName.IsNullOrEmpty())
{
context.Application._shortOptions.TryAdd(option.ShortName, null!);
}

if (!string.IsNullOrEmpty(option.LongName))
if (!option.LongName.IsNullOrEmpty())
{
context.Application._longOptions.TryAdd(option.LongName, null!);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using McMaster.Extensions.CommandLineUtils.Extensions;
using McMaster.Extensions.CommandLineUtils.Validation;

namespace McMaster.Extensions.CommandLineUtils.Conventions
Expand Down Expand Up @@ -38,7 +39,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
throw new InvalidOperationException(Strings.NoValueTypesMustBeBoolean);
}

if (!string.IsNullOrEmpty(option.ShortName))
if (!option.ShortName.IsNullOrEmpty())
{
if (context.Application._shortOptions.TryGetValue(option.ShortName, out var otherProp))
{
Expand All @@ -53,7 +54,7 @@ private protected void AddOption(ConventionContext context, CommandOption option
context.Application._shortOptions.Add(option.ShortName, prop);
}

if (!string.IsNullOrEmpty(option.LongName))
if (!option.LongName.IsNullOrEmpty())
{
if (context.Application._longOptions.TryGetValue(option.LongName, out var otherProp))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Reflection;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Errors;
using McMaster.Extensions.CommandLineUtils.Extensions;
using McMaster.Extensions.CommandLineUtils.SourceGeneration;

namespace McMaster.Extensions.CommandLineUtils.Conventions
Expand Down Expand Up @@ -46,7 +47,7 @@ public virtual void Apply(ConventionContext context)
private static string GetSubcommandName(Type subcommandType, ICommandMetadataProvider provider)
{
var commandInfo = provider.CommandInfo;
if (!string.IsNullOrEmpty(commandInfo?.Name))
if (!(commandInfo?.Name).IsNullOrEmpty())
{
// Use the explicit name as-is
return commandInfo.Name;
Expand All @@ -58,7 +59,10 @@ private static string GetSubcommandName(Type subcommandType, ICommandMetadataPro

private void AddSubcommandFromMetadata(
ConventionContext context,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type subcommandType,
#if NET6_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
#endif
Type subcommandType,
ICommandMetadataProvider provider,
string name)
{
Expand Down
22 changes: 22 additions & 0 deletions src/CommandLineUtils/Extensions/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;

namespace McMaster.Extensions.CommandLineUtils.Extensions
{
internal static class DictionaryExtensions
{
#if !NET6_0_OR_GREATER
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
{
if (dictionary.ContainsKey(key))
{
return false;
}
dictionary.Add(key, value);
return true;
}
#endif
}
}
21 changes: 21 additions & 0 deletions src/CommandLineUtils/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Diagnostics.CodeAnalysis;

namespace McMaster.Extensions.CommandLineUtils.Extensions
{
internal static class StringExtensions
{
/// <summary>
/// A wrapper around <see cref="string.IsNullOrEmpty(string)"/> that allows proper nullability annotation.
/// This is a workaround because .NET Framework assemblies are not nullability annotated.
/// </summary>
public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value);
/// <summary>
/// A wrapper around <see cref="string.IsNullOrWhiteSpace(string)"/> that allows proper nullability annotation.
/// This is a workaround because .NET Framework assemblies are not nullability annotated.
/// </summary>
public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) => string.IsNullOrWhiteSpace(value);
}
}
3 changes: 2 additions & 1 deletion src/CommandLineUtils/HelpText/HangingIndentWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Text;
using McMaster.Extensions.CommandLineUtils.Extensions;

namespace McMaster.Extensions.CommandLineUtils.HelpText
{
Expand Down Expand Up @@ -46,7 +47,7 @@ public HangingIndentWriter(int indentSize, int? maxLineLength = null, bool inden
/// <returns>Dynamically wrapped description with explicit newlines preserved.</returns>
public string Write(string? input)
{
if (string.IsNullOrWhiteSpace(input))
if (input.IsNullOrWhiteSpace())
{
return string.Empty;
}
Expand Down
3 changes: 2 additions & 1 deletion src/CommandLineUtils/Internal/CommandLineProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IO;
using System.Linq;
using McMaster.Extensions.CommandLineUtils.Abstractions;
using McMaster.Extensions.CommandLineUtils.Extensions;

namespace McMaster.Extensions.CommandLineUtils
{
Expand Down Expand Up @@ -325,7 +326,7 @@ private bool ProcessUnexpectedArg(string argTypeName, string? argValue = null)

var suggestions = Enumerable.Empty<string>();

if (_currentCommand.MakeSuggestionsInErrorMessage && !string.IsNullOrEmpty(value))
if (_currentCommand.MakeSuggestionsInErrorMessage && !value.IsNullOrEmpty())
{
suggestions = SuggestionCreator.GetTopSuggestions(_currentCommand, value);
}
Expand Down
27 changes: 26 additions & 1 deletion src/CommandLineUtils/Internal/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,37 @@ public bool Equals(MethodInfo? x, MethodInfo? y)
return true;
}

return x != null && y != null && x.HasSameMetadataDefinitionAs(y);
if (x == null || y == null)
{
return false;
}

#if NET_6_0_OR_GREATER
return x.HasSameMetadataDefinitionAs(y);
#else
return x.MetadataToken == y.MetadataToken && x.Module.Equals(y.Module);
#endif
}

public int GetHashCode(MethodInfo obj)
{
#if NET_6_0_OR_GREATER
return obj.HasMetadataToken() ? obj.GetMetadataToken().GetHashCode() : 0;
#else
// see https://github.com/dotnet/dotnet/blob/b0f34d51fccc69fd334253924abd8d6853fad7aa/src/runtime/src/libraries/System.Reflection.TypeExtensions/src/System/Reflection/TypeExtensions.cs#L496
int token = obj.MetadataToken;

// Tokens have MSB = table index, 3 LSBs = row index
// row index of 0 is a nil token
const int rowMask = 0x00FFFFFF;
if ((token & rowMask) == 0)
{
// Nil token is returned for edge cases like typeof(byte[]).MetadataToken.
return 0;
}

return token;
#endif
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/CommandLineUtils/Internal/SuggestionCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Linq;
using McMaster.Extensions.CommandLineUtils.Extensions;

namespace McMaster.Extensions.CommandLineUtils
{
Expand Down Expand Up @@ -45,17 +46,17 @@ private static IEnumerable<string> GetCandidates(CommandLineApplication command)

foreach (var option in command.GetOptions().Where(o => o.ShowInHelpText))
{
if (!string.IsNullOrEmpty(option.LongName))
if (!option.LongName.IsNullOrEmpty())
{
yield return option.LongName;
}

if (!string.IsNullOrEmpty(option.ShortName))
if (!option.ShortName.IsNullOrEmpty())
{
yield return option.ShortName;
}

if (!string.IsNullOrEmpty(option.SymbolName))
if (!option.SymbolName.IsNullOrEmpty())
{
yield return option.SymbolName;
}
Expand Down
Loading
Loading