Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # MinVer reads the version from git tags

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x

- name: Restore
run: dotnet restore holonsoft.CmdLineParser.slnx

- name: Build
run: dotnet build holonsoft.CmdLineParser.slnx -c Release --no-restore

- name: Test
run: dotnet test holonsoft.CmdLineParser.Tests/holonsoft.CmdLineParser.Tests.csproj -c Release --no-build

- name: Pack
run: dotnet pack holonsoft.CmdLineParser.slnx -c Release --no-build -o artifacts

- name: Upload packages
if: matrix.os == 'ubuntu-latest'
uses: actions/upload-artifact@v7
with:
name: packages
path: artifacts/*.nupkg
59 changes: 59 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Publish to NuGet

# Triggered by version tags such as v5.0.0 or v5.1.0-preview.1.
# Authenticates against nuget.org with trusted publishing (OIDC), no API key is stored anywhere.
# The nuget.org policy matches this file name (publish.yml) and the environment name (release).

on:
push:
tags:
- 'v*'

permissions:
contents: read

jobs:
publish:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # required for the OIDC token that NuGet/login exchanges
contents: read

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0 # MinVer reads the version from git tags

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x

- name: Build
run: dotnet build holonsoft.CmdLineParser.slnx -c Release

- name: Test
run: dotnet test holonsoft.CmdLineParser.Tests/holonsoft.CmdLineParser.Tests.csproj -c Release --no-build

- name: Pack
run: dotnet pack holonsoft.CmdLineParser.slnx -c Release --no-build -o artifacts

- name: Verify package version matches the tag
run: |
expected="${GITHUB_REF_NAME#v}"
ls -l artifacts
test -f "artifacts/holonsoft.CmdLineParser.${expected}.nupkg"
test -f "artifacts/holonsoft.CmdLineParser.Abstractions.${expected}.nupkg"

- name: NuGet login (OIDC)
id: login
uses: NuGet/login@v1
with:
user: ${{ secrets.NUGET_USER }} # nuget.org profile name, not the email

- name: Push to nuget.org
run: dotnet nuget push "artifacts/*.nupkg" --api-key ${{ steps.login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,6 @@ healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
/Acs.BackendHost/app.config

# Editor-local settings
.vscode/
12 changes: 11 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
</PropertyGroup>

<PropertyGroup Label="Package metadata">
<Version>5.0.0</Version>
<Company>holonsoft</Company>
<Authors>Christian Vogt</Authors>
<Copyright>Christian Vogt, holonsoft</Copyright>
Expand All @@ -36,4 +35,15 @@
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="\" Visible="false" />
</ItemGroup>

<PropertyGroup Label="Versioning">
<!-- MinVer derives the version from git tags (v5.0.0). Untagged commits become 5.0.x-preview.0.N. -->
<MinVerTagPrefix>v</MinVerTagPrefix>
<MinVerMinimumMajorMinor>5.0</MinVerMinimumMajorMinor>
<MinVerDefaultPreReleaseIdentifiers>preview.0</MinVerDefaultPreReleaseIdentifiers>
</PropertyGroup>

<ItemGroup Condition="'$(IsPackable)' != 'false'">
<PackageReference Include="MinVer" PrivateAssets="all" />
</ItemGroup>

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

<ItemGroup Label="Build">
<PackageVersion Include="MinVer" Version="7.0.0" />
</ItemGroup>

<ItemGroup Label="Benchmarks">
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CmdLineParser

[![CI](https://github.com/holonsoft/CmdLineParser/actions/workflows/ci.yml/badge.svg)](https://github.com/holonsoft/CmdLineParser/actions/workflows/ci.yml)
[![NuGet](https://img.shields.io/nuget/v/holonsoft.CmdLineParser.svg)](https://www.nuget.org/packages/holonsoft.CmdLineParser/)

Reflection based fast command line parser (`args[]` -> POCO).

Define a class, put attributes on its public fields or properties, call `Parse`. You get a filled object, a list of structured errors and a help text. No builder DSL, no handlers, no ceremony.
Expand Down Expand Up @@ -150,7 +153,7 @@ public class Options : IValidatableArguments {

## Parsing rules

* `-name`, `--name` and `/name` are equivalent. `/` can be disabled in the options for Unix style tools.
* `-name` and `--name` are equivalent. `/name` is accepted too when `AllowSlashPrefix` is on. That is the default on Windows only, because absolute paths start with `/` on Unix-like systems.
* A value can follow as separate token or inline: `-name value`, `-name:value`, `-name=value`. Only the first separator counts, so `-url:http://x` works.
* Values wrapped in double quotes are unquoted.
* Negative numbers do not need quotes: `-Value -5`, `-Points -1.5 -.5 2`.
Expand Down Expand Up @@ -231,7 +234,7 @@ switch (result.Value) {
```csharp
var options = new CommandLineParserOptions {
IgnoreCase = true, // default false
AllowSlashPrefix = false, // default true
AllowSlashPrefix = true, // default: true on Windows, false elsewhere
ValueSeparators = [':', '='], // default
RecognizeEndOfOptionsMarker = true, // default
AutoHelp = true, // default
Expand Down Expand Up @@ -275,6 +278,7 @@ Breaking changes in 5.0:
* Empty `args` applies defaults and checks required arguments.
* Built-in help names `help`, `h` and `?` are recognized unless your class defines them.
* `--name=value`, unquoted negative numbers and `@file` response files are recognized. A bare `--` ends the options.
* `/name` is recognized by default on Windows only. Set `AllowSlashPrefix = true` to keep it on Linux and macOS.
* `Uri`, `Version`, `FileInfo`, `DirectoryInfo` and many more types are supported. Byte overflow is an error instead of silent truncation.
* The `Argument` class was removed from the Abstractions package. `ArgumentAttribute` string properties are nullable. Missing required arguments are reported once, not once per alias.
* `GetHelpTexts()` returns empty strings instead of null for missing parts and sorts case-insensitively.
Expand Down
30 changes: 24 additions & 6 deletions holonsoft.CmdLineParser.Tests/ArgumentLexerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ private static ArgumentLexer Create(Action<CommandLineParserOptions>? configure
[Theory]
[InlineData("-name", "name")]
[InlineData("--name", "name")]
[InlineData("/name", "name")]
[InlineData("-n", "n")]
[InlineData("/?", "?")]
[InlineData("- name ", "name")]
public void OptionPrefixes(string raw, string expectedName) {
var token = Create().Classify(raw, afterEndOfOptions: false);
Expand All @@ -28,7 +26,6 @@ public void OptionPrefixes(string raw, string expectedName) {
[Theory]
[InlineData("-name:value", "name", "value")]
[InlineData("--name=value", "name", "value")]
[InlineData("/name:\"quoted value\"", "name", "quoted value")]
[InlineData("-url:http://host:8080/x", "url", "http://host:8080/x")]
[InlineData("-path=C:\\temp", "path", "C:\\temp")]
[InlineData("-x:a=b", "x", "a=b")]
Expand Down Expand Up @@ -82,11 +79,32 @@ public void EndOfOptionsMarkerCanBeDisabled() {
}

[Fact]
public void SlashPrefixCanBeDisabled() {
var token = Create(o => o.AllowSlashPrefix = false).Classify("/etc/passwd", afterEndOfOptions: false);
public void SlashPrefixDefaultFollowsTheOperatingSystem() {
Assert.Equal(OperatingSystem.IsWindows(), new CommandLineParserOptions().AllowSlashPrefix);
}

[Theory]
[InlineData("/name", "name", null)]
[InlineData("/?", "?", null)]
[InlineData("/name:\"quoted value\"", "name", "quoted value")]
[InlineData("/etc/passwd", "etc/passwd", null)]
public void SlashPrefixWhenEnabled(string raw, string expectedName, string? expectedValue) {
var token = Create(o => o.AllowSlashPrefix = true).Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Option, token.Kind);
Assert.Equal(expectedName, token.Name);
Assert.Equal(expectedValue, token.Value);
}

[Theory]
[InlineData("/name")]
[InlineData("/?")]
[InlineData("/etc/passwd")]
public void SlashPrefixWhenDisabled(string raw) {
var token = Create(o => o.AllowSlashPrefix = false).Classify(raw, afterEndOfOptions: false);

Assert.Equal(LexedTokenKind.Value, token.Kind);
Assert.Equal("/etc/passwd", token.Value);
Assert.Equal(raw, token.Value);
}

[Fact]
Expand Down
Loading