Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 27 additions & 10 deletions .claude/skills/prepare-release/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ Run git commands to analyze commits since last release:
- `git log --grep` for PR merges
- Look for conventional commit patterns: `fix:`, `feat:`, `break:`, `docs:`, etc.

### 3. Categorize Changes
### 3. Filter and Categorize Changes

Group into categories (in this order):
**Exclude from release notes:**
- Dependabot / automated dependency bump commits (e.g., `chore(deps): Bump ...`). These are routine maintenance and not user-facing.
- CI/tooling-only changes (e.g., updating GitHub Actions workflows, claude workflows) unless they affect the shipped package.

Group remaining changes into categories (in this order):

1. **Breaking changes** (major versions only) - API removals, behavior changes
2. **Features** - New functionality or APIs
3. **Fixes** - Bug fixes and corrections
4. **Improvements** - Performance or usability enhancements
5. **Docs** - Documentation-only changes
6. **Other** - Infrastructure, tooling, CI/CD
6. **Other** - Infrastructure, tooling, CI/CD (only if user-facing or noteworthy)

### 4. Generate releasenotes.props Entry

Expand All @@ -57,15 +61,31 @@ Fixes:
```

**Patch versions:**

Append the patch notes directly at the end of the existing parent version's `StartsWith('X.Y.')` block. Do NOT create a separate conditional block. Add the patch section just before the closing `</PackageReleaseNotes>` tag of the parent version:

```xml
<PackageReleaseNotes Condition="'$(VersionPrefix)' == 'X.Y.Z'">
$(PackageReleaseNotes)
<PackageReleaseNotes Condition="$(VersionPrefix.StartsWith('X.Y.'))">
...existing X.Y.0 release notes...

