Skip to content
Open
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
20 changes: 18 additions & 2 deletions source/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
<Project>

<PropertyGroup>
<LangVersion>14.0</LangVersion>
<LangVersion Condition="'$(MSBuildProjectExtension)' == '.csproj'">14.0</LangVersion>
<LangVersion Condition="'$(MSBuildProjectExtension)' == '.fsproj'">10.0</LangVersion>
<LangVersion Condition="'$(MSBuildProjectExtension)' == '.vbproj'">17.13</LangVersion>
</PropertyGroup>

<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

@Flash0ver Flash0ver Aug 13, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
</PropertyGroup>

<PropertyGroup>
<Nullable Condition="'$(MSBuildProjectExtension)' == '.csproj' Or '$(MSBuildProjectExtension)' == '.fsproj'">enable</Nullable>
<ImplicitUsings Condition="'$(MSBuildProjectExtension)' == '.csproj'">enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>10.0</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>

Expand Down
4 changes: 4 additions & 0 deletions source/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="FSharp.Core" Version="10.0.100" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.11" />
</ItemGroup>

</Project>
25 changes: 25 additions & 0 deletions source/FlashOWare.CommandLine.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,29 @@
<File Path="Directory.Build.targets" />
<File Path="Directory.Packages.props" />
</Folder>
<Folder Name="/examples/">
<File Path="examples/.globalconfig" />
<File Path="examples/Directory.Build.props" />
<Project Path="examples/FlashOWare.CommandLine.Example.CSharp/FlashOWare.CommandLine.Example.CSharp.csproj" />
<Project Path="examples/FlashOWare.CommandLine.Example.FSharp/FlashOWare.CommandLine.Example.FSharp.fsproj" />
<Project Path="examples/FlashOWare.CommandLine.Example.VisualBasic/FlashOWare.CommandLine.Example.VisualBasic.vbproj" />
</Folder>
<Folder Name="/lib/">
<File Path="lib/.globalconfig" />
<File Path="lib/Directory.Build.props" />
<Project Path="lib/FlashOWare.CommandLine.CliSchema/FlashOWare.CommandLine.CliSchema.csproj" />
<Project Path="lib/FlashOWare.CommandLine.OpenCli/FlashOWare.CommandLine.OpenCli.csproj" />
</Folder>
<Folder Name="/perf/">
<File Path="perf/.globalconfig" />
<File Path="perf/Directory.Build.props" />
<Project Path="perf/FlashOWare.CommandLine.CliSchema.Benchmarks/FlashOWare.CommandLine.CliSchema.Benchmarks.csproj" />
<Project Path="perf/FlashOWare.CommandLine.OpenCli.Benchmarks/FlashOWare.CommandLine.OpenCli.Benchmarks.csproj" />
</Folder>
<Folder Name="/tests/">
<File Path="tests/.globalconfig" />
<File Path="tests/Directory.Build.props" />
<Project Path="tests/FlashOWare.CommandLine.CliSchema.Tests/FlashOWare.CommandLine.CliSchema.Tests.csproj" />
<Project Path="tests/FlashOWare.CommandLine.OpenCli.Tests/FlashOWare.CommandLine.OpenCli.Tests.csproj" />
</Folder>
</Solution>
4 changes: 4 additions & 0 deletions source/examples/.globalconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
is_global = true

# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = none
16 changes: 16 additions & 0 deletions source/examples/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project>

<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))"
Condition="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../')) != ''" />

<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>FlashOWare.CommandLine.Example.CSharp</AssemblyName>
<RootNamespace>FlashOWare.CommandLine.Example</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Octokit" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.CliSchema\FlashOWare.CommandLine.CliSchema.csproj" />
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.OpenCli\FlashOWare.CommandLine.OpenCli.csproj" />
</ItemGroup>

<ItemGroup>
<TrimmerRootAssembly Include="FlashOWare.CommandLine.CliSchema" />
<TrimmerRootAssembly Include="FlashOWare.CommandLine.OpenCli" />
</ItemGroup>

</Project>
111 changes: 111 additions & 0 deletions source/examples/FlashOWare.CommandLine.Example.CSharp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System.CommandLine;
using System.CommandLine.Invocation;
using System.CommandLine.Parsing;
using System.Reflection;
using Octokit;

RootCommand rootCommand = new("C# sample app.");

Option<bool> option = new("--language", ["-l", "--lang"])
{
Description = "Display the .NET language in use.",
Arity = ArgumentArity.Zero,
Action = new LanguageCommandLineAction(),
};
rootCommand.Options.Add(option);

Command command = new("repository", "Display GitHub repository information.");
command.Aliases.Add("repo");
Argument<string> argument = new("FULL-NAME")
{
Description = """The full name of the repository in the form of "owner/repo".""",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory = static string (ArgumentResult argumentResult) => "FlashOWare/command-line-interfaces",
};
argument.Validators.Add(void (ArgumentResult argumentResult) =>
{
string value = argumentResult.GetRequiredValue(argument);

int index = value.IndexOf('/');
if (index == -1 || index != value.LastIndexOf('/') || index == 0 || index + 1 == value.Length)
{
argumentResult.AddError("""The full name of the repository must be in the form of "owner/repo".""");
}
});
command.Arguments.Add(argument);
command.SetAction(async Task<int> (ParseResult parseResult, CancellationToken cancellationToken) =>
{
string fullName = parseResult.GetRequiredValue(argument);
int index = fullName.IndexOf('/');
string owner = fullName[..index];
string repo = fullName[(index + 1)..];

AssemblyName name = typeof(Program).Assembly.GetName();
GitHubClient client = new(new ProductHeaderValue(name.Name, name.Version?.ToString()));
Repository repository = await client.Repository.Get(owner, repo);
RateLimit rateLimit = client.GetLastApiInfo().RateLimit;

const string TabString = " ";
TextWriter output = parseResult.InvocationConfiguration.Output;

await output.WriteLineAsync($"""
Repository
{TabString}Full Name: {repository.FullName}
{TabString}Stargazers: {repository.StargazersCount}
{TabString}Watchers: {repository.SubscribersCount}
{TabString}Forks: {repository.ForksCount}
{TabString}Open Issues: {repository.OpenIssuesCount}
""");
await output.WriteLineAsync($"""
Rate Limiting
{TabString}Requests per hour: {rateLimit.Limit - rateLimit.Remaining} / {rateLimit.Limit}
{TabString}Window resets at: {rateLimit.Reset.LocalDateTime:yyyy-MM-dd HH:mm:ss.fffffff}
""");

return 0;
});
rootCommand.Subcommands.Add(command);

Directive directive = new("config")
{
Description = "Show the command-line configuration that would have been used if the given command line were run.",
Action = new ConfigCommandLineAction(),
};
rootCommand.Directives.Add(directive);

ParseResult parseResult = rootCommand.Parse(args);
return await parseResult.InvokeAsync();

internal sealed class LanguageCommandLineAction : SynchronousCommandLineAction
{
public override int Invoke(ParseResult parseResult)
{
TextWriter output = parseResult.InvocationConfiguration.Output;
output.WriteLine("C#");
return 0;
}
}

internal sealed class ConfigCommandLineAction : SynchronousCommandLineAction
{
public override int Invoke(ParseResult parseResult)
{
const string TabString = " ";
TextWriter output = parseResult.InvocationConfiguration.Output;

output.WriteLine($"""
Configuration
{TabString}EnablePosixBundling: {parseResult.Configuration.EnablePosixBundling}
""");

output.WriteLine($"""
Invocation
{TabString}EnableDefaultExceptionHandler: {parseResult.InvocationConfiguration.EnableDefaultExceptionHandler}
{TabString}ProcessTerminationTimeout: {(parseResult.InvocationConfiguration.ProcessTerminationTimeout is { } timeout ? timeout.ToString("c") : "<null>")}
{TabString}Output: {parseResult.InvocationConfiguration.Output}
{TabString}Error: {parseResult.InvocationConfiguration.Error}
""");

return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Reflection;

[assembly: AssemblyCopyright("Copyright © FlashOWare 2026")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>FlashOWare.CommandLine.Example.FSharp</AssemblyName>
<RootNamespace>FlashOWare.CommandLine.Example</RootNamespace>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="FSharp.Core" />
<PackageReference Include="Octokit" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.CliSchema\FlashOWare.CommandLine.CliSchema.csproj" />
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.OpenCli\FlashOWare.CommandLine.OpenCli.csproj" />
</ItemGroup>

<ItemGroup>
<TrimmerRootAssembly Include="FlashOWare.CommandLine.CliSchema" />
<TrimmerRootAssembly Include="FlashOWare.CommandLine.OpenCli" />
</ItemGroup>

</Project>
99 changes: 99 additions & 0 deletions source/examples/FlashOWare.CommandLine.Example.FSharp/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
open System
open System.CommandLine
open System.CommandLine.Invocation
open System.CommandLine.Parsing
open System.Reflection
open System.Threading
open System.Threading.Tasks
open Octokit

[<Sealed>]
type private LanguageCommandLineAction() =
inherit SynchronousCommandLineAction()

override this.Invoke(parseResult: ParseResult) : int =
let output = parseResult.InvocationConfiguration.Output
output.WriteLine("F#")
0

[<Sealed>]
type private ConfigCommandLineAction() =
inherit SynchronousCommandLineAction()

override this.Invoke(parseResult: ParseResult) : int =
let tabString = " "
let output = parseResult.InvocationConfiguration.Output

output.WriteLine("Configuration")
output.WriteLine($"{tabString}EnablePosixBundling: %b{parseResult.Configuration.EnablePosixBundling}")

output.WriteLine("Invocation")
output.WriteLine($"{tabString}EnableDefaultExceptionHandler: %b{parseResult.InvocationConfiguration.EnableDefaultExceptionHandler}")
output.WriteLine($"""{tabString}ProcessTerminationTimeout: {(if parseResult.InvocationConfiguration.ProcessTerminationTimeout.HasValue then parseResult.InvocationConfiguration.ProcessTerminationTimeout.Value.ToString("c") else "<null>")}""")
output.WriteLine($"{tabString}Output: {parseResult.InvocationConfiguration.Output}")
output.WriteLine($"{tabString}Error: {parseResult.InvocationConfiguration.Error}")

0

let rootCommand = RootCommand("F# sample app.")

let option = Option<bool>("--language", [| "-l"; "--lang" |],
Description = "Display the .NET language in use.",
Arity = ArgumentArity.Zero,
Action = LanguageCommandLineAction()
)
rootCommand.Options.Add(option)

let command = Command("repository", "Display GitHub repository information.")
command.Aliases.Add("repo")
let argument = Argument<string>("FULL-NAME",
Description = """The full name of the repository in the form of "owner/repo".""",
Arity = ArgumentArity.ZeroOrOne,
DefaultValueFactory = fun (argumentResult: ArgumentResult) -> "FlashOWare/command-line-interfaces"
)
argument.Validators.Add(fun (argumentResult : ArgumentResult) ->
let value = argumentResult.GetRequiredValue(argument)

let index = value.IndexOf('/')
if index = -1 || index <> value.LastIndexOf('/') || index = 0 || index + 1 = value.Length then
argumentResult.AddError("""The full name of the repository must be in the form of "owner/repo".""")
)
command.Arguments.Add(argument)
command.SetAction(fun (parseResult: ParseResult) (cancellationToken: CancellationToken) -> (task {
let fullName = parseResult.GetRequiredValue(argument)
let index = fullName.IndexOf('/')
let owner = fullName.Substring(0, index)
let repo = fullName.Substring(index + 1)

let name = Assembly.GetExecutingAssembly().GetName()
let client = GitHubClient(ProductHeaderValue(name.Name, match name.Version with | null -> null | version -> version.ToString()))
let! repository = client.Repository.Get(owner, repo)
let rateLimit = client.GetLastApiInfo().RateLimit

let tabString = " "
let output = parseResult.InvocationConfiguration.Output

do! output.WriteLineAsync("Repository")
do! output.WriteLineAsync($"{tabString}Full Name: {repository.FullName}")
do! output.WriteLineAsync($"{tabString}Stargazers: {repository.StargazersCount}")
do! output.WriteLineAsync($"{tabString}Watchers: {repository.SubscribersCount}")
do! output.WriteLineAsync($"{tabString}Forks: {repository.ForksCount}")
do! output.WriteLineAsync($"{tabString}Open Issues: {repository.OpenIssuesCount}")

do! output.WriteLineAsync("Rate Limiting")
do! output.WriteLineAsync($"{tabString}Requests per hour: {rateLimit.Limit - rateLimit.Remaining} / {rateLimit.Limit}")
do! output.WriteLineAsync($"""{tabString}Window resets at: {rateLimit.Reset.LocalDateTime.ToString("yyyy-MM-dd HH:mm:ss.fffffff")}""")

return 0
} : Task<int>))
rootCommand.Subcommands.Add(command)

let directive = Directive("config",
Description = "Show the command-line configuration that would have been used if the given command line were run.",
Action = ConfigCommandLineAction()
)
rootCommand.Directives.Add(directive)

let parseResult = rootCommand.Parse(Environment.GetCommandLineArgs() |> Array.skip 1)
let exitCode = parseResult.Invoke()
exit exitCode
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>FlashOWare.CommandLine.Example.VisualBasic</AssemblyName>
<RootNamespace>FlashOWare.CommandLine.Example</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Octokit" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.CliSchema\FlashOWare.CommandLine.CliSchema.csproj" />
<ProjectReference Include="..\..\lib\FlashOWare.CommandLine.OpenCli\FlashOWare.CommandLine.OpenCli.csproj" />
</ItemGroup>

<ItemGroup>
<TrimmerRootAssembly Include="FlashOWare.CommandLine.CliSchema" />
<TrimmerRootAssembly Include="FlashOWare.CommandLine.OpenCli" />
</ItemGroup>

</Project>
Loading