Skip to content

Commit 7413c90

Browse files
authored
Merge pull request #1 from di-VISION-Dev/develop
Merge 0.1.0-rc02 changes from develop into main
2 parents bc1cab7 + 86f2b96 commit 7413c90

16 files changed

+343
-32
lines changed

.github/workflows/dotnet.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
- name: Build
3939
run: dotnet build --no-restore
4040
- name: Test
41-
run: dotnet test --no-build -p:Threshold=\"95,90,95\"
41+
run: dotnet test --no-build -p:Threshold=\"95,95,100\"
4242
- name: Code Coverage Report
4343
if: always()
4444
run: |

.github/workflows/publish.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
2+
name: Publish Release
3+
4+
on:
5+
release:
6+
types: [published]
7+
env:
8+
PROJECT_NAME: CommandLineX
9+
10+
jobs:
11+
publish:
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 15
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
- name: Setup .NET
18+
uses: actions/setup-dotnet@v4
19+
with:
20+
dotnet-version: 8.0.x
21+
# - name: Verify commit exists in origin/main
22+
# run: |
23+
# git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
24+
# git branch --remote --contains | grep origin/main
25+
- name: Set VERSION variable from tag
26+
shell: bash
27+
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
28+
29+
- name: Build
30+
run: dotnet build -c Release /p:Version=${VERSION}
31+
- name: Test
32+
run: dotnet test -c Release /p:Version=${VERSION} --no-build
33+
- name: Pack
34+
run: dotnet pack -c Release /p:Version=${VERSION} --no-build --output .
35+
- name: Push NuGet
36+
run: dotnet nuget push "*.nupkg" -s https://api.nuget.org/v3/index.json -k ${NUGET_TOKEN} --skip-duplicate
37+
env:
38+
NUGET_TOKEN: ${{ secrets.NUGET_PUBLISH_TOKEN }}
39+
40+
- name: Publish Binaries to GitHub
41+
uses: softprops/action-gh-release@v2
42+
with:
43+
files: "*.nupkg"
44+
env:
45+
GITHUB_TOKEN: ${{ secrets.PACKAGE_WRITE_TOKEN }}

Common.Build.props

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@
1313
<PackageId>$(Company).$(AssemblyName)</PackageId>
1414
<!-- <PackageIcon>library.png</PackageIcon> -->
1515
</PropertyGroup>
16-
<PropertyGroup Condition="Exists('readme.md')">
16+
<PropertyGroup Condition="Exists('$(MSBuildProjectDirectory)\readme.md')">
1717
<PackageReadmeFile>readme.md</PackageReadmeFile>
1818
</PropertyGroup>
19+
<PropertyGroup Condition="!Exists('$(MSBuildProjectDirectory)\readme.md') And Exists('$(MSBuildThisFileDirectory)Readme.md')">
20+
<PackageReadmeFile>Readme.md</PackageReadmeFile>
21+
</PropertyGroup>
1922

20-
<!-- <ItemGroup> -->
23+
<!-- <ItemGroup> -->
2124
<!-- <None Include="$(MSBuildThisFileDirectory)doc/library.png" Pack="true" PackagePath="\"/> -->
2225
<!-- </ItemGroup> -->
23-
<ItemGroup Condition="Exists('readme.md')">
26+
<ItemGroup Condition="Exists('$(MSBuildProjectDirectory)\readme.md')">
2427
<None Include="readme.md" Pack="true" PackagePath="\"/>
2528
</ItemGroup>
29+
<ItemGroup Condition="!Exists('$(MSBuildProjectDirectory)\readme.md') And Exists('$(MSBuildThisFileDirectory)Readme.md')">
30+
<None Include="$(MSBuildThisFileDirectory)Readme.md" Pack="true" PackagePath="\"/>
31+
</ItemGroup>
2632
</Project>