X.Y.Z patch:
Updates in X.Y.Z patch:
* @user: fix description (#123)
</PackageReleaseNotes>
```

For multiple patches, append each one in order at the end of the same block:

```xml
...existing X.Y.0 release notes...

Updates in X.Y.1 patch:
* @user: fix description (#123)

Updates in X.Y.2 patch:
* @user: another fix (#456)
</PackageReleaseNotes>
```

### 5. Generate CHANGELOG.md Entry

**Format:** `* [@contributor]: description ([#PR])`
Expand Down Expand Up @@ -196,10 +216,7 @@ See https://natemcmaster.github.io/CommandLineUtils/vX.0/upgrade-guide.html

### Patch Versions

Use `$(PackageReleaseNotes)` to inherit parent version's notes.

For first patch (X.Y.1), create new conditional entry after parent.
For subsequent patches, add BEFORE existing patches but AFTER minor version.
Append patch notes directly into the existing parent version's `StartsWith('X.Y.')` block in `releasenotes.props`. Do NOT create a separate conditional block or use `$(PackageReleaseNotes)` inheritance. Each patch gets an "Updates in X.Y.Z patch:" section appended at the end of the parent block.

## Quality Checklist

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [v5.0.1](https://github.com/natemcmaster/CommandLineUtils/compare/v5.0.0...v5.0.1)

### Features
* [@sensslen]: Restore target framework compilation for .NET Framework ([#591])

[#591]: https://github.com/natemcmaster/CommandLineUtils/pull/591

## [v5.0.0](https://github.com/natemcmaster/CommandLineUtils/compare/v4.1.1...v5.0.0)

### Breaking changes
Expand Down
9 changes: 8 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
<WarningsNotAsErrors>$(WarningsNotAsErrors);1591</WarningsNotAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<Nullable>enable</Nullable>
<!-- .NET Framework does not support nullable checks as well as .NET 8+ -->
<Nullable Condition="'$(TargetFramework)' == 'net472'">annotations</Nullable>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)src\StrongName.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>

Expand All @@ -41,7 +43,7 @@
</PropertyGroup>

<PropertyGroup>
<VersionPrefix>5.0.0</VersionPrefix>
<VersionPrefix>5.0.1</VersionPrefix>
<VersionSuffix>beta</VersionSuffix>
<IncludePreReleaseLabelInPackageVersion Condition="'$(IS_STABLE_BUILD)' != 'true'">true</IncludePreReleaseLabelInPackageVersion>
<BuildNumber Condition=" '$(BuildNumber)' == '' ">$(GITHUB_RUN_NUMBER)</BuildNumber>
Expand All @@ -53,6 +55,11 @@
<InformationalVersion Condition="'$(RepositoryCommit)' != ''">$(PackageVersion)+$(RepositoryCommit)</InformationalVersion>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<!-- Backfills features of .NET 8 for older versions of .NET -->
<PackageReference Include="PolySharp" Version="1.15.0" PrivateAssets="All" />
</ItemGroup>

<Import Project="$(MSBuildProjectDirectory)/releasenotes.props"
Condition="Exists('$(MSBuildProjectDirectory)/releasenotes.props')" />

Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,15 @@ If you need help with this project, please ...

This is a fork of [Microsoft.Extensions.CommandLineUtils](https://github.com/aspnet/Common), which was [completely abandoned by Microsoft](https://github.com/aspnet/Common/issues/257). This project [forked in 2017](https://github.com/natemcmaster/CommandLineUtils/commit/f039360e4e51bbf8b8eb6236894b626ec7944cec) and continued to make improvements. From 2017 to 2021, over 30 contributors added new features and fixed bugs. As of 2022, the project has entered maintenance mode, so no major changes are planned. [See this issue for details on latest project status.](https://github.com/natemcmaster/CommandLineUtils/issues/485) This project is not abandoned -- I believe this library provides a stable API and rich feature set good enough for most developers to create command line apps in .NET -- but only the most critical of bugs will be fixed (such as security issues).

## Supported .NET Versions

Framework | Version | Reason
---------------|---------|--------------------
`dotnet` | 8.0 | Lowest Microsoft LTS version at time of release. See <https://endoflife.date/dotnet>
.NET Framework | 4.7.2 | Lowest .NET Framework version fully compatible with [.NET Standard 2.0][netstandard-guidance]

_Why not directly compile for .NET Standard?_

Microsoft guidance says ".NET 5 and later versions adopt a different approach to establishing uniformity that eliminates the need for .NET Standard in most scenarios." Compiling for 2 frameworks appears to be sufficient, so we avoid added complexity.

[netstandard-guidance]: https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0
10 changes: 9 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ exec dotnet tool run dotnet-format -- -v detailed @formatArgs "$PSScriptRoot/doc
exec dotnet build --configuration $Configuration '-warnaserror:CS1591'
exec dotnet pack --no-build --configuration $Configuration -o $artifacts
exec dotnet build --configuration $Configuration "$PSScriptRoot/docs/samples/samples.sln"
exec dotnet test --no-build --configuration $Configuration --collect:"XPlat Code Coverage"

[string[]] $testArgs = @()
if (-not $IsWindows) {
$testArgs += '-p:TestFullFramework=false'
}

exec dotnet test --no-build --configuration $Configuration `
--collect:"XPlat Code Coverage" `
@testArgs

write-host -f green 'BUILD SUCCEEDED'
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
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ 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)]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
Type subcommandType,
ICommandMetadataProvider provider,
string name)
{
Expand Down
5 changes: 4 additions & 1 deletion src/CommandLineUtils/IO/Pager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ public void Kill()
FileName = "less",
Arguments = ArgumentEscaper.EscapeAndConcatenate(args),
RedirectStandardInput = true,
#if NET46_OR_GREATER
#if NET472_OR_GREATER
UseShellExecute = false,
#elif NET6_0_OR_GREATER
#else
#error Target framework misconfiguration
#endif
}
};
Expand Down
25 changes: 25 additions & 0 deletions src/CommandLineUtils/Internal/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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
{
internal static class DictionaryExtensions
{
#if NET6_0_OR_GREATER
#elif NET472_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;
}
#else
#error Target framework misconfiguration
#endif
}
}
31 changes: 30 additions & 1 deletion src/CommandLineUtils/Internal/ReflectionHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,41 @@ 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 NET6_0_OR_GREATER
return x.HasSameMetadataDefinitionAs(y);
#elif NET472_OR_GREATER
return x.MetadataToken == y.MetadataToken && x.Module.Equals(y.Module);
#else
#error Target framework misconfiguration
#endif
}

public int GetHashCode(MethodInfo obj)
{
#if NET6_0_OR_GREATER
return obj.HasMetadataToken() ? obj.GetMetadataToken().GetHashCode() : 0;
#elif NET472_OR_GREATER
// 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;
#else
#error Target framework misconfiguration
#endif
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFrameworks>net8.0;net472</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<Description>Command-line parsing API.</Description>
Expand Down Expand Up @@ -30,6 +30,10 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="4.14.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
<Reference Include="System.ComponentModel.DataAnnotations" />
</ItemGroup>

<!-- Include the source generator for AOT support -->
<ItemGroup>
<ProjectReference Include="..\CommandLineUtils.Generators\McMaster.Extensions.CommandLineUtils.Generators.csproj"
Expand All @@ -39,7 +43,7 @@ McMaster.Extensions.CommandLineUtils.ArgumentEscaper

<!-- Pack the source generator into analyzers folder for NuGet package -->
<ItemGroup>
<None Include="$(OutputPath)..\..\..\McMaster.Extensions.CommandLineUtils.Generators\$(Configuration)\netstandard2.0\McMaster.Extensions.CommandLineUtils.Generators.dll"
<None Include="$(OutputPath)..\..\McMaster.Extensions.CommandLineUtils.Generators\$(Configuration)\netstandard2.0\McMaster.Extensions.CommandLineUtils.Generators.dll"
Pack="true"
PackagePath="analyzers/dotnet/cs"
Visible="false" />
Expand Down
21 changes: 0 additions & 21 deletions src/CommandLineUtils/Properties/NullabilityHelpers.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration
/// <summary>
/// Model factory that uses Activator.CreateInstance or DI with constructor injection.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses Activator.CreateInstance or DI with constructor injection")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
internal sealed class ActivatorModelFactory : IModelFactory
{
private readonly Type _modelType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ private DefaultMetadataResolver()
/// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced
/// and the source generator runs during compilation.
/// </remarks>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
public ICommandMetadataProvider GetProvider(Type modelType)
{
// Check for generated metadata first (AOT-safe path)
Expand All @@ -50,7 +55,12 @@ public ICommandMetadataProvider GetProvider(Type modelType)
/// For full AOT compatibility, ensure the CommandLineUtils.Generators package is referenced
/// and the source generator runs during compilation.
/// </remarks>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Falls back to reflection when no generated metadata is available. Use the source generator for AOT compatibility.")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
public ICommandMetadataProvider<TModel> GetProvider<TModel>() where TModel : class
{
// Check for generated metadata first (AOT-safe path)
Expand Down Expand Up @@ -78,7 +88,12 @@ public bool HasGeneratedMetadata(Type modelType)
return CommandMetadataRegistry.HasMetadata(modelType);
}

#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to analyze the model type")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
private static ICommandMetadataProvider CreateReflectionProvider(Type modelType)
{
// This creates a reflection-based implementation of ICommandMetadataProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration
/// <summary>
/// Execute handler that uses reflection to invoke OnExecute/OnExecuteAsync.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to invoke method")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
internal sealed class ReflectionExecuteHandler : IExecuteHandler
{
private readonly MethodInfo _method;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration
/// Provides command metadata by analyzing a type using reflection.
/// This is the fallback when generated metadata is not available.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to analyze the model type")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
internal sealed class ReflectionMetadataProvider : ICommandMetadataProvider
{
private const BindingFlags MethodLookup = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ namespace McMaster.Extensions.CommandLineUtils.SourceGeneration
/// <summary>
/// Validate handler that uses reflection to invoke OnValidate.
/// </summary>
#if NET6_0_OR_GREATER
[RequiresUnreferencedCode("Uses reflection to invoke method")]
#elif NET472_OR_GREATER
#else
#error Target framework misconfiguration
#endif
internal sealed class ReflectionValidateHandler : IValidateHandler
{
private readonly MethodInfo _method;
Expand Down
Loading
Loading