Skip to content

Commit af973e5

Browse files
committed
readme completed, increased code coverage
1 parent bc1cab7 commit af973e5

File tree

11 files changed

+179
-31
lines changed

11 files changed

+179
-31
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: |

CommandLineX.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ EndProject
1313
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{8D85AE92-B925-47E0-9913-0CE8840E017C}"
1414
ProjectSection(SolutionItems) = preProject
1515
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
16-
.github\workflows\publish.yml = .github\workflows\publish.yml
1716
EndProjectSection
1817
EndProject
1918
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandLineX", "src\CommandLineX\CommandLineX.csproj", "{66FE8D23-FD3C-45CB-8C71-D54E48AD39CA}"

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: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,115 @@
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
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+
private readonly ILogger<MyFirstAction> _logger = logger;
33+
34+
// do-amount argument
35+
public IEnumerable<int> DoAmount { get; set; } = [0];
36+
// --directory option
37+
public DirectoryInfo Directory { get; set; } = new (".");
38+
39+
public int Invoke(CommandActionContext context)
40+
{
41+
return InvokeAsync(context).Result;
42+
}
43+
44+
public Task<int> InvokeAsync(CommandActionContext context, CancellationToken cancellationToken = default)
45+
{
46+
_logger.LogDebug("Starting work");
47+
Console.WriteLine($"Doing it {string.Join(" then ", DoAmount)} times on {Directory.FullName}");
48+
return Task.FromResult(DoAmount.FirstOrDefault());
49+
}
50+
51+
}
52+
}
53+
```
54+
1. Modify `Program.cs` (`Main` method resp. depending on whether `--use-program-main` option used with `dotnet new`):
55+
```cs
56+
using diVISION.CommandLineX.Hosting;
57+
using System.CommandLine;
58+
59+
var rootCmd = new RootCommand("Running commands");
60+
61+
var builder = Host.CreateDefaultBuilder(args);
62+
builder
63+
.ConfigureAppConfiguration((config) =>
64+
{
65+
config.SetBasePath(AppDomain.CurrentDomain.BaseDirectory);
66+
})
67+
.ConfigureDefaults(null)
68+
.UseConsoleLifetime()
69+
.UseCommandLine(rootCmd)
70+
.UseCommandWithAction<MyFirstAction>(rootCmd, new("myFirst", "Doing things")
71+
{
72+
new Argument<IEnumerable<int>>("do-amount")
73+
{
74+
Arity = new(1, 2)
75+
},
76+
new Option<DirectoryInfo>("-d", ["--directory"])
77+
{
78+
Description = "Some directory to use",
79+
DefaultValueFactory = (_) => new DirectoryInfo(".")
80+
}
81+
});
82+
83+
using var host = builder.Build();
84+
85+
return await host.RunCommandLineAsync(args);
86+
```
87+
### Running Your App
88+
Execute one of the following in your project directory
89+
```sh
90+
dotnet run -- myFirst 42
91+
```
92+
```sh
93+
dotnet run -- myFirst 42 43
94+
```
95+
```sh
96+
dotnet run -- myFirst 42 43 -d someOtherDirectory
97+
```
98+
You can also request help on commands like
99+
```sh
100+
dotnet run -- -?
101+
```
102+
```sh
103+
dotnet run -- myFirst -?
104+
```
105+
106+
## Building
107+
For building the application .NET SDK 8.x is required (recommended: Visual Studio or Visual Studio Code).
108+
109+
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
110+
```sh
111+
dotnet build
112+
```
113+
114+
## Contributing
115+
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. All code changes should meet minimal code coverage requirements to be merged into `main` or `develop`, the coverage requirements are: lines - 95%, branches - 95%, methods - 100%.

src/CommandLineX/CommandLineX.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
<Version>0.1.0-rc01</Version>
9+
<Description>Hosting and model binding extensions for System.CommandLine</Description>
910
</PropertyGroup>
1011

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

test/CommandLineX.Tests/AsyncBindingCommandLineActionTest.cs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public async Task InvokeAsync_NoArgsCommandAction_returning_default_given_empty_
1616
var command = new Command("simple");
1717
var bindingAction = new AsyncBindingCommandLineAction<NoArgsCommandAction>(command, () => new());
1818
bindingAction.Should().NotBeNull();
19-
var actionResult = await bindingAction.InvokeAsync(command.Parse(string.Empty), TestContext.CancellationTokenSource.Token);
19+
var actionResult = await bindingAction.InvokeAsync(command.Parse(string.Empty), TestContext.CancellationToken);
2020
actionResult.Should().Be(42);
2121
}
2222

@@ -25,12 +25,25 @@ public async Task InvokeAsync_NoArgsCommandAction_returning_default_given_Comman
2525
{
2626
var command = new Command("simple")
2727
{
28-
new Argument<int>("nonce")
28+
new Argument<int>("nonce"),
29+
new Option<string>("-f", ["--foo"])
2930
};
30-
var bindingAction = new AsyncBindingCommandLineAction<NoArgsCommandAction>(command, () => new());
31+
32+
var action = new NoArgsCommandAction();
33+
var bindingAction = new AsyncBindingCommandLineAction<NoArgsCommandAction>(command, () => action);
3134
bindingAction.Should().NotBeNull();
32-
var actionResult = await bindingAction.InvokeAsync(command.Parse("666"), TestContext.CancellationTokenSource.Token);
35+
36+
var actionResult = await bindingAction.InvokeAsync(command.Parse("666"), TestContext.CancellationToken);
3337
actionResult.Should().Be(42);
38+
action.Context.Should()
39+
.NotBeNull()
40+
.And.Satisfy<CommandActionContext>(context =>
41+
{
42+
context.UnboundSymbols.Should()
43+
.NotBeNull()
44+
.And.Contain([command.Arguments[0], command.Options[0]]);
45+
context.ParseResult.Should().NotBeNull();
46+
});
3447
}
3548

3649
[TestMethod]
@@ -43,7 +56,7 @@ public async Task InvokeAsync_OneIntArgCommandAction_returning_Argument_given_Co
4356
var action = new OneIntArgCommandAction();
4457
var bindingAction = new AsyncBindingCommandLineAction<OneIntArgCommandAction>(command, () => action);
4558
bindingAction.Should().NotBeNull();
46-
var actionResult = await bindingAction.InvokeAsync(command.Parse("42"), TestContext.CancellationTokenSource.Token);
59+
var actionResult = await bindingAction.InvokeAsync(command.Parse("42"), TestContext.CancellationToken);
4760
actionResult.Should().Be(action.Answer).And.Be(42);
4861
}
4962

@@ -57,7 +70,7 @@ public async Task InvokeAsync_OneIntArgCommandAction_returning_default_given_Com
5770
var action = new OneIntArgCommandAction();
5871
var bindingAction = new AsyncBindingCommandLineAction<OneIntArgCommandAction>(command, () => action);
5972
bindingAction.Should().NotBeNull();
60-
var actionResult = await bindingAction.InvokeAsync(command.Parse("42"), TestContext.CancellationTokenSource.Token);
73+
var actionResult = await bindingAction.InvokeAsync(command.Parse("42"), TestContext.CancellationToken);
6174
actionResult.Should().Be(action.Answer).And.Be(0);
6275
}
6376

@@ -89,7 +102,7 @@ public async Task InvokeAsync_TwoPrimitiveArgsCommandAction_returning_combinatio
89102
var bindingAction = new AsyncBindingCommandLineAction<TwoPrimitiveArgsCommandAction>(command, () => action);
90103
bindingAction.Should().NotBeNull();
91104
var args = new string[] { "what's the question?", "42" };
92-
var actionResult = await bindingAction.InvokeAsync(command.Parse(args), TestContext.CancellationTokenSource.Token);
105+
var actionResult = await bindingAction.InvokeAsync(command.Parse(args), TestContext.CancellationToken);
93106
action.TheQuestion.Should().Be(args[0]);
94107
action.TheAnswer.Should().Be(42);
95108
actionResult.Should().Be(args[0].Length + 42);
@@ -105,7 +118,7 @@ public async Task InvokeAsync_OneStringOptionCommandAction_returning_Option_leng
105118
var action = new OneStringOptionCommandAction();
106119
var bindingAction = new AsyncBindingCommandLineAction<OneStringOptionCommandAction>(command, () => action);
107120
bindingAction.Should().NotBeNull();
108-
var actionResult = await bindingAction.InvokeAsync(command.Parse("-o whatever"), TestContext.CancellationTokenSource.Token);
121+
var actionResult = await bindingAction.InvokeAsync(command.Parse("-o whatever"), TestContext.CancellationToken);
109122
action.TheOption.Should().Be("whatever");
110123
actionResult.Should().Be(action.TheOption.Length).And.Be("whatever".Length);
111124
}
@@ -120,7 +133,7 @@ public async Task InvokeAsync_OneStringOptionCommandAction_returning_default_giv
120133
var action = new OneStringOptionCommandAction();
121134
var bindingAction = new AsyncBindingCommandLineAction<OneStringOptionCommandAction>(command, () => action);
122135
bindingAction.Should().NotBeNull();
123-
var actionResult = await bindingAction.InvokeAsync(command.Parse("-o whatever"), TestContext.CancellationTokenSource.Token);
136+
var actionResult = await bindingAction.InvokeAsync(command.Parse("-o whatever"), TestContext.CancellationToken);
124137
action.TheOption.Should().BeEmpty();
125138
actionResult.Should().Be(0);
126139
}
@@ -136,7 +149,7 @@ public async Task InvokeAsync_ComplexArgAndOptionCommandAction_returning_Symbol_
136149
var action = new ComplexArgAndOptionCommandAction();
137150
var bindingAction = new AsyncBindingCommandLineAction<ComplexArgAndOptionCommandAction>(command, () => action);
138151
bindingAction.Should().NotBeNull();
139-
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB -f testfile"), TestContext.CancellationTokenSource.Token);
152+
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB -f testfile"), TestContext.CancellationToken);
140153
var file = new FileInfo("testfile");
141154
action.GuidArgs.Should().HaveCount(1).And.Contain(Guid.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB"));
142155
action.FileOption.Should().NotBeNull().And.Satisfy<FileInfo>(x => x.FullName.Should().Be(file.FullName));
@@ -154,7 +167,7 @@ public async Task InvokeAsync_ComplexArgAndOptionCommandAction_returning_Symbol_
154167
var action = new ComplexArgAndOptionCommandAction();
155168
var bindingAction = new AsyncBindingCommandLineAction<ComplexArgAndOptionCommandAction>(command, () => action);
156169
bindingAction.Should().NotBeNull();
157-
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB 43B95992-25E0-40BC-AC59-D8B3E4CB7BFD -f testfile"), TestContext.CancellationTokenSource.Token);
170+
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB 43B95992-25E0-40BC-AC59-D8B3E4CB7BFD -f testfile"), TestContext.CancellationToken);
158171
var testfile = new FileInfo("testfile");
159172
action.GuidArgs.Should().HaveCount(2).And.Contain([Guid.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB"), Guid.Parse("43B95992-25E0-40BC-AC59-D8B3E4CB7BFD")]);
160173
action.FileOption.Should().NotBeNull().And.Satisfy<FileInfo>(x => x.FullName.Should().Be(testfile.FullName));
@@ -171,7 +184,7 @@ public async Task InvokeAsync_ComplexArgAndOptionCommandAction_returning_Symbol_
171184
var action = new ComplexArgAndOptionCommandAction();
172185
var bindingAction = new AsyncBindingCommandLineAction<ComplexArgAndOptionCommandAction>(command, () => action);
173186
bindingAction.Should().NotBeNull();
174-
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB 43B95992-25E0-40BC-AC59-D8B3E4CB7BFD"), TestContext.CancellationTokenSource.Token);
187+
var actionResult = await bindingAction.InvokeAsync(command.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB 43B95992-25E0-40BC-AC59-D8B3E4CB7BFD"), TestContext.CancellationToken);
175188
var file = new FileInfo("testfile");
176189
action.GuidArgs.Should().HaveCount(2).And.Contain([Guid.Parse("E7AB96D2-C535-416B-959D-6DFC4F2F50AB"), Guid.Parse("43B95992-25E0-40BC-AC59-D8B3E4CB7BFD")]);
177190
action.FileOption.Should().BeNull();

test/CommandLineX.Tests/CommandLineHostedServiceTest.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ public async Task StartAsync_setting_ExitCode_to_result_of_NoArgsCommandAction_m
8181
var service = new CommandLineHostedService(invoker, new CommandLineInvocationContext(["noarg"]), lifetime
8282
, _serviceProvider.GetRequiredService<ILogger<CommandLineHostedService>>());
8383

84-
await service.StartAsync(TestContext.CancellationTokenSource.Token);
85-
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationTokenSource.Token);
84+
await service.StartAsync(TestContext.CancellationToken);
85+
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationToken);
8686
finished.Should().BeTrue();
8787
service.Result.Should().Be(84);
8888
}
@@ -99,8 +99,8 @@ public async Task StartAsync_setting_ExitCode_to_result_of_OneIntArgCommandActio
9999
var service = new CommandLineHostedService(invoker, new CommandLineInvocationContext(["onearg", "42"]), lifetime
100100
, _serviceProvider.GetRequiredService<ILogger<CommandLineHostedService>>());
101101

102-
await service.StartAsync(TestContext.CancellationTokenSource.Token);
103-
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationTokenSource.Token);
102+
await service.StartAsync(TestContext.CancellationToken);
103+
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationToken);
104104
finished.Should().BeTrue();
105105
service.Result.Should().Be(42);
106106
}
@@ -117,8 +117,8 @@ public async Task StartAsync_logging_Exception_thrown_by_OneStringOptionCommandA
117117
var lifetime = new HostApplicationLifetimeMock();
118118
var service = new CommandLineHostedService(invoker, new CommandLineInvocationContext(["oneopt", "-o", "error"]), lifetime, logger);
119119

120-
await service.StartAsync(TestContext.CancellationTokenSource.Token);
121-
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationTokenSource.Token);
120+
await service.StartAsync(TestContext.CancellationToken);
121+
var finished = await lifetime.WaitForStopAsync(TestContext.CancellationToken);
122122
finished.Should().BeTrue();
123123
service.Result.Should().Be(-2);
124124

test/CommandLineX.Tests/CommandLineInvokerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task InvokeAsync_OneIntArgCommandAction_returning_Argument_given_Co
4848
SetupServices<OneIntArgCommandAction>(command, true);
4949

5050
var invoker = new CommandLineInvoker([command], _registry, _serviceProvider);
51-
var commandResult = await invoker.InvokeAsync(["onearg", "42"], TestContext.CancellationTokenSource.Token);
51+
var commandResult = await invoker.InvokeAsync(["onearg", "42"], TestContext.CancellationToken);
5252
commandResult.Should().Be(42);
5353
}
5454

test/CommandLineX.Tests/CommmandLineHostingExtensionTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ public async Task RunCommandLineAsync_throwing_Exception_given_hosted_invocation
167167

168168
using var host = builder.Build();
169169
host.Should().NotBeNull();
170-
await host.Invoking(async x => await x.RunCommandLineAsync(["onearg", "66"], TestContext.CancellationTokenSource.Token)).Should().ThrowAsync<InvalidOperationException>();
170+
await host.Invoking(async x => await x.RunCommandLineAsync(["onearg", "66"], TestContext.CancellationToken)).Should().ThrowAsync<InvalidOperationException>();
171171
}
172172

173173
[TestMethod]
@@ -186,7 +186,7 @@ public async Task RunCommandLineAsync_returning_result_of_ComplexArgAndOptionCom
186186

187187
using var host = builder.Build();
188188
host.Should().NotBeNull();
189-
var runResult = await host.RunCommandLineAsync(["argandopt", "E7AB96D2-C535-416B-959D-6DFC4F2F50AB", "-f", "testfile"], TestContext.CancellationTokenSource.Token);
189+
var runResult = await host.RunCommandLineAsync(["argandopt", "E7AB96D2-C535-416B-959D-6DFC4F2F50AB", "-f", "testfile"], TestContext.CancellationToken);
190190
runResult.Should().Be(2);
191191
}
192192

@@ -236,7 +236,7 @@ public async Task RunCommandLineHostedAsync_throwing_Exception_given_missing_Com
236236

237237
using var host = builder.Build();
238238
host.Should().NotBeNull();
239-
await host.Invoking(async x => await x.RunCommandLineHostedAsync(TestContext.CancellationTokenSource.Token)).Should().ThrowAsync<InvalidOperationException>();
239+
await host.Invoking(async x => await x.RunCommandLineHostedAsync(TestContext.CancellationToken)).Should().ThrowAsync<InvalidOperationException>();
240240
}
241241

242242
[TestMethod]
@@ -256,7 +256,7 @@ public async Task RunCommandLineHostedAsync_returning_result_of_ComplexArgAndOpt
256256

257257
using var host = builder.Build();
258258
host.Should().NotBeNull();
259-
var runResult = await host.RunCommandLineHostedAsync(TestContext.CancellationTokenSource.Token);
259+
var runResult = await host.RunCommandLineHostedAsync(TestContext.CancellationToken);
260260
runResult.Should().Be(2);
261261
}
262262
}

0 commit comments

Comments
 (0)