README.md

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,117 @@
11
# command-line-x
2-
Extension library for System.CommandLine
2+
![Tool](https://img.shields.io/badge/.Net-8-lightblue) [<img src="https://img.shields.io/github/v/release/di-VISION-Dev/command-line-x" title="Latest">](../../releases/latest)
3+
4+
[System.CommandLine](https://github.com/dotnet/command-line-api) is a really handy .Net library for parsing command line arguments passed to an application. Unfortunately some useful features available in its beta stage were removed from the library's release candidates. CommandLineX brings back some of them: **hosting extensions** and **arguments model binding** including DI.
5+
6+
## Getting Started
7+
### Creating Console App
8+
1. Open terminal in the directory where your project usually are located.
9+
1. Execute
10+
```sh
11+
dotnet new console -n MyConsoleApp --no-restore
12+
```
13+
1. Navigate to the project directory.
14+
1. Open `MyConsoleApp.csproj` in XML editor of your choice.
15+
1. Change project SDK by replacing `<Project Sdk="Microsoft.NET.Sdk">` with `<Project Sdk="Microsoft.NET.Sdk.Worker">` (needed for hosting integration).
16+
17+
### Installing The Library
18+
In the directory of your project execute
19+
```sh
20+
dotnet add package diVISION.CommandLineX
21+
```
22+
23+
### Integrating The Library In Your Application
24+
1. Create `MyFirstAction.cs` (command action model) in your project directory:
25+
```cs
26+
using diVISION.CommandLineX;
27+
28+
namespace MyConsoleApp
29+
{
30+
public class MyFirstAction(ILogger<MyFirstAction> logger): ICommandAction
31+
{
32+
// injected by the host via DI
33+
private readonly ILogger<MyFirstAction> _logger = logger;
34+
35+
// "do-amount" argument
36+
public IEnumerable<int> DoAmount { get; set; } = [0];
37+
// "--directory" option
38+
public DirectoryInfo Directory { get; set; } = new (".");
39+
40+
public int Invoke(CommandActionContext context)
41+
{
42+
return InvokeAsync(context).Result;
43+
}
44+
45+
public Task<int> InvokeAsync(CommandActionContext context, CancellationToken cancellationToken = default)
46+
{
47+
_logger.LogDebug("Starting work");
48+
Console.WriteLine($"Doing it {string.Join(" then ", DoAmount)} times on {Directory.FullName}");
49+
return Task.FromResult(DoAmount.FirstOrDefault());
50+
}
51+
52+
}
53+
}
54+
```
55+
1. Modify `Program.cs` (`Main` method resp. depending on whether `--use-program-main` option used with `dotnet new`):
56+
```cs
57+
using diVISION.CommandLineX.Hosting;
58+
using System.CommandLine;
59+
60+
var rootCmd = new RootCommand("Running commands");
61+
62+
var builder = Host.CreateDefaultBuilder(args);
63+
builder
64+
.ConfigureAppConfiguration((config) =>
65+
{
66+
config.SetBasePath(AppDomain.CurrentDomain.BaseDirectory);
67+
})
68+
.ConfigureDefaults(null)
69+
.UseConsoleLifetime()
70+
// next 2 calls initalize CommandLine hosting
71+
.UseCommandLine(rootCmd)
72+
.UseCommandWithAction<MyFirstAction>(rootCmd, new("myFirst", "Doing things")
73+
{
74+
new Argument<IEnumerable<int>>("do-amount")
75+
{
76+
Arity = new(1, 2)
77+
},
78+
new Option<DirectoryInfo>("-d", ["--directory"])
79+
{
80+
Description = "Some directory to use",
81+
DefaultValueFactory = (_) => new DirectoryInfo(".")
82+
}
83+
});
84+
85+
using var host = builder.Build();
86+
87+
return await host.RunCommandLineAsync(args);
88+
```
89+
### Running Your App
90+
Execute one of the following in your project directory
91+
```sh
92+
dotnet run -- myFirst 42
93+
```
94+
```sh
95+
dotnet run -- myFirst 42 43
96+
```
97+
```sh
98+
dotnet run -- myFirst 42 43 -d someOtherDirectory
99+
```
100+
You can also request help on commands like
101+
```sh
102+
dotnet run -- -?
103+
```
104+
```sh
105+
dotnet run -- myFirst -?
106+
```
107+
108+
## Building
109+
For building the application .NET SDK 8.x is required (recommended: Visual Studio or Visual Studio Code).
110+
111+
After cloning the repository you can either open the solution `CommandLineX.sln` in your IDE and hit "Build" or open the terminal in the solution directory and execute
112+
```sh
113+
dotnet build
114+
```
115+
116+
## Contributing
117+
All contributions to development and error fixing are welcome. Please always use `develop` branch for forks and pull requests, `main` is reserved for stable releases and critical vulnarability fixes only. Please note: all code changes should meet minimal code coverage requirements to be merged into `main` or `develop`.

src/CommandLineX/CommandActionContext.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,22 @@
77

88
namespace diVISION.CommandLineX
99
{
10+
/// <summary>
11+
/// Context information collected by the framwork during parsing of the command line arguments.
12+
/// All bound command action models are passed this in the <c cref="ICommandAction.Invoke(CommandActionContext)">Invoke</c>
13+
/// or resp. <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">InvokeAsync</c> call.
14+
/// </summary>
15+
/// <param name="parseResult"></param>
16+
/// <param name="unboundSymbols"></param>
1017
public class CommandActionContext(ParseResult parseResult, IEnumerable<Symbol> unboundSymbols)
1118
{
19+
/// <summary>
20+
/// Result of command line parsing.
21+
/// </summary>
1222
public ParseResult ParseResult => parseResult;
23+
/// <summary>
24+
/// Collection of symbols that have been defined by the bound <c cref="Command">Command</c> but could not be resolved in the model.
25+
/// </summary>
1326
public IEnumerable<Symbol> UnboundSymbols => unboundSymbols;
1427
}
1528
}

src/CommandLineX/CommandLineOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@
77

88
namespace diVISION.CommandLineX
99
{
10+
/// <summary>
11+
/// Container with configuration options for <c cref="System.CommandLine">System.CommandLine</c>.
12+
/// An instance of this class initialized with the defaults is passed to the configure action by
13+
/// <c cref="diVISION.CommandLineX.Hosting.CommmandLineHostingExtension.UseCommandLine(Microsoft.Extensions.Hosting.IHostBuilder, RootCommand, Action{CommandLineOptions}?)">CommmandLineHostingExtension.UseCommandLine</c>,
14+
/// the action then can modify the configuration as required.
15+
/// </summary>
1016
public class CommandLineOptions
1117
{
1218
public ParserConfiguration? ParserConfiguration { get; set; }

src/CommandLineX/CommandLineX.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
<TargetFramework>net8.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8-
<Version>0.1.0-rc01</Version>
8+
<Version>0.1.0-rc02</Version>
9+
<Description>Hosting and model binding extensions for System.CommandLine</Description>
910
</PropertyGroup>
1011

1112
<PropertyGroup Condition="'$(Configuration)'=='Debug'">

src/CommandLineX/Hosting/CommmandLineHostingExtension.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@
1010

1111
namespace diVISION.CommandLineX.Hosting
1212
{
13+
/// <summary>
14+
/// <see cref="https://github.com/dotnet/command-line-api"><c>System.CommandLine</c></see> integration with <c>IHostBuilder</c> and <c>IHost</c>.
15+
/// </summary>
1316
public static class CommmandLineHostingExtension
1417
{
18+
/// <summary>
19+
/// Registers basic services neccessary to invoke commands from within a host.
20+
/// If <paramref name="configure"/> action is provided, it is called with <c>CommandLineOptions</c> instance
21+
/// where configuration for <c>System.CommandLine</c> can be set up. Call this method before any other extension is used.
22+
/// </summary>
23+
/// <seealso cref="CommandLineOptions"/>
24+
/// <param name="builder">host builder instance being extended</param>
25+
/// <param name="rootCommand" cref="RootCommand">root command to be invoked by host</param>
26+
/// <param name="configure">optional configure action</param>
27+
/// <returns><paramref name="builder"/></returns>
1528
public static IHostBuilder UseCommandLine(this IHostBuilder builder, RootCommand rootCommand, Action<CommandLineOptions>? configure = null)
1629
{
1730
var registry = new CommandActionRegistry();
@@ -37,6 +50,13 @@ public static IHostBuilder UseCommandLine(this IHostBuilder builder, RootCommand
3750
return builder;
3851
}
3952

53+
/// <summary>
54+
/// Registers <c cref="CommandLineInvocationContext">CommandLineInvocationContext</c> and <c cref="CommandLineHostedService">CommandLineHostedService</c>
55+
/// so that commands specified by <paramref name="args"/> will be parsed and invoked asynchronously at the host startup.
56+
/// </summary>
57+
/// <param name="builder">host builder instance being extended</param>
58+
/// <param name="args">command line arguments to parse</param>
59+
/// <returns><paramref name="builder"/></returns>
4060
public static IHostBuilder UseHostedCommandInvocation(this IHostBuilder builder, string[] args)
4161
{
4262
builder.ConfigureServices(services =>
@@ -47,6 +67,19 @@ public static IHostBuilder UseHostedCommandInvocation(this IHostBuilder builder,
4767
return builder;
4868
}
4969

70+
/// <summary>
71+
/// Adds specified <paramref name="command"/> to <paramref name="parent"/> command (ususally <c>RootCommand</c>) and binds <typeparamref name="TAction"/> model to the former.
72+
/// Requires comand line services to be set up by <c cref="UseCommandLine(IHostBuilder, RootCommand, Action{CommandLineOptions}?)">UseCommandLine</c> prior to this call.
73+
/// </summary>
74+
/// <seealso cref="UseCommandLine(IHostBuilder, RootCommand, Action{CommandLineOptions}?)"/>
75+
/// <typeparam name="TAction">action to bind</typeparam>
76+
/// <param name="builder">host builder instance being extended</param>
77+
/// <param name="parent">parent <c cref="Command">Command</c> to which <paramref name="command"/> will be added</param>
78+
/// <param name="command"><c cref="Command">Command</c> used for binding</param>
79+
/// <param name="runAsync">indicates whether <typeparamref name="TAction"/>.<c cref="ICommandAction.Invoke(CommandActionContext)">Invoke</c>
80+
/// or <typeparamref name="TAction"/>.<c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">InvokeAsync</c> is used to execute the action</param>
81+
/// <returns><paramref name="builder"/></returns>
82+
/// <exception cref="InvalidOperationException"></exception>
5083
public static IHostBuilder UseCommandWithAction<TAction>(this IHostBuilder builder, Command parent, Command command, bool runAsync = true)
5184
where TAction : class, ICommandAction
5285
{
@@ -90,6 +123,14 @@ public static IHostBuilder UseCommandWithAction<TAction>(this IHostBuilder build
90123
return builder;
91124
}
92125

126+
/// <summary>
127+
/// Starts the <paramref name="host"/>, parses command line <paramref name="args"/> and executes matching <c cref="ICommandAction.Invoke(CommandActionContext)">ICommandAction.Invoke</c>
128+
/// previously bound by <c cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)">UseCommandWithAction</c>. If no match exists an error message is displayed and an error code is returned.
129+
/// </summary>
130+
/// <seealso cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)"/>
131+
/// <param name="host">host instance being extended</param>
132+
/// <param name="args">command line arguments to parse</param>
133+
/// <returns>either result of <c cref="ICommandAction.Invoke(CommandActionContext)">ICommandAction.Invoke</c> or an error code</returns>
93134
public static int RunCommandLine(this IHost host, string[] args)
94135
{
95136
host.ThrowIfHostedCommandLine();
@@ -98,6 +139,15 @@ public static int RunCommandLine(this IHost host, string[] args)
98139
return invoker.Invoke(args);
99140
}
100141

142+
/// <summary>
143+
/// Starts the <paramref name="host"/>, parses command line <paramref name="args"/> and executes matching <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">ICommandAction.InvokeAsync</c>
144+
/// previously bound by <c cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)">UseCommandWithAction</c>. If no match exists an error message is displayed and an error code is returned.
145+
/// </summary>
146+
/// <seealso cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)"/>
147+
/// <param name="host">host instance being extended</param>
148+
/// <param name="args">command line arguments to parse</param>
149+
/// <param name="cancellationToken">cancellation token passed to <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">ICommandAction.InvokeAsync</c></param>
150+
/// <returns>either result of <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">ICommandAction.InvokeAsync</c> or an error code</returns>
101151
public static async Task<int> RunCommandLineAsync(this IHost host, string[] args, CancellationToken cancellationToken = default)
102152
{
103153
host.ThrowIfHostedCommandLine();
@@ -106,6 +156,15 @@ public static async Task<int> RunCommandLineAsync(this IHost host, string[] args
106156
return await invoker.InvokeAsync(args, cancellationToken);
107157
}
108158

159+
/// <summary>
160+
/// Starts the <paramref name="host"/>, during the startup the <c cref="CommandLineHostedService">CommandLineHostedService</c>
161+
/// (registered by <c cref="UseHostedCommandInvocation(IHostBuilder, string[])">UseHostedCommandInvocation</c>)
162+
/// parses the command line and executes matching <c cref="ICommandAction.Invoke(CommandActionContext)">ICommandAction.Invoke</c>
163+
/// previously bound by <c cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)">UseCommandWithAction</c>. If no match exists an error message is displayed and an error code is returned.
164+
/// </summary>
165+
/// <seealso cref="UseHostedCommandInvocation(IHostBuilder, string[])"/>
166+
/// <param name="host">host instance being extended</param>
167+
/// <returns>either result of <c cref="ICommandAction.Invoke(CommandActionContext)">ICommandAction.Invoke</c> or an error code</returns>
109168
public static int RunCommandLineHosted(this IHost host)
110169
{
111170
host.CheckInvocationContext();
@@ -119,6 +178,16 @@ public static int RunCommandLineHosted(this IHost host)
119178
return Environment.ExitCode;
120179
}
121180

181+
/// <summary>
182+
/// Starts the <paramref name="host"/>, during the startup the <c cref="CommandLineHostedService">CommandLineHostedService</c>
183+
/// (registered by <c cref="UseHostedCommandInvocation(IHostBuilder, string[])">UseHostedCommandInvocation</c>)
184+
/// parses the command line and executes matching <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">ICommandAction.InvokeAsync</c>
185+
/// previously bound by <c cref="UseCommandWithAction{TAction}(IHostBuilder, Command, Command, bool)">UseCommandWithAction</c>. If no match exists an error message is displayed and an error code is returned.
186+
/// </summary>
187+
/// <seealso cref="UseHostedCommandInvocation(IHostBuilder, string[])"/>
188+
/// <param name="host">host instance being extended</param>
189+
/// <param name="cancellationToken">cancellation token passed to the <paramref name="host"/></param>
190+
/// <returns>either result of <c cref="ICommandAction.InvokeAsync(CommandActionContext, CancellationToken)">ICommandAction.InvokeAsync</c> or an error code</returns>
122191
public static async Task<int> RunCommandLineHostedAsync(this IHost host, CancellationToken cancellationToken = default)
123192
{
124193
host.CheckInvocationContext();
@@ -133,6 +202,11 @@ public static async Task<int> RunCommandLineHostedAsync(this IHost host, Cancell
133202
return Environment.ExitCode;
134203
}
135204

205+
/// <summary>
206+
/// For internal use.
207+
/// </summary>
208+
/// <param name="host">host instance being extended</param>
209+
/// <exception cref="InvalidOperationException"></exception>
136210
public static void CheckInvocationContext(this IHost host)
137211
{
138212
if (null == host.Services.GetService<CommandLineInvocationContext>())
@@ -141,6 +215,11 @@ public static void CheckInvocationContext(this IHost host)
141215
}
142216
}
143217

218+
/// <summary>
219+
/// For internal use.
220+
/// </summary>
221+
/// <param name="host">host instance being extended</param>
222+
/// <exception cref="InvalidOperationException"></exception>
144223
public static void ThrowIfHostedCommandLine(this IHost host)
145224
{
146225
if (host.Services.GetService<IHostedService>() is CommandLineHostedService)

0 commit comments

Comments
 (0)