diff --git a/Build-Source.ps1 b/Build-Source.ps1
index 3c1c7b0..169afe3 100644
--- a/Build-Source.ps1
+++ b/Build-Source.ps1
@@ -30,9 +30,15 @@ try
# script. But for a local "inner loop" development this might be the only script used.
$buildInfo = Initialize-BuildEnvironment -FullInit:$FullInit
- # build the Managed code support
+ # build the core library support
Write-Information "dotnet build --tl:off 'src\Ubiquity.NET.Utils.slnx' -c $Configuration"
Invoke-External dotnet build --tl:off 'src\Ubiquity.NET.Utils.slnx' '-c' $Configuration
+
+ # build the demo that consumes the output packages as a package reference only.
+ # This ensures that the proper references are present and work. The demo uses the
+ # library and the source generation to validate it is actually functional.
+ Write-Information "dotnet build --tl:off 'src\DemoCommandLineSrcGen\DemoCommandLineSrcGen.slnx' -c $Configuration"
+ Invoke-External dotnet build --tl:off 'src\DemoCommandLineSrcGen\DemoCommandLineSrcGen.slnx' '-c' $Configuration
}
catch
{
diff --git a/Directory.Packages.props b/Directory.Packages.props
index b54992b..a221a7f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -41,7 +41,7 @@
-
+
diff --git a/Invoke-Tests.ps1 b/Invoke-Tests.ps1
index 299769e..0890e9b 100644
--- a/Invoke-Tests.ps1
+++ b/Invoke-Tests.ps1
@@ -38,6 +38,7 @@ try
Push-Location $BuildInfo['SrcRootPath']
try
{
+ dir (Join-Path $BuildInfo['BuildOutputPath'] 'bin' 'Ubiquity.NET.CommandLine.SrcGen.UT' "$Configuration" 'net10.0' 'Ubiquity.NET.CommandLine.*')
Invoke-External dotnet test Ubiquity.NET.Utils.slnx '-c' $Configuration '-tl:off' '--logger:trx' '--no-build' '-s' '.\x64.runsettings'
}
finally
diff --git a/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.csproj b/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.csproj
new file mode 100644
index 0000000..d4edd88
--- /dev/null
+++ b/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.slnx b/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.slnx
new file mode 100644
index 0000000..99e0a57
--- /dev/null
+++ b/src/DemoCommandLineSrcGen/DemoCommandLineSrcGen.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/DemoCommandLineSrcGen/Program.cs b/src/DemoCommandLineSrcGen/Program.cs
new file mode 100644
index 0000000..b43693a
--- /dev/null
+++ b/src/DemoCommandLineSrcGen/Program.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+#pragma warning disable SA1600 // Elements should be documented
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Ubiquity.NET.CommandLine;
+
+namespace TestNamespace
+{
+ public static class Program
+ {
+ public static async Task Main( string[] args )
+ {
+ // pressing CTRL-C cancels the entire operation
+ using CancellationTokenSource cts = new();
+ Console.CancelKeyPress += ( _, e ) =>
+ {
+ e.Cancel = true;
+ cts.Cancel();
+ };
+
+ // start with information level for parsing; parsed options might specify different level
+ var reporter = new ColoredConsoleReporter( MsgLevel.Information );
+
+ return await TestOptions.BuildRootCommand( ( options, ct ) => AppMainAsync( options, reporter, ct ) )
+ .ParseAndInvokeResultAsync( reporter, cts.Token, args );
+ }
+
+ private static async Task AppMainAsync(
+ TestOptions options,
+ ColoredConsoleReporter reporter,
+ CancellationToken ct
+ )
+ {
+ // Now that args are parsed, if a distinct verbosity level is specified use that
+ if(options.Verbosity != reporter.Level)
+ {
+ reporter = new ColoredConsoleReporter( options.Verbosity );
+ }
+
+ reporter.Verbose( "AppMainAsync" );
+
+ // Core application code here...
+
+ // Use the cancellation token to indicate cancellation
+ // This is set when CTRL-C is pressed in Main() above.
+ ct.ThrowIfCancellationRequested();
+ return 0;
+ }
+ }
+}
diff --git a/src/DemoCommandLineSrcGen/TestOptions.cs b/src/DemoCommandLineSrcGen/TestOptions.cs
new file mode 100644
index 0000000..7f6a0fc
--- /dev/null
+++ b/src/DemoCommandLineSrcGen/TestOptions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+
+using System.CommandLine;
+using System.IO;
+
+using Ubiquity.NET.CommandLine;
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace
+{
+ [RootCommand( Description = "Root command for tests" )]
+ internal partial class TestOptions
+ {
+ [Option( "-o", Description = "Test SomePath" )]
+ [FolderValidation( FolderValidation.CreateIfNotExist )]
+ public required DirectoryInfo SomePath { get; init; }
+
+ [Option( "-v", Description = "Verbosity Level" )]
+ public MsgLevel Verbosity { get; init; } = MsgLevel.Information;
+
+ [Option( "-b", Description = "Test Some existing Path" )]
+ [FolderValidation( FolderValidation.ExistingOnly )]
+ public required DirectoryInfo SomeExistingPath { get; init; }
+
+ [Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
+ public bool Thing1 { get; init; }
+
+ // This should be ignored by generator
+ public string? NotAnOption { get; set; }
+
+ [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomeOtherPath { get; init; }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.Pkg/PackageReadMe.md b/src/Ubiquity.NET.CommandLine.Pkg/PackageReadMe.md
new file mode 100644
index 0000000..4ce264d
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.Pkg/PackageReadMe.md
@@ -0,0 +1,86 @@
+# Ubiquity.NET.CommandLine
+Common Text CommandLine support. This provides a number of support classes for
+Text based UI/UX, including command line parsing extensions. This is generally only relevant
+for console based apps.
+
+## Example Command line parsing
+Normal single command application:
+Options binding types are normally split into two partial classes. The normal types that
+would contain attributes for source generation and the generated partial implementation.
+While there is no source generator (yet) for the options [That is intended for a future
+release] this keeps the responsibilities clearer and aids in migration to a source generator.
+
+### Root command type
+Options.cs:
+```
+using System.IO;
+
+using Ubiquity.NET.CommandLine;
+
+namespace TestSample
+{
+ // Root command for the app, Disable the default options and directive support
+ // This puts the app in total control of the parsing. Normally an app would leave the
+ // defaults but it might not want to for command line syntax compat with a previous
+ // release...
+ [RootCommand(DefaultOptions = DefaultOption.None, DefaultDirectives = DefaultDirective.None )]
+ internal partial class ParsedArgs
+ {
+ [Option("-v", Description="Verbosity Level")]
+ internal MsgLevel Verbosity? { get; init; }
+
+ // Additional options here...
+ }
+}
+```
+
+### Usage in common single command/no command case
+Most applications don't have or use any concept of commands/sub-commands. Thus, there is
+support to make this scenario as simple as possible. [This uses the previously listed
+`ParsedArgs` class.
+
+``` C#
+public static async Task Main( string[] args )
+{
+ // pressing CTRL-C cancels the entire operation
+ using CancellationTokenSource cts = new();
+ Console.CancelKeyPress += ( _, e ) =>
+ {
+ e.Cancel = true;
+ cts.Cancel();
+ };
+
+ // start with information level for parsing; parsed options might specify a different
+ // level `AppMain` will adapt to any new value.
+ var reporter = new ColoredConsoleReporter( MsgLevel.Information );
+
+ return await TestOptions.BuildRootCommand( ( options, ct ) => AppMainAsync( options, reporter, ct ) )
+ .ParseAndInvokeResultAsync( reporter, cts.Token, args );
+}
+
+private static int AppMain( ParsedArgs args, ColoredConsoleReporter reporter )
+{
+ // Real main here.
+ // The pre-parsed and validated command line arguments and the reporter are provided as
+ // arguments to this function
+
+ // Now that args are parsed, if a distinct verbosity level is specified use that
+ if(args.Verbosity != reporter.Level)
+ {
+ reporter = new ColoredConsoleReporter( args.Verbosity );
+ }
+
+ // ...
+}
+```
+This is a simplified usage for the common case of an app without commands/sub-commands.
+
+## Supported Functionality
+`IDiagnosticReporter` interface is at the core of the UX. It is similar in many ways to many
+of the logging interfaces available. The primary distinction is with the ***intention*** of
+use. `IDiagnosticReporter`, specifically, assumes the use for UI/UX rather than a
+debugging/diagnostic log. These have VERY distinct use cases and purposes and generally show
+very different information. (Not to mention the overly complex requirements of
+the anti-pattern DI container/Service Locator assumed in `Microsoft.Extensions.Logging`)
+
+
diff --git a/src/Ubiquity.NET.CommandLine.Pkg/ReadMe.md b/src/Ubiquity.NET.CommandLine.Pkg/ReadMe.md
new file mode 100644
index 0000000..a89fc93
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.Pkg/ReadMe.md
@@ -0,0 +1,24 @@
+# Ubiquity.NET.CommandLine.Pkg
+This is a special project that creates the meta package to reference the library AND the
+associated Roslyn components. This is done without restoring any packages by a "NOTARGETS"
+project that uses an explicit NUSPEC. This is used instead of the CSPROJ built-in support
+for generation of the NuSpec as that would try and restore ALL references first. This causes
+problems for build ordering. Since a CSPROJ will try to restore all packages it will fail to
+restore anything that isn't built yet. While it is plausible to construct build order
+dependencies, such things are not honored when restoring. NuGet package restore happens
+***BEFORE*** build so the packages won't exist.
+
+That limitation is strictly in how CSPROJ system works and not an actual limit of NuGet
+itself. Thus, to workaround that, this project uses a NuSpec file and generates the meta
+package directly (even if the dependencies are not resolved). The package is created
+directly referencing what will exist in the future breaking the dependency cycle/problem.
+
+## Build Caveat
+Due to the nature of this, the version reference must remain constant across all project
+builds. This is true for command line builds but NOT for IDE builds. Each project in an IDE
+build gets a new version. This means that the version references in this package are wrong
+and won't exist. The solution is to build this and all packages from the command line to
+guarantee correct references. This is done automatically in all automated builds as they
+don't use any IDE. Thus, the problem is for local loop development only. It is only an issue
+when using the demo project as that is designed to leverage ONLY the package without any
+project references to validate that works as expected.
diff --git a/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj
new file mode 100644
index 0000000..2bc29f1
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.Pkg.csproj
@@ -0,0 +1,81 @@
+
+
+
+ net10.0;net8.0
+
+
+ false
+ false
+ true
+ true
+
+
+ true
+ Ubiquity.NET.CommandLine.nuspec
+
+
+ Ubiquity.NET.CommandLine
+
+ 4.9.0
+ .NET Foundation,Ubiquity.NET
+ false
+ General use Support for Command line parsing based on System.CommandLine. This is a meta package that references the library and the associated Roslyn components
+ Extensions,.NET,Ubiquity.NET, Console
+ PackageReadMe.md
+ https://github.com/UbiquityDotNET/Ubiquity.NET.Utils
+ https://github.com/UbiquityDotNET/Ubiquity.NET.Utils.git
+ git
+ Apache-2.0 WITH LLVM-exception
+
+
+ false
+ $(NoWarn);NU5128
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ configuration=$(Configuration)
+ $(NuspecProperties);packageID=$(PackageID)
+ $(NuspecProperties);version=$(PackageVersion)
+ $(NuspecProperties);authors=$(Authors)
+ $(NuspecProperties);projectUrl=$(PackageProjectUrl)
+ $(NuspecProperties);description=$(Description)
+ $(NuspecProperties);tags=$(PackageTags)
+ $(NuSpecProperties);licExpression=$(PackageLicenseExpression)
+ $(NuSpecProperties);tfmGroup=$(TargetFramework)
+
+
+
diff --git a/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.nuspec b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.nuspec
new file mode 100644
index 0000000..cb18069
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.Pkg/Ubiquity.NET.CommandLine.nuspec
@@ -0,0 +1,45 @@
+
+
+
+
+ $packageID$
+ $version$
+ $authors$
+ $description$
+ $tags$
+ $licExpression$
+ $projectUrl$
+ PackageReadMe.md
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/AssemblyInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/AssemblyInfo.cs
new file mode 100644
index 0000000..ee0fc35
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/AssemblyInfo.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+// In SDK-style projects such as this one, several assembly attributes that were historically
+// defined in this file are now automatically added during build and populated with
+// values defined in project properties. For details of which attributes are included
+// and how to customize this process see: https://aka.ms/assembly-info-properties
+
+// Setting ComVisible to false makes the types in this assembly not visible to COM
+// components. If you need to access a type in this assembly from COM, set the ComVisible
+// attribute to true on that type.
+
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM.
+
+[assembly: Guid("f17052e2-d54d-454d-b677-f9f8df9b3db8")]
+
+[assembly: Parallelize( Scope = ExecutionScope.MethodLevel )]
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs
new file mode 100644
index 0000000..ac6dd60
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalNamespaceImports.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+global using System;
+global using System.Collections.Generic;
+global using System.Collections.Immutable;
+global using System.Diagnostics.CodeAnalysis;
+global using System.IO;
+global using System.Runtime.InteropServices;
+global using System.Text;
+
+global using Basic.Reference.Assemblies;
+
+global using Microsoft.CodeAnalysis;
+global using Microsoft.CodeAnalysis.CSharp;
+global using Microsoft.CodeAnalysis.Text;
+global using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+global using Ubiquity.NET.SourceGenerator.Test.Utils;
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs
new file mode 100644
index 0000000..a79c00f
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/GlobalSuppression.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Unit Tests" )]
+[assembly: SuppressMessage( "StyleCop.CSharp.DocumentationRules", "SA1652:Enable XML documentation output", Justification = "Unit Tests" )]
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs
new file mode 100644
index 0000000..3373111
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/RootCommandAttributeTests.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ [TestClass]
+ public sealed class RootCommandAttributeTests
+ {
+ public TestContext TestContext { get; set; }
+
+ [TestMethod]
+ public void Basic_golden_path_succeeds( )
+ {
+ var sourceGenerator = new CommandGenerator().AsSourceGenerator();
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(
+ generators: [sourceGenerator],
+ driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)
+ );
+
+ SourceText input = TestHelpers.GetTestText(nameof(RootCommandAttributeTests), "input.cs");
+ SourceText expected = TestHelpers.GetTestText(nameof(RootCommandAttributeTests), "expected.cs");
+
+ CSharpCompilation compilation = CreateCompilation(input, TestProgramCSPath);
+ var diagnostics = compilation.GetDiagnostics( TestContext.CancellationToken );
+ foreach(var diagnostic in diagnostics)
+ {
+ TestContext.WriteLine( diagnostic.ToString() );
+ }
+
+ Assert.HasCount( 0, diagnostics );
+
+ var results = driver.RunGeneratorAndAssertResults(compilation, [TrackingNames.CommandClass]);
+ Assert.IsEmpty( results.Diagnostics, "Should not have ANY diagnostics reported during generation" );
+
+ // validate the generated trees have the correct count and names
+ Assert.HasCount( 1, results.GeneratedTrees, "Should create 1 'files' during generation" );
+ for(int i = 0; i < results.GeneratedTrees.Length; ++i)
+ {
+ string expectedName = GeneratedFilePaths[i];
+ SyntaxTree tree = results.GeneratedTrees[i];
+
+ Assert.AreEqual( expectedName, tree.FilePath, "Generated files should use correct name" );
+ Assert.AreEqual( Encoding.UTF8, tree.Encoding, $"Generated files should use UTF8. [{expectedName}]" );
+ }
+
+ SourceText actual = results.GeneratedTrees[0].GetText( TestContext.CancellationToken );
+ string uniDiff = expected.UniDiff(actual);
+ if(!string.IsNullOrWhiteSpace( uniDiff ))
+ {
+ TestContext.WriteLine( uniDiff );
+ Assert.Fail( "No Differences Expected" );
+ }
+ }
+
+ // simple helper for these tests to create a C# Compilation
+ internal static CSharpCompilation CreateCompilation(
+ SourceText source,
+ string path,
+ CSharpParseOptions? parseOptions = default,
+ CSharpCompilationOptions? compileOptions = default,
+ List? references = default
+ )
+ {
+ parseOptions ??= new CSharpParseOptions(LanguageVersion.CSharp14, DocumentationMode.None);
+ compileOptions ??= new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable );
+
+ // Default to .NET 10 if not specified.
+ references ??= [ .. Net100.References.All ];
+ references.Add( MetadataReference.CreateFromFile( Path.Combine( Environment.CurrentDirectory, "Ubiquity.NET.CommandLine.dll" ) ) );
+
+ return CSharpCompilation.Create( "TestAssembly",
+ [ CSharpSyntaxTree.ParseText( source, parseOptions, path ) ],
+ references,
+ compileOptions
+ );
+ }
+
+ private const string TestProgramCSPath = @"input.cs";
+
+ private readonly ImmutableArray GeneratedFilePaths
+ = [
+ @"Ubiquity.NET.CommandLine.SrcGen\Ubiquity.NET.CommandLine.SrcGen.CommandGenerator\TestNamespace.TestOptions.g.cs",
+ ];
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/ReadMe.md b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/ReadMe.md
new file mode 100644
index 0000000..e2436eb
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/ReadMe.md
@@ -0,0 +1,39 @@
+# TestFiles
+These are test files used as input, expected content etc...
+
+They are `Embedded Resources` that are retrievable by `Assembly.GetManafiestResourceStream()`.
+To simplify things the `TestHelpers` class provides overloads that follow the patterns
+described here.
+
+## Naming Patterns
+
+```
+📁 TestFiles
+└─📁
+ ├─📄
+ └─📄
+```
+Where `` is the name of the test (in code that is usually `nameof(T)`).
+
+>[!IMPORTANT]
+> It is important to note that .NET considers resource names as case sensitive. While
+> Microsoft Windows normally uses a case preserving but insensitive match for file names the
+> resource names are captured from the underlying FS and are case preserving. Thus, the case
+> of file names ***MUST*** match the name used to retrieve it as a resource.
+
+### Final Resource Name
+The full resource name pattern is:
+`...`
+
+where:
+
+| Name | Description |
+|------|-------------|
+| `` | Default namespace for the assembly (Technically the namespace that contains `TestHelpers`.|
+| `` | The name of the folder containing this file. |
+| `` | The name of the tests (as a sub-folder of the one containing this file) |
+| `` | The name of the file used by the tests. |
+
+This ensures the contents are easily retrieved using just a test name (via `nameof(T)`) and
+the name of the file of interest. Everything else is formed from the pattern and doesn't
+change for each file/test.
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/expected.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/expected.cs
new file mode 100644
index 0000000..d70daab
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/expected.cs
@@ -0,0 +1,74 @@
+// ------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// testhost [18.0.1]
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+// ------------------------------------------------------------------------------
+using static global::Ubiquity.NET.CommandLine.SymbolValidationExtensions;
+
+namespace TestNamespace
+{
+ internal partial class TestOptions
+ : global::Ubiquity.NET.CommandLine.IRootCommandBuilderWithSettings
+ , global::Ubiquity.NET.CommandLine.ICommandBinder
+ {
+ public static global::Ubiquity.NET.CommandLine.CommandLineSettings Settings => new();
+
+ public static TestOptions Bind( global::System.CommandLine.ParseResult parseResult )
+ {
+ return new()
+ {
+ SomePath = parseResult.GetValue( Descriptors.SomePath ),
+ SomeExistingPath = parseResult.GetValue( Descriptors.SomeExistingPath ),
+ Thing1 = parseResult.GetRequiredValue( Descriptors.Thing1 ),
+ SomeOtherPath = parseResult.GetValue( Descriptors.SomeOtherPath ),
+ };
+ }
+
+ public static global::Ubiquity.NET.CommandLine.AppControlledDefaultsRootCommand Build( )
+ {
+ return new global::Ubiquity.NET.CommandLine.AppControlledDefaultsRootCommand( Settings, "Root command for tests" )
+ {
+ Descriptors.SomePath,
+ Descriptors.SomeExistingPath,
+ Descriptors.Thing1,
+ Descriptors.SomeOtherPath,
+ };
+ }
+ }
+
+ file static class Descriptors
+ {
+ internal static readonly global::System.CommandLine.Option SomePath
+ = new global::System.CommandLine.Option("-o")
+ {
+ Description = "Test SomePath",
+ }.EnsureFolder();
+
+ internal static readonly global::System.CommandLine.Option SomeExistingPath
+ = new global::System.CommandLine.Option("-b")
+ {
+ Description = "Test Some existing Path",
+ }.AcceptExistingFolderOnly();
+
+ internal static readonly global::System.CommandLine.Option Thing1
+ = new global::System.CommandLine.Option("--thing1", "-t")
+ {
+ HelpName = "Help name for thing1",
+ Description = "Test Thing1",
+ Required = true,
+ };
+
+ internal static readonly global::System.CommandLine.Option SomeOtherPath
+ = new global::System.CommandLine.Option("-a")
+ {
+ Description = "Test SomeOtherPath",
+ Required = false,
+ Hidden = true,
+ Arity = new global::System.CommandLine.ArgumentArity(0, 3),
+ }.AcceptExistingFileOnly();
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/input.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/input.cs
new file mode 100644
index 0000000..0f3bfe8
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestFiles/RootCommandAttributeTests/input.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+[RootCommand( Description = "Root command for tests" )]
+internal partial class TestOptions
+{
+ [Option( "-o", Description = "Test SomePath" )]
+ [FolderValidation( FolderValidation.CreateIfNotExist )]
+ public required DirectoryInfo SomePath { get; init; }
+
+ [Option( "-b", Description = "Test Some existing Path" )]
+ [FolderValidation( FolderValidation.ExistingOnly )]
+ public required DirectoryInfo SomeExistingPath { get; init; }
+
+ [Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
+ public bool Thing1 { get; init; }
+
+ // This should be ignored by generator
+ public string? NotAnOption { get; set; }
+
+ [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomeOtherPath { get; init; }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs
new file mode 100644
index 0000000..e266a2e
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/TestHelpers.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ internal static class TestHelpers
+ {
+ public static Stream GetTestResourceStream( string testName, string name )
+ {
+ // Example:
+ // Ubiquity.NET.CommandLine.SrcGen.UT.TestFiles.RootCommandAttributeTests.expected.cs
+ // nameof(T) doesn't work for the namespace, so use reflection information to form the
+ // name with a well defined pattern. (refactoring safe)
+ var myType = typeof(TestHelpers);
+ string resourceName = $"{myType.Namespace}.{TestFilesFolderName}.{testName}.{name}";
+ return myType.Assembly.GetManifestResourceStream( resourceName )
+ ?? throw new InvalidOperationException( $"Resource '{resourceName}' not found" );
+ }
+
+ public static SourceText GetTestText( string testName, string name )
+ {
+ using var strm = GetTestResourceStream( testName, name );
+ return SourceText.From( strm );
+ }
+
+ private const string TestFilesFolderName = "TestFiles";
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj b/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj
new file mode 100644
index 0000000..1cb51da
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/Ubiquity.NET.CommandLine.SrcGen.UT.csproj
@@ -0,0 +1,48 @@
+
+
+
+ net10.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen.UT/UniDiffExtensions.cs b/src/Ubiquity.NET.CommandLine.SrcGen.UT/UniDiffExtensions.cs
new file mode 100644
index 0000000..8af12f4
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen.UT/UniDiffExtensions.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using DiffPlex.Renderer;
+
+namespace Ubiquity.NET.CommandLine.SrcGen.UT
+{
+ internal static class UniDiffExtensions
+ {
+ public static string UniDiff( this SourceText self, SourceText other, string leftFileName = "expected", string rightFileName = "actual" )
+ {
+ return UniDiff( self.ToString(), other, leftFileName, rightFileName );
+ }
+
+ public static string UniDiff( this string self, SourceText other, string leftFileName = "expected", string rightFileName = "actual" )
+ {
+ var renderer = new UnidiffRenderer(contextLines: 1);
+ return renderer.Generate(
+ self,
+ other.ToString(),
+ leftFileName,
+ rightFileName,
+ ignoreWhitespace: false
+ );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs
new file mode 100644
index 0000000..fecf04c
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandGenerator.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Linq;
+
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ /// Roslyn Source generator to generate backing support for command line parsing
+ [Generator]
+ public class CommandGenerator
+ : IIncrementalGenerator
+ {
+ ///
+ public void Initialize( IncrementalGeneratorInitializationContext context )
+ {
+ // NOTE: if the tests aren't finding anything double check that Constants.* contains the correct namespace name.
+
+ string rootCommandAttribName = Constants.RootCommandAttribute.ToString(); // Don't use alias or global prefix for this
+
+ var optionClasses
+ = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ rootCommandAttribName,
+ predicate: static (s, _) => true,
+ transform: static (ctx, _) => CollectCommandAttributeData( ctx )
+ ) // filter for command attribute only
+ .Where( static m => m is not null )
+ .Select( static (m, ct) => (RootCommandInfo)m! ) // convert nullable type to non null as preceding where clause filters out null values
+ .WithTrackingName( TrackingNames.CommandClass );
+
+ context.RegisterSourceOutput( optionClasses, Execute );
+ }
+
+ private static RootCommandInfo? CollectCommandAttributeData( GeneratorAttributeSyntaxContext context )
+ {
+ // Do nothing if the target doesn't support what the generated code needs or something is wrong.
+ // Errors are detected by a distinct analyzer; code generators just NOP as fast as possible.
+ var compilation = context.SemanticModel.Compilation;
+ if( context.Attributes.Length != 1 // Multiple instances not allowed and 0 is just broken.
+ || compilation.Language != "C#"
+ || !compilation.HasLanguageVersionAtLeastEqualTo(LanguageVersion.CSharp13)
+ || context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol
+ || context.TargetNode is not ClassDeclarationSyntax commandClass
+ )
+ {
+ return null;
+ }
+
+ var theAttribute = context.Attributes[0];
+
+ // Capture the names of all properties with a generating attribute.
+ // This starts out assuming all members are a property. This is a bit aggressive
+ // but ensures only one allocation of the result is needed for all (That is, all
+ // members is the max size needed so allocate that amount once).
+ var members = namedTypeSymbol.GetMembers();
+ var propertyInfoBuilder = ImmutableArray.CreateBuilder( members.Length );
+ foreach(ISymbol member in members)
+ {
+ // filter to referenceable properties
+ if(member.CanBeReferencedByName && member is IPropertySymbol propSym)
+ {
+ var attributes = propSym.CaptureMatchingAttributes( Constants.GeneratingAttributeNames );
+ if(attributes.Length > 0 )
+ {
+ var propInfo = new PropertyInfo( propSym.Name, propSym.Type.GetNamespaceQualifiedName(), attributes );
+ propertyInfoBuilder.Add( propInfo );
+ }
+ }
+ }
+
+ return new RootCommandInfo(
+ namedTypeSymbol.GetNamespaceQualifiedName(),
+ theAttribute,
+ propertyInfoBuilder.ToImmutable()
+ );
+ }
+
+ private static void Execute( SourceProductionContext context, RootCommandInfo source )
+ {
+ var template = new Templates.RootCommandClassTemplate(source);
+ var generatedSource = template.GenerateText();
+ context.AddSource( $"{source.TargetName:R}.g.cs", generatedSource );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/CommandLinePolyFill.cs b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLinePolyFill.cs
new file mode 100644
index 0000000..0baf59b
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/CommandLinePolyFill.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ // Roslyn generators **MUST** target .NET standard 2.0; However the CommandLine assembly
+ // only supports .NET 8.0 and .NET 10. So this fills in the gaps on enumerations as the
+ // library with the declarations can't be referenced directly.
+
+ /// Flags to determine the default Options for an
+ [Flags]
+ internal enum DefaultOption
+ {
+ /// No default options used
+ None = 0,
+
+ /// Include the default help option
+ Help,
+
+ /// Include the default version option
+ Version,
+ }
+
+ /// Flags to determine the default directives supported for an
+ [Flags]
+ internal enum DefaultDirective
+ {
+ /// No default directives included
+ None = 0,
+
+ /// Include support for
+ Suggest,
+
+ /// Include support for
+ Diagram,
+
+ /// Include support for
+ EnvironmentVariables,
+ }
+
+ /// Enumeration for folder validation
+ internal enum FileValidation
+ {
+ /// No validation
+ None,
+
+ /// Existing files only accepted.
+ ///
+ /// If a file specified does not exist then an exception results from the validation stage of
+ /// processing the command line.
+ ///
+ ExistingOnly,
+ }
+
+ /// Enumeration for folder validation
+ internal enum FolderValidation
+ {
+ /// No validation
+ None,
+
+ /// Creates the folder if it doesn't exist
+ CreateIfNotExist,
+
+ /// Existing folders only accepted.
+ ///
+ /// If a folder specified does not exist then an exception results from the validation stage of
+ /// processing the command line.
+ ///
+ ExistingOnly,
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs
new file mode 100644
index 0000000..44d76a6
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Constants.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ // Implementation Note:
+ // use of pattern `string X = nameof(X)` allows for refactoring the name without the problem
+ // of repetitive typo errors.
+ //
+ // These name values are needed as the target DLL is NOT referenceable by this generator
+ // and therefore does not have anyway to resolve the names.
+
+ internal static class Constants
+ {
+ internal static readonly IEnumerable SystemIONamespaceParts = [ "System", "IO" ];
+
+ internal static readonly IEnumerable SystemCommandLineNamespaceParts = [ "System", "CommandLine" ];
+
+ internal static readonly IEnumerable UbiquityNETCommandLineNamespaceParts = [ "Ubiquity", "NET", "CommandLine" ];
+
+ internal static readonly IEnumerable GeneratorAttributesNamespaceParts = UbiquityNETCommandLineNamespaceParts.Append("GeneratorAttributes");
+
+ internal static string UbiquityNETCommandLineNamespaceName { get; } = string.Join(".", UbiquityNETCommandLineNamespaceParts);
+
+ internal static readonly NamespaceQualifiedName RootCommandAttribute
+ = new( GeneratorAttributesNamespaceParts, nameof(RootCommandAttribute));
+
+ internal static readonly NamespaceQualifiedName OptionAttribute
+ = new( GeneratorAttributesNamespaceParts, nameof(OptionAttribute));
+
+ internal static readonly NamespaceQualifiedName FileValidationAttribute
+ = new( GeneratorAttributesNamespaceParts, nameof(FileValidationAttribute));
+
+ internal static readonly NamespaceQualifiedName FolderValidationAttribute
+ = new( GeneratorAttributesNamespaceParts, nameof(FolderValidationAttribute));
+
+ internal static readonly NamespaceQualifiedName IRootCommandBuilderWithSettings
+ = new( UbiquityNETCommandLineNamespaceParts, nameof(IRootCommandBuilderWithSettings));
+
+ [SuppressMessage( "StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "Generic type - no other way to represent name in C# literal" )]
+ internal static readonly NamespaceQualifiedName ICommandBinder
+ = new( UbiquityNETCommandLineNamespaceParts, nameof(ICommandBinder)); // Actual name is ICommandBinder`1, but that is not needed for code gen
+
+ internal static readonly NamespaceQualifiedName AppControlledDefaultsRootCommand
+ = new( UbiquityNETCommandLineNamespaceParts, nameof(AppControlledDefaultsRootCommand));
+
+ internal static readonly NamespaceQualifiedName SymbolValidationExtensions
+ = new( UbiquityNETCommandLineNamespaceParts, nameof(SymbolValidationExtensions));
+
+ internal static readonly NamespaceQualifiedName CommandLineSettings
+ = new( UbiquityNETCommandLineNamespaceParts, nameof(CommandLineSettings));
+
+ internal static readonly NamespaceQualifiedName ArgumentArity
+ = new( SystemCommandLineNamespaceParts, nameof(ArgumentArity));
+
+ internal static readonly NamespaceQualifiedName Option
+ = new( SystemCommandLineNamespaceParts, nameof(Option));
+
+ internal static readonly NamespaceQualifiedName ParseResult
+ = new( SystemCommandLineNamespaceParts, nameof(ParseResult));
+
+ internal static readonly NamespaceQualifiedName DirectoryInfo
+ = new( SystemIONamespaceParts, nameof(DirectoryInfo));
+
+ internal static readonly NamespaceQualifiedName FileInfo
+ = new( SystemIONamespaceParts, nameof(FileInfo));
+
+ // Names for the attributes to allow quickly filtering to a matching
+ // simple name before testing if it's full name is a match
+ internal static readonly ImmutableArray GeneratingAttributeNames
+ = [
+ RootCommandAttribute,
+ OptionAttribute,
+ FolderValidationAttribute,
+ FileValidationAttribute,
+ ];
+
+ internal static class CommonAttributeNamedArgs
+ {
+ internal const string Required = nameof(Required);
+ }
+
+ internal static class RootCommandAttributeNamedArgs
+ {
+ internal const string Description = nameof(Description);
+ internal const string ShowHelpOnErrors = nameof(ShowHelpOnErrors);
+ internal const string ShowTypoCorrections = nameof(ShowTypoCorrections);
+ internal const string EnablePosixBundling = nameof(EnablePosixBundling);
+ internal const string DefaultOptions = nameof(DefaultOptions);
+ internal const string DefaultDirectives = nameof(DefaultDirectives);
+ }
+
+ internal static class OptionAttributeNamedArgs
+ {
+ internal const string HelpName = nameof(HelpName);
+ internal const string Aliases = nameof(Aliases);
+ internal const string Description = nameof(Description);
+ internal const string Required = CommonAttributeNamedArgs.Required;
+ internal const string Hidden = nameof(Hidden);
+ internal const string ArityMin = nameof(ArityMin);
+ internal const string ArityMax = nameof(ArityMax);
+ internal const string DefaultValueFactoryName = nameof(DefaultValueFactoryName);
+ internal const string CustomParserName = nameof(CustomParserName);
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/GlobalNamespaceImports.cs b/src/Ubiquity.NET.CommandLine.SrcGen/GlobalNamespaceImports.cs
new file mode 100644
index 0000000..12dfc85
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/GlobalNamespaceImports.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+global using System;
+global using System.CodeDom.Compiler;
+global using System.Collections.Generic;
+global using System.Diagnostics.CodeAnalysis;
+global using System.Text;
+
+global using Microsoft.CodeAnalysis;
+global using Microsoft.CodeAnalysis.CSharp.Syntax;
+global using Microsoft.CodeAnalysis.Text;
+
+global using Ubiquity.NET.CodeAnalysis.Utils;
+global using Ubiquity.NET.SrcGeneration;
+global using Ubiquity.NET.SrcGeneration.CSharp;
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs
new file mode 100644
index 0000000..dcabdb8
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/OptionInfo.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ /// Wrapper for for an Option Attribute
+ ///
+ /// This is a simple wrapper around a managed reference. The size of this struct is no different
+ /// than the managed reference it wraps as that is the only field. This type supplies a validating
+ /// constructor and accessor to simplify access to the captured data.
+ ///
+ internal readonly record struct OptionInfo
+ {
+ public OptionInfo( EquatableAttributeData attributeInfo )
+ {
+ // Runtime sanity check in debug mode
+ DebugAssert.StructSizeOK();
+
+ if( attributeInfo.Name != Constants.OptionAttribute)
+ {
+ throw new ArgumentException("Not an option attribute!");
+ }
+
+ AttributeInfo = attributeInfo;
+ }
+
+ public bool IsValid => !string.IsNullOrWhiteSpace(Name);
+
+ public string Name => AttributeInfo.ConstructorArguments.Length < 1
+ ? string.Empty
+ : AttributeInfo.ConstructorArguments[0].Value as string ?? string.Empty;
+
+ public Optional HelpName => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.HelpName );
+
+ public Optional> Aliases => AttributeInfo.GetNamedArgValueArray( Constants.OptionAttributeNamedArgs.Aliases );
+
+ public Optional Description => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Description );
+
+ public Optional Required => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Required );
+
+ public Optional Hidden => AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.Hidden );
+
+ public Optional<(int Min, int Max)> Arity
+ {
+ get
+ {
+ // ignore argument if both aren't available
+ Optional min = AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.ArityMin);
+ Optional max = AttributeInfo.GetNamedArgValue( Constants.OptionAttributeNamedArgs.ArityMax);
+
+ return min.HasValue && max.HasValue
+ ? new((min.Value, max.Value))
+ : default;
+ }
+ }
+
+ private readonly EquatableAttributeData AttributeInfo;
+
+ public static implicit operator OptionInfo(EquatableAttributeData attributeInfo)
+ {
+ return new(attributeInfo);
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/PackageReadMe.md b/src/Ubiquity.NET.CommandLine.SrcGen/PackageReadMe.md
new file mode 100644
index 0000000..1fcc108
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/PackageReadMe.md
@@ -0,0 +1,105 @@
+# Ubiquity.NET.CommandLine.SrcGen
+This is the Roslyn Source generator for Ubiquity.NET.CommandLine.
+
+## Attributes Controlling generation
+| Name | Description |
+|------------------|-------------|
+| RootCommandAttribute | Applied to a class to generate bindings for the root command |
+| CommandAttribute | Applied to a sub-command class to indicate support for a sub-command |
+| FileValidationAttribute | Applied `System.IO.FileInfo` properties to validate the input |
+| FolderValidationAttribute | Applied `System.IO.DirectoryInfo` properties to validate the input |
+| OptionAttribute | Applied to properties of a class marked with either 'CommandAttribute' or `RootCommandAttribute`]
+
+### Example
+
+CommandLineOptions.cs
+``` C#
+using System.IO;
+using Ubiquity.NET.CommandLine.GeneratorAttributes;
+
+namespace TestNamespace;
+
+[RootCommand( Description = "Root command for tests" )]
+internal partial class TestOptions
+{
+ [Option( "-o", Description = "Test SomePath" )]
+ [FolderValidation( FolderValidation.CreateIfNotExist )]
+ public required DirectoryInfo SomePath { get; init; }
+
+ [Option( "-v", Description = "Verbosity Level" )]
+ public MsgLevel Verbosity { get; init; } = MsgLevel.Information;
+
+ [Option( "-b", Description = "Test Some existing Path" )]
+ [FolderValidation( FolderValidation.ExistingOnly )]
+ public required DirectoryInfo SomeExistingPath { get; init; }
+
+ [Option( "--thing1", Aliases = [ "-t" ], Required = true, Description = "Test Thing1", HelpName = "Help name for thing1" )]
+ public bool Thing1 { get; init; }
+
+ // This should be ignored by generator
+ public string? NotAnOption { get; set; }
+
+ [Option( "-a", Hidden = true, Required = false, ArityMin = 0, ArityMax = 3, Description = "Test SomeOtherPath" )]
+ [FileValidation( FileValidation.ExistingOnly )]
+ public required FileInfo SomeOtherPath { get; init; }
+}
+```
+
+Program.cs
+``` C#
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Ubiquity.NET.CommandLine;
+
+namespace TestNamespace
+{
+ public static class Program
+ {
+ public static async Task Main( string[] args )
+ {
+ // pressing CTRL-C cancels the entire operation
+ using CancellationTokenSource cts = new();
+ Console.CancelKeyPress += ( _, e ) =>
+ {
+ e.Cancel = true;
+ cts.Cancel();
+ };
+
+ // start with information level for parsing; parsed options might specify different level
+ var reporter = new ColoredConsoleReporter( MsgLevel.Information );
+
+ return await TestOptions.BuildRootCommand( ( options, ct ) => AppMainAsync( options, reporter, ct ) )
+ .ParseAndInvokeResultAsync( reporter, cts.Token, args );
+ }
+
+ private static async Task AppMainAsync(
+ TestOptions options,
+ ColoredConsoleReporter reporter,
+ CancellationToken ct
+ )
+ {
+ // Now that args are parsed, if a distinct verbosity level is specified use that
+ if(options.Verbosity != reporter.Level)
+ {
+ reporter = new ColoredConsoleReporter( options.Verbosity );
+ }
+
+ reporter.Verbose( "AppMainAsync" );
+
+ // Core application code here...
+
+ // Use the cancellation token to indicate cancellation
+ // This is set when CTRL-C is pressed in Main() above.
+ ct.ThrowIfCancellationRequested();
+ return 0;
+ }
+ }
+}
+```
+
+---
+> [!NOTE]
+> `CommandAttribute`, and sub-commands in general, is not ***currently*** supported.
+
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/PropertyInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen/PropertyInfo.cs
new file mode 100644
index 0000000..a2e363f
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/PropertyInfo.cs
@@ -0,0 +1,96 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ internal class PropertyInfo
+ : IEquatable
+ {
+ /// Initializes a new instance of the class.
+ /// Simple name of this property
+ /// Name of the type of this property
+ /// Map of Attributes for this property, keyed by the name of the attribute
+ public PropertyInfo(
+ string simpleName,
+ NamespaceQualifiedName typeName,
+ EquatableAttributeDataCollection attributes
+ )
+ {
+ SimpleName = simpleName;
+ TypeName = typeName;
+ Attributes = attributes;
+ }
+
+ /// Gets a value indicating whether this instance is default constructed
+ public bool IsDefault => SimpleName is null
+ && TypeName is null
+ && Attributes is null;
+
+ /// Gets the simple name of this property
+ public string SimpleName { get; }
+
+ /// Gets the namespace qualified name of the type of this property
+ public NamespaceQualifiedName TypeName { get; }
+
+ /// Gets a dictionary of generating attributes for this property
+ ///
+ /// Ordinarily a generator pipeline doesn't capture this data and instead has a pipeline for each attribute.
+ /// However, in the case of generating the binder, it needs to generate different code if the contributing
+ /// property is "required".
+ ///
+ public EquatableAttributeDataCollection Attributes { get; }
+
+ /// Gets a value indicating whether this property is required
+ ///
+ /// The concept "required" applies to the execution of the action for the command line. If a required
+ /// value is not provided (and no default is set), then an error is produced at the time of invocation.
+ ///
+ /// Each attribute is different but the "Required" named argument is designed for consistency across all.
+ /// Therefore, this will use a common name.
+ ///
+ ///
+ public bool IsRequired
+ {
+ get
+ {
+ foreach(var attr in Attributes)
+ {
+ var required = attr.GetNamedArgValue(Constants.CommonAttributeNamedArgs.Required);
+ if(required.HasValue && (bool)(required.Value.Value!))
+ {
+ return true;
+ }
+ }
+
+ // no attribute with the correct named arg set to "true"
+ return false;
+ }
+ }
+
+ public bool Equals( PropertyInfo other )
+ {
+ bool retVal = SimpleName == other.SimpleName
+ && TypeName == other.TypeName
+ && StructuralComparisons.StructuralEqualityComparer.Equals(Attributes, other.Attributes);
+
+ return retVal;
+ }
+
+ public override bool Equals( object obj )
+ {
+ return obj is PropertyInfo other
+ && Equals( other );
+ }
+
+ public override int GetHashCode( )
+ {
+ return HashCode.Combine(
+ SimpleName,
+ TypeName,
+ StructuralComparisons.StructuralEqualityComparer.GetHashCode(Attributes)
+ );
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/RootCommandInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen/RootCommandInfo.cs
new file mode 100644
index 0000000..ed49766
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/RootCommandInfo.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections;
+using System.Collections.Immutable;
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ internal readonly struct RootCommandInfo
+ : IEquatable
+ {
+ public RootCommandInfo(
+ NamespaceQualifiedName targetName,
+ EquatableAttributeData attributeInfo,
+ ImmutableArray properties
+ )
+ {
+ TargetName = targetName ?? throw new ArgumentNullException( nameof( targetName ) );
+ AttributeInfo = attributeInfo;
+ Properties = properties;
+ }
+
+ public NamespaceQualifiedName TargetName { get; }
+
+ public EquatableAttributeData AttributeInfo { get; }
+
+ public ImmutableArray Properties { get; }
+
+ // This is used to simplify the code generation to limit
+ // to the default settings if not explicitly overridden.
+ public bool HasSettings
+ {
+ get
+ {
+ // If the attribute has any named args, beside "Description" it's a setting
+ // so as soon as one is found report settings are present.
+ foreach(string argName in AttributeInfo.NamedArguments.Keys)
+ {
+ if(argName != Constants.RootCommandAttributeNamedArgs.Description)
+ {
+ // Currently, all non-description args are settings
+ return true;
+ }
+ }
+
+ // no named args or none that are not the description
+ return false;
+ }
+ }
+
+ public Optional Description => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.Description );
+
+ public Optional ShowHelpOnErrors => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.ShowHelpOnErrors );
+
+ public Optional ShowTypoCorrections => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.ShowTypoCorrections );
+
+ public Optional EnablePosixBundling => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.EnablePosixBundling );
+
+ public Optional DefaultOptions => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.DefaultOptions );
+
+ public Optional DefaultDirectives => AttributeInfo.GetNamedArgValue( Constants.RootCommandAttributeNamedArgs.DefaultDirectives );
+
+ public bool Equals( RootCommandInfo other )
+ {
+ bool retVal = TargetName.Equals(other.TargetName)
+ && AttributeInfo.Equals(other.AttributeInfo)
+ && StructuralComparisons.StructuralEqualityComparer.Equals(Properties, other.Properties);
+
+ return retVal;
+ }
+
+ public override bool Equals( object obj )
+ {
+ return obj is RootCommandInfo other
+ && Equals( other );
+ }
+
+ public override int GetHashCode( )
+ {
+ return HashCode.Combine(TargetName, AttributeInfo, Properties);
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ISourceGenTemplate.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ISourceGenTemplate.cs
new file mode 100644
index 0000000..1eda713
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ISourceGenTemplate.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen.Templates
+{
+ // Implementation Note: This is NOT in Ubiquity.NET.SrcGeneration to prevent the need for
+ // that to take a dependency on Mirosoft.CodeAnalyis just for the SourceText
+ // result. Though a version that returns a StringBuilder might be useful...
+
+ /// Interface for a source generating template
+ ///
+ /// This simple interface defines the minimal requirements of a source generating
+ /// template. It is based on the common use of generated T4 wrapper classes and
+ /// intended for compatibility with those. (Though it uses
+ /// instead of `TransformText` and that method returns
+ /// instead of ) This aids in transitioning such implementations
+ /// to this new form of templating, especially for Roslyn Source generators.
+ ///
+ public interface ISourceGenTemplate
+ {
+ /// Transforms the input and properties for the template into a
+ /// Generated textual representation
+ SourceText GenerateText( );
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs
new file mode 100644
index 0000000..3704b30
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/RootCommandClassTemplate.cs
@@ -0,0 +1,323 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Ubiquity.NET.CommandLine.SrcGen.Templates
+{
+ internal class RootCommandClassTemplate
+ : ISourceGenTemplate
+ {
+ internal RootCommandClassTemplate( RootCommandInfo info )
+ {
+ Info = info;
+ }
+
+ public RootCommandInfo Info { get; }
+
+ public ImmutableArray Properties => Info.Properties;
+
+ public string ClassName => Info.TargetName.SimpleName;
+
+ public string Namespace => Info.TargetName.Namespace;
+
+ [SuppressMessage("Style", "IDE0063:Use simple 'using' statement", Justification = "Scoping intent is clearer without them")]
+ public SourceText GenerateText( )
+ {
+ var srcTxt = new StringBuilderText(Encoding.UTF8);
+ using var simpleWriter = srcTxt.CreateWriter();
+ using var writer = new IndentedTextWriter(simpleWriter);
+
+ writer.WriteAutoGeneratedComment(ToolInfo.Name, ToolInfo.Version);
+ writer.WriteEmptyLine();
+
+ writer.WriteLine($"using static {Constants.SymbolValidationExtensions:G};");
+ writer.WriteEmptyLine();
+
+ using (writer.Namespace(Namespace, writeClosingNewLine: true ))
+ {
+ writer.WriteLine("internal partial class {0}", ClassName);
+ using (var scope = writer.PushIndent())
+ {
+ writer.WriteLine($": {Constants.IRootCommandBuilderWithSettings:G}");
+ writer.WriteLine($", {Constants.ICommandBinder:G}<{ClassName}>");
+ }
+
+ using (var typeScope = writer.Scope(writeClosingNewLine: true))
+ {
+ WriteSettingsProperty(writer);
+ writer.WriteEmptyLine();
+
+ WriteBindMethod(writer);
+ writer.WriteEmptyLine();
+
+ WriteBuildCommandMethod(writer);
+ }
+
+ writer.WriteEmptyLine();
+ WriteDescriptors( writer );
+ }
+
+ writer.Flush();
+ return srcTxt;
+ }
+
+ private void WriteBindMethod( IndentedTextWriter writer )
+ {
+ using var bindMethodScope = writer.Scope($"public static {ClassName} Bind( {Constants.ParseResult:G} parseResult )", writeClosingNewLine: true);
+ using (var allocationScope = writer.Scope("return new()"))
+ {
+ foreach (PropertyInfo info in Properties)
+ {
+ string methodName = info.IsRequired ? "GetRequiredValue" : "GetValue";
+ writer.WriteLine($"{info.SimpleName} = parseResult.{methodName}( Descriptors.{info.SimpleName} ),");
+ }
+ }
+
+ writer.WriteLine(";");
+ }
+
+ private void WriteBuildCommandMethod( IndentedTextWriter writer )
+ {
+ string methodDecl = $"public static {Constants.AppControlledDefaultsRootCommand:G} Build( )";
+ using var buildRootCommandScope = writer.Scope(methodDecl, writeClosingNewLine: true);
+
+ string returnLine = Info.Description.HasValue
+ ? $"return new {Constants.AppControlledDefaultsRootCommand:G}( Settings, \"{Info.Description.Value}\" )"
+ : $"return new {Constants.AppControlledDefaultsRootCommand:G}( Settings )";
+
+ using (var allocationScope = writer.Scope(returnLine))
+ {
+ foreach (PropertyInfo property in Properties)
+ {
+ writer.WriteLine($"Descriptors.{property.SimpleName},");
+ }
+ }
+
+ writer.WriteLine(";");
+ }
+
+ private void WriteSettingsProperty( IndentedTextWriter writer )
+ {
+ writer.Write($"public static {Constants.CommandLineSettings:G} Settings => new()");
+ if (Info.HasSettings)
+ {
+ writer.WriteLine();
+ using var offset = writer.PushIndent();
+ using var initScope = writer.Scope(writeClosingNewLine: false);
+ {
+ if (Info.ShowHelpOnErrors.HasValue)
+ {
+ writer.WriteLine("ShowHelpOnErrors = {0},", CSharpLanguage.AsLiteral( Info.ShowHelpOnErrors.Value ));
+ }
+
+ if (Info.ShowTypoCorrections.HasValue)
+ {
+ writer.WriteLine("ShowTypoCorrections = {0},", CSharpLanguage.AsLiteral( Info.ShowTypoCorrections.Value ));
+ }
+
+ if (Info.EnablePosixBundling.HasValue)
+ {
+ writer.WriteLine("EnablePosixBundling = {0},", CSharpLanguage.AsLiteral( Info.EnablePosixBundling.Value ));
+ }
+
+ if (Info.DefaultOptions.HasValue)
+ {
+ writer.WriteLine("DefaultOptions = {0},", Info.DefaultOptions.Value);
+ }
+
+ if (Info.DefaultDirectives.HasValue)
+ {
+ writer.WriteLine("DefaultDirectives = {0},", Info.DefaultDirectives.Value);
+ }
+
+ // TODO: ResponseFileTokenReplacer
+ }
+ }
+
+ writer.WriteLine(";");
+ }
+
+ private void WriteDescriptors( IndentedTextWriter writer )
+ {
+ using var descriptorScope = writer.Class("file static", "Descriptors", writeClosingNewLine: true);
+ for (int i = 0; i < Properties.Length; ++i)
+ {
+ var property = Properties[i];
+ if (property.Attributes.TryGetValue(Constants.OptionAttribute, out EquatableAttributeData? attr))
+ {
+ WriteOptionDescriptor(writer, property, attr);
+ }
+
+ // if not the last property generate an empty line between entries
+ if (i != Properties.Length - 1)
+ {
+ writer.WriteEmptyLine();
+ }
+ }
+ }
+
+ private static void WriteOptionDescriptor( IndentedTextWriter writer, PropertyInfo property, OptionInfo info )
+ {
+ // NOP if the attribute does not contain valid data.
+ // Analyzer should detect that case and complain.
+ if (!info.IsValid)
+ {
+ Debug.WriteLine("!!Skipping Invalid OptionInfo!!");
+ return;
+ }
+
+ string propTypeName = $"{Constants.Option:G}<{property.TypeName:AG}>";
+ writer.WriteLine($"internal static readonly {propTypeName} {property.SimpleName}");
+ using (writer.PushIndent()) // property initializer indentation
+ {
+ // Aliases are only set via the constructor
+ // CONSIDER: Should this match behavior for the attribute?
+ // What does syntax look like for named parameters AND the params keyword...
+ writer.Write( $"= new {propTypeName}({CSharpLanguage.AsLiteral( info.Name )}" );
+ var aliases = info.Aliases;
+ if(aliases.HasValue)
+ {
+ writer.Write( ", " );
+ writer.Write( string.Join( ", ", aliases.Value.Select( CSharpLanguage.AsLiteral ) ) );
+ }
+
+ writer.WriteLine( ')' );
+ using(var valueInitializerScope = writer.Scope())
+ {
+ // This, intentionally, does not take into account any "default" values
+ // The source generator should remain ignorant of the default values for the
+ // generated source OR the attributes. If the setting is present the value is
+ // used, even if it's the same as a default. This keeps the generator independent
+ // of such details; allowing them to change without changing the generator!
+ var helpName = info.HelpName;
+ if(helpName.HasValue)
+ {
+ writer.WriteLine( "HelpName = {0},", CSharpLanguage.AsLiteral( helpName.Value ) );
+ }
+
+ var description = info.Description;
+ if(description.HasValue)
+ {
+ writer.WriteLine( "Description = {0},", CSharpLanguage.AsLiteral( description.Value ) );
+ }
+
+ var required = info.Required;
+ if(required.HasValue)
+ {
+ writer.WriteLine( "Required = {0},", CSharpLanguage.AsLiteral( required.Value ) );
+ }
+
+ var hidden = info.Hidden;
+ if(hidden.HasValue)
+ {
+ writer.WriteLine( "Hidden = {0},", CSharpLanguage.AsLiteral( hidden.Value ) );
+ }
+
+ var arity = info.Arity;
+ if(arity.HasValue)
+ {
+ // while there are static instance values for convenience
+ // there is no way to know the *OrMore variants as the
+ // max value used for them is a private const int. Besides,
+ // that the type is a struct and therefore use of those results
+ // in a copy anyway. Thus, this just emits construction of the
+ // correct type.
+
+ (int min, int max) = arity.Value;
+ writer.WriteLine( $"Arity = new {Constants.ArgumentArity:G}({min}, {max})," );
+ }
+ }
+
+ // Handle the indentation for validation, it isn't a full indentation scope, it's just alignment
+ string extraSpace=string.Empty;
+
+ // ANALYZER VALIDATION: These are mutually exclusive.
+ // Generator ignores the file validation attribute if the folder validation attribute
+ // is present. Analyzer should generate a diagnostic.
+ if(!WriteFolderValidation( writer, property, ref extraSpace ))
+ {
+ WriteFileValidation( writer, property, ref extraSpace );
+ }
+
+ // TODO: DefaultValueFactory
+ // TODO: Custom validators...
+
+ writer.WriteLine( ";" );
+ }
+ }
+
+ private static bool WriteFolderValidation( IndentedTextWriter writer, PropertyInfo property, ref string extraSpace )
+ {
+ if(!property.Attributes.TryGetValue( Constants.FolderValidationAttribute, out var folderValidationAttrib))
+ {
+ return false;
+ }
+
+ if((property.TypeName == Constants.DirectoryInfo) && !folderValidationAttrib.ConstructorArguments[ 0 ].IsNull)
+ {
+ int enumVal = (int)folderValidationAttrib.ConstructorArguments[ 0 ].Value!;
+ switch((FolderValidation)enumVal)
+ {
+ case FolderValidation.CreateIfNotExist:
+ if(extraSpace.Length > 0)
+ {
+ writer.WriteLine();
+ }
+
+ writer.Write( $"{extraSpace}.EnsureFolder()" );
+ extraSpace = " ";
+
+ break;
+
+ case FolderValidation.ExistingOnly:
+ if(extraSpace.Length > 0)
+ {
+ writer.WriteLine();
+ }
+
+ writer.Write( $"{extraSpace}.AcceptExistingFolderOnly()" );
+ extraSpace = " ";
+ break;
+
+ case FolderValidation.None:
+ default:
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private static void WriteFileValidation( IndentedTextWriter writer, PropertyInfo property, ref string extraSpace )
+ {
+ if(property.Attributes.TryGetValue( Constants.FileValidationAttribute, out var fileValidationAttrib )
+ && (property.TypeName == Constants.FileInfo)
+ && !fileValidationAttrib.ConstructorArguments[ 0 ].IsNull
+ )
+ {
+ int enumVal = (int)fileValidationAttrib.ConstructorArguments[ 0 ].Value!;
+ switch((FileValidation)enumVal)
+ {
+ case FileValidation.ExistingOnly:
+ if(extraSpace.Length > 0)
+ {
+ writer.WriteLine();
+ }
+
+ writer.Write( $"{extraSpace}.AcceptExistingFileOnly()" );
+ extraSpace = " ";
+ break;
+
+ case FileValidation.None:
+ default:
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ToolInfo.cs b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ToolInfo.cs
new file mode 100644
index 0000000..6093819
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Templates/ToolInfo.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+using Ubiquity.NET.Extensions;
+
+namespace Ubiquity.NET.CommandLine.SrcGen.Templates
+{
+ internal static class ToolInfo
+ {
+ public static string Name => ProcessInfo.ExecutableName;
+
+ public static string Version => ProcessInfo.ActiveAssembly?.GetInformationalVersion() ?? "";
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/TrackingNames.cs b/src/Ubiquity.NET.CommandLine.SrcGen/TrackingNames.cs
new file mode 100644
index 0000000..26f3dec
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/TrackingNames.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.SrcGen
+{
+ /// Tracking names used by the generators in this assembly
+ /// These are useful for testing and diagnostics
+ public static class TrackingNames
+ {
+ /// Tracking name of the class generation based off the CommandAttribute
+ public const string CommandClass = "CommandAttributeTracking";
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj b/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj
new file mode 100644
index 0000000..5cd96a9
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine.SrcGen/Ubiquity.NET.CommandLine.SrcGen.csproj
@@ -0,0 +1,99 @@
+
+
+
+ netstandard2.0
+
+ 12
+ enable
+ true
+ false
+ true
+ True
+
+
+ true
+ 4.9.0
+ .NET Foundation,Ubiquity.NET
+ PackageReadMe.md
+ false
+ Roslyn source generator for Ubiquity.NET.CommandLine
+ generator,.NET,Ubiquity.NET, Console
+ https://github.com/UbiquityDotNET/Ubiquity.NET.Utils
+ https://github.com/UbiquityDotNET/Ubiquity.NET.Utils.git
+ git
+ Apache-2.0 WITH LLVM-exception
+
+ $(NoWarn);NU5128
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ <__AssemblyPath>$(PkgMicrosoft_Bcl_HashCode)\lib\$(TargetFramework)\*.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ubiquity.NET.CommandLine/GeneratorAttributes/CommandAttribute.cs b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/CommandAttribute.cs
new file mode 100644
index 0000000..75c7eda
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/CommandAttribute.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.GeneratorAttributes
+{
+ /// Attribute to mark a command for generation of the backing implementation
+ [AttributeUsage( AttributeTargets.Class, Inherited = false, AllowMultiple = false )]
+ public sealed class CommandAttribute
+ : Attribute
+ {
+ /// Initializes a new instance of the class.
+ /// Description for the command (Default: string.Empty)
+ public CommandAttribute( string? description = null )
+ {
+ Description = description ?? string.Empty;
+ }
+
+ /// Gets the Description for this command
+ public string Description { get; }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FileValidationAttribute.cs b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FileValidationAttribute.cs
new file mode 100644
index 0000000..1ccbaf4
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FileValidationAttribute.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.GeneratorAttributes
+{
+ /// Enumeration for folder validation
+ public enum FileValidation
+ {
+ /// No validation
+ None,
+
+ /// Existing files only accepted.
+ ///
+ /// If a file specified does not exist then an exception results from the validation stage of
+ /// processing the command line.
+ ///
+ ExistingOnly,
+ }
+
+ // Analyzer checks: Only used on a type that is FileInfo
+ // Only used on a type that has an OptionAttribute as well.
+ //
+ // Generator ignores this attribute if all analyzer checks are not valid
+
+ /// Attribute to declare common validation for a property of type
+ [AttributeUsage( AttributeTargets.Property, Inherited = false, AllowMultiple = false )]
+ public sealed class FileValidationAttribute
+ : Attribute
+ {
+ /// Initializes a new instance of the class.
+ /// Validation to perform for this folder
+ ///
+ /// A that is is the same
+ /// as not specifying this attribute.
+ ///
+ public FileValidationAttribute( FileValidation validation )
+ {
+ Validation = validation;
+ }
+
+ /// Gets the to use for this
+ public FileValidation Validation { get; }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FolderValidationAttribute.cs b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FolderValidationAttribute.cs
new file mode 100644
index 0000000..be6a3a6
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/FolderValidationAttribute.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.GeneratorAttributes
+{
+ /// Enumeration for folder validation
+ public enum FolderValidation
+ {
+ /// No validation
+ None,
+
+ /// Creates the folder if it doesn't exist
+ CreateIfNotExist,
+
+ /// Existing folders only accepted.
+ ///
+ /// If a folder specified does not exist then an exception results from the validation stage of
+ /// processing the command line.
+ ///
+ ExistingOnly,
+ }
+
+ // Analyzer checks: Only used on a type that is DirectoryInfo
+ // Only used on a type that has an OptionAttribute as well.
+ //
+ // Generator ignores this attribute if all analyzer checks are not valid
+
+ /// Attribute to declare common validation for a property of type
+ [AttributeUsage( AttributeTargets.Property, Inherited = false, AllowMultiple = false )]
+ public sealed class FolderValidationAttribute
+ : Attribute
+ {
+ /// Initializes a new instance of the class.
+ /// Validation to perform for this folder
+ ///
+ /// A that is is the same
+ /// as not specifying this attribute.
+ ///
+ public FolderValidationAttribute( FolderValidation validation )
+ {
+ Validation = validation;
+ }
+
+ /// Gets the to use for this
+ public FolderValidation Validation { get; }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine/GeneratorAttributes/OptionAttribute.cs b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/OptionAttribute.cs
new file mode 100644
index 0000000..441ca80
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/OptionAttribute.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.GeneratorAttributes
+{
+ /// Attribute to apply to properties of a command that indicates generation of implementation
+ ///
+ [AttributeUsage( AttributeTargets.Property, Inherited = false, AllowMultiple = false )]
+ public sealed class OptionAttribute
+ : Attribute
+ {
+ /// Initializes a new instance of the class.
+ /// Name of the option
+ public OptionAttribute( string name )
+ {
+ ArgumentException.ThrowIfNullOrWhiteSpace( name );
+ Name = name;
+ }
+
+ /// Gets the name of the option (Including any "prefix" for the switch character)
+ public string Name { get; }
+
+ /// Gets the help name for this option
+ ///
+ public string? HelpName { get; init; }
+
+ /*
+ ANALYZER VALIDATION:
+ var bldr = ImmutableArray.CreateBuilder(value.Length);
+ for(int i = 0; i < value.Length; ++i)
+ {
+ if(string.IsNullOrWhiteSpace(value[i]))
+ {
+ bldr.Add(i);
+ }
+ }
+
+ if(bldr.Count > 0)
+ {
+ var badIndexArray = bldr.ToImmutable();
+ throw new ArgumentException($"Aliases cannot be null or whitespace. Invalid indexes: {badIndexArray.Format()}", nameof(value));
+ }
+ */
+
+ /// Gets the aliases for this option
+ public string[] Aliases { get; init; } = [];
+
+ /// Gets the description for this option
+ public string? Description { get; init; }
+
+ /// Gets a value indicating whether this option is required for execution of the command
+ ///
+ /// Options that are required and not provided by the user AND don't have a default are reported as an error.
+ /// That is, the "required" concept applies to the execution of the command and ***not*** the parsing of data.
+ ///
+ public bool Required { get; init; }
+
+ /// Gets a value indicating whether this option is hidden in help information
+ ///
+ /// This is often used for internal debugging/diagnostics and potentially preview functionality.
+ ///
+ ///
+ public bool Hidden { get; init; }
+
+ // ANALYZER VERIFY: ArityMin > = 0
+ // ArityMin < = ArgumentArity.MaximumArity [Sadly, an internal const]
+ // BOTH values are set or NONE is set.
+ // GENERATOR: ignore if not valid
+
+ /// Gets the minimum arity
+ ///
+ /// Instances of are not allowed as const args for an attribute.
+ /// Thus, this pair of properties represents the arity of an option. If not specified the default depends
+ /// on the type as defined by the underlying command line system.
+ ///
+ ///
+ ///
+ public int ArityMin { get; init; }
+
+ /// Gets the maximum arity
+ ///
+ public int ArityMax { get; init; }
+
+ // ANALYZER check: name refers to a Func
+ // where T is the type of the property
+ // GENERATOR: Ignore if analyzer validity checks fail
+
+ /// Gets the default value factory for the option
+ /// If no value is provided via parsing the command line, this is called to produce the default
+ public string? DefaultValueFactoryName { get; init; }
+
+ // ANALYZER check: name refers to a Func
+ // where T is the type of the property
+ // GENERATOR: Ignore if analyzer validity checks fail
+
+ /// Gets a Custom parser for the option
+ /// If provided, this parser is called to convert the argument to a value
+ public string? CustomParserName { get; init; }
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine/GeneratorAttributes/RootCommandAttribute.cs b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/RootCommandAttribute.cs
new file mode 100644
index 0000000..f473841
--- /dev/null
+++ b/src/Ubiquity.NET.CommandLine/GeneratorAttributes/RootCommandAttribute.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
+// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
+
+namespace Ubiquity.NET.CommandLine.GeneratorAttributes
+{
+ /// Attribute to mark a command for generation of the backing implementation
+ [AttributeUsage( AttributeTargets.Class, Inherited = false, AllowMultiple = false )]
+ public sealed class RootCommandAttribute
+ : Attribute
+ {
+ /// Initializes a new instance of the class.
+ public RootCommandAttribute( )
+ {
+ }
+
+ /// Gets or sets the Description for this command (Default: null)
+ public string? Description { get; set; } = null;
+
+ /// Gets a value indicating whether errors reported should also show the help message [Default: ]
+ /// This has a default, which is the opposite of the default from
+ public bool ShowHelpOnErrors { get; init; } = false;
+
+ /// Gets a value indicating whether errors reported should also show Typo corrections [Default: ]
+ /// This has a default, which is the opposite of the default from
+ public bool ShowTypoCorrections { get; init; } = false;
+
+ ///
+ public bool EnablePosixBundling { get; init; } = true;
+
+ /// Gets a value that indicates the default options to include
+ ///
+ /// Default handling includes and .
+ /// This allows overriding that to specify behavior as desired.
+ ///
+ public DefaultOption DefaultOptions { get; init; } = DefaultOption.Help | DefaultOption.Version;
+
+ /// Gets a value that indicates the default Directives to include
+ ///
+ /// Default handling includes .
+ /// This allows overriding that to specify behavior as needed.
+ ///
+ public DefaultDirective DefaultDirectives { get; init; } = DefaultDirective.Suggest;
+ }
+}
diff --git a/src/Ubiquity.NET.CommandLine/Ubiquity.NET.CommandLine.csproj b/src/Ubiquity.NET.CommandLine/Ubiquity.NET.CommandLine.csproj
index 0d9cd97..1290f49 100644
--- a/src/Ubiquity.NET.CommandLine/Ubiquity.NET.CommandLine.csproj
+++ b/src/Ubiquity.NET.CommandLine/Ubiquity.NET.CommandLine.csproj
@@ -4,8 +4,11 @@
net10.0;net8.0
enable
True
+ True
+
+ Ubiquity.NET.CommandLine.Lib
true
4.9.0
.NET Foundation,Ubiquity.NET
@@ -28,10 +31,13 @@
-
+
+
+
+
diff --git a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillExceptionValidators.cs b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillExceptionValidators.cs
index 3ce29de..f668f60 100644
--- a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillExceptionValidators.cs
+++ b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillExceptionValidators.cs
@@ -34,7 +34,8 @@ file enum ResourceId
// The actual strings used are the same as the values in the official runtime
// support so are at least compatible for "en-us". This fakes it to make it
// more readable AND make it easier to shift if a means of injecting resources
- // is found.
+ // is found. Also until such a mechanism is found this suppresses the warnings
+ // (RS1035) about use of banned APIs when used by a Roslyn component.
file static class ResourceIdExtensions
{
internal static string GetResourceString(this ResourceId id)
@@ -188,6 +189,7 @@ public static void ThrowIfLessThanOrEqual( T value, T other, [global::System.
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification="Poly Fill extension API")]
private static void ThrowZero( T value, string? paramName )
{
string msg = string.Format( global::System.Globalization.CultureInfo.CurrentCulture
@@ -200,6 +202,7 @@ private static void ThrowZero( T value, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowNegative( T value, string? paramName )
{
string msg = string.Format( global::System.Globalization.CultureInfo.CurrentCulture
@@ -212,6 +215,7 @@ private static void ThrowNegative( T value, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowNegativeOrZero( T value, string? paramName )
{
string msg = string.Format( global::System.Globalization.CultureInfo.CurrentCulture
@@ -224,6 +228,7 @@ private static void ThrowNegativeOrZero( T value, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowGreater( T value, T other, string? paramName )
{
var msg = string.Format(
@@ -238,6 +243,7 @@ private static void ThrowGreater( T value, T other, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowGreaterEqual( T value, T other, string? paramName )
{
@@ -253,6 +259,7 @@ private static void ThrowGreaterEqual( T value, T other, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowLess( T value, T other, string? paramName )
{
var msg = string.Format(
@@ -267,6 +274,7 @@ private static void ThrowLess( T value, T other, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowLessEqual( T value, T other, string? paramName )
{
var msg = string.Format(
@@ -281,6 +289,7 @@ private static void ThrowLessEqual( T value, T other, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowEqual( T value, T other, string? paramName )
{
var msg = string.Format(
@@ -295,6 +304,7 @@ private static void ThrowEqual( T value, T other, string? paramName )
}
[global::System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
private static void ThrowNotEqual( T value, T other, string? paramName )
{
var msg = string.Format(
diff --git a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillOperatingSystem.cs b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillOperatingSystem.cs
index ea19790..1343108 100644
--- a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillOperatingSystem.cs
+++ b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillOperatingSystem.cs
@@ -16,6 +16,7 @@ internal static class PolyFillOperatingSystem
#if !NET5_0_OR_GREATER
/// Indicates whether the current application is running on Windows.
/// if the current application is running on Windows; otherwise.
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "Poly Fill extension API" )]
public static bool IsWindows()
{
return global::System.Environment.OSVersion.Platform switch
diff --git a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillStringExtensions.cs b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillStringExtensions.cs
index 0a4cfbe..5ffb0b8 100644
--- a/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillStringExtensions.cs
+++ b/src/Ubiquity.NET.PollyFill.SharedSources/PolyFillStringExtensions.cs
@@ -49,6 +49,13 @@ public static int GetHashCode( this string self, StringComparison comparisonType
/// Replace line endings in the string with environment specific forms
/// string to change line endings for
/// string with environment specific line endings
+ ///
+ /// This API will explicitly replace line endings using the Runtime newline format. In most cases that is
+ /// what is desired. However, when generating files or content consumed by something other than the
+ /// current runtime it is usually not what is desired. In such a case the more explicit
+ /// is used to specify the precise line ending form to use. (Or Better yet, use Ubiquity.NET.Extensions.StringNormalizer.NormalizeLineEndings(string?, LineEndingKind)) />
+ ///
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessage( "MicrosoftCodeAnalysis", "RS1035:Banned Symbol", Justification = "This form explicitly uses the runtime form" )]
public static string ReplaceLineEndings( this string self )
{
return ReplaceLineEndings( self, global::System.Environment.NewLine );
diff --git a/src/Ubiquity.NET.Utils.slnx b/src/Ubiquity.NET.Utils.slnx
index 1f05e06..8e264a8 100644
--- a/src/Ubiquity.NET.Utils.slnx
+++ b/src/Ubiquity.NET.Utils.slnx
@@ -31,6 +31,7 @@
+
@@ -41,6 +42,8 @@
+
+