Split code from SabreTools

This commit is contained in:
Matt Nadareski
2025-10-04 18:35:33 -04:00
commit 081e758dd6
44 changed files with 5301 additions and 0 deletions

40
.github/workflows/build_and_test.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Build and Test
on:
push:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Run tests
run: dotnet test
- name: Run publish script
run: ./publish-nix.sh
- name: Upload to rolling
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: True
artifacts: "*.nupkg,*.snupkg"
body: 'Last built commit: ${{ github.sha }}'
name: 'Rolling Release'
prerelease: True
replacesArtifacts: True
tag: "rolling"
updateOnlyUnreleased: True

23
.github/workflows/check_pr.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Build PR
on: [pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
6.0.x
8.0.x
9.0.x
- name: Build
run: dotnet build
- name: Run tests
run: dotnet test

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/

7
LICENSE Executable file
View File

@@ -0,0 +1,7 @@
Copyright (c) 2016-2025 Matt Nadareski
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

64
README.MD Normal file
View File

@@ -0,0 +1,64 @@
# SabreTools.CommandLine
This library contains logic to both parse commandlines using typed inputs as well as format and output help text. Formatting and output is context-aware, allowing for different behavior if console outputs are redirected.
All inputs allow for non-flag inputs in both `a=b` and `a b` formats. This allows for greater flexibility for cross-platform support. Flags for each input are fully defined by the implementer, so inputs of the form `-a`, `--a`, `/a`, and `a` are all accepted and can even be mixed. The only restriction is that flags should not end with the `=` character as it may interfere with default parsing.
For an example of a program implementing the library, see [SabreTools](https://github.com/SabreTools/SabreTools).
## Special Types
Included in the library are a few special classes that make up the core of the functionality. Each work a different layers of implementation and can be used in combination with each other. For more details on their language construct equivalents and longer descriptions, see the table and sections below.
| Class | Language Construct |
| --- | --- |
| `CommandSet` | Vocabulary |
| `Feature` | Verb |
| `Inputs.UserInput` | Noun |
### `CommandSet`
Represents a logically-grouped set of functionality, usually scoped to an application. Inputs defined in a command set do not need to be any specific type, but using `Feature` is recommended. `CommandSet` also allows implementers to easily print help text to the screen.
An example of a command set would be including `Program.exe feature1` and `Program.exe feature2`as top-level pieces of functionality. This can allow implementers to have multiple command sets that depend on execution environment or even other commandline arguments. In single-operation or single-set programs, this can represent the default state of the program internally.
### `Feature`
Represents an application-level feature that may have its own custom set of supported inputs. Features can also allow for internal processing if preferred by the implementer. A default implementation of argument parsing is included but can be overridden by any implementing class. It is recommended but not required to include defined features in a `CommandSet` to allow for better flexibility.
An example of a Feature would be something like `Program.exe featurename` where it represents a single type of operation that can be done. In single-operation programs, this can represent the default state of the program internally without needing an external name.
### `Inputs.UserInput`
Base class used for all supported input classes. Both typed and untyped variants exist. This can be used to define custom input classes for any type, including types already defined by one of the default included classes. The typed version of the base class is recommended, but not mandatory.
`Get` and `TryGet` methods are included to make finding values from child items easier. If new input types are defined by the implementer, it is recommended that extension methods are created to include this functionality.
## Supported Input Formats
Below is a mapping from default supported types to their respective class names in `SabreTools.CommandLine.Inputs`.
| Type | Class | Notes |
| --- | --- | --- |
| `bool` | `BooleanInput` | Requires either `true` or `false` as the value, e.g. `--flag=true` |
| `bool` | `FlagInput` | Inclusion of this indicates a `true` value, e.g. `--flag` |
| `sbyte` | `Int8Input` | Numeric input bounded to `sbyte.MinValue` to `sbyte.MaxValue`, inclusive |
| `short` | `Int16Input` | Numeric input bounded to `short.MinValue` to `short.MaxValue`, inclusive |
| `int` | `Int32Input` | Numeric input bounded to `int.MinValue` to `int.MaxValue`, inclusive |
| `long` | `Int64Input` | Numeric input bounded to `long.MinValue` to `long.MaxValue`, inclusive |
| `string?` | `StringInput` | Non-repeating generic string input; may be empty |
| `List<string>` | `StringListInput` | Repeating generic string input; may be empty |
| `byte` | `UInt8Input` | Numeric input bounded to `byte.MinValue` to `byte.MaxValue`, inclusive |
| `ushort` | `UInt16Input` | Numeric input bounded to `ushort.MinValue` to `ushort.MaxValue`, inclusive |
| `uint` | `UInt32Input` | Numeric input bounded to `uint.MinValue` to `uint.MaxValue`, inclusive |
| `ulong` | `UInt64Input` | Numeric input bounded to `ulong.MinValue` to `ulong.MaxValue`, inclusive |
## Reference Implementations
Three reference feature implementations are included for common cases. They are defined in the table below.
| Class | Description |
| --- | --- |
| `DefaultHelp` | Outputs either a generic help text or one specific to a feature name included as the second argument |
| `DefaultHelpExtended` | Outputs either a generic help text with extended descriptions or one specific to a feature name included as the second argument |
| `DefaultVersion` | Outputs the version number of the program, derived from the informational version |

View File

@@ -0,0 +1,72 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test
{
public class CommandSetTests
{
[Fact]
public void AddAndRetrieveTest()
{
var input1 = new FlagInput("input1", "--input1", "input1");
var input2 = new FlagInput("input2", "--input2", "input2");
var featureSet = new CommandSet();
featureSet.Add(input1);
featureSet.Add(input2);
var actualInput1 = featureSet["input1"];
Assert.NotNull(actualInput1);
Assert.Equal("input1", actualInput1.Name);
var actualInput2 = featureSet[input2];
Assert.NotNull(actualInput2);
Assert.Equal("input2", actualInput2.Name);
var actualInput3 = featureSet["input3"];
Assert.Null(actualInput3);
}
[Fact]
public void GetInputNameTest()
{
var input1 = new FlagInput("input1", "--input1", "input1");
var input2 = new FlagInput("input2", "--input2", "input2");
var featureSet = new CommandSet();
featureSet.Add(input1);
featureSet.Add(input2);
var actualName1 = featureSet.GetInputName("input1");
Assert.NotEmpty(actualName1);
Assert.Equal("input1", actualName1);
var actualName2 = featureSet.GetInputName("--input2");
Assert.NotEmpty(actualName2);
Assert.Equal("input2", actualName2);
var actualName3 = featureSet.GetInputName("input3");
Assert.Empty(actualName3);
}
[Fact]
public void TopLevelFlagTest()
{
var input1 = new FlagInput("input1", "--input1", "input1");
var input2 = new FlagInput("input2", "--input2", "input2");
var featureSet = new CommandSet();
featureSet.Add(input1);
featureSet.Add(input2);
bool actualTop1 = featureSet.TopLevelFlag("input1");
Assert.True(actualTop1);
bool actualTop2 = featureSet.TopLevelFlag("--input2");
Assert.True(actualTop2);
bool actualTop3 = featureSet.TopLevelFlag("input3");
Assert.False(actualTop3);
}
}
}

View File

@@ -0,0 +1,115 @@
using Xunit;
namespace SabreTools.CommandLine.Test
{
public class FeatureTests
{
[Fact]
public void ProcessArgs_EmptyArgs_Success()
{
Feature feature = new MockFeature("", "", "");
string[] args = [];
int index = 0;
bool actual = feature.ProcessArgs(args, index);
Assert.True(actual);
Assert.Empty(feature.Inputs);
}
[Fact]
public void ProcessArgs_NegativeIndex_Failure()
{
Feature feature = new MockFeature("", "", "");
string[] args = ["a", "b", "c"];
int index = -1;
bool actual = feature.ProcessArgs(args, index);
Assert.False(actual);
Assert.Empty(feature.Inputs);
}
[Fact]
public void ProcessArgs_OverIndex_Failure()
{
Feature feature = new MockFeature("", "", "");
string[] args = ["a", "b", "c"];
int index = 3;
bool actual = feature.ProcessArgs(args, index);
Assert.False(actual);
Assert.Empty(feature.Inputs);
}
[Fact]
public void ProcessArgs_ValidArgs_Success()
{
Feature feature = new MockFeature("a", "a", "a");
feature.Add(new MockFeature("b", "b", "b"));
feature.Add(new MockFeature("c", "c", "c"));
string[] args = ["a", "b", "c"];
int index = 0;
bool actual = feature.ProcessArgs(args, index);
Assert.True(actual);
Assert.Empty(feature.Inputs);
}
[Fact]
public void ProcessArgs_InvalidArg_AddedAsGeneric()
{
Feature feature = new MockFeature("a", "a", "a");
feature.Add(new MockFeature("b", "b", "b"));
feature.Add(new MockFeature("d", "d", "d"));
string[] args = ["a", "b", "c"];
int index = 0;
bool actual = feature.ProcessArgs(args, index);
Assert.True(actual);
string input = Assert.Single(feature.Inputs);
Assert.Equal("c", input);
}
[Fact]
public void ProcessArgs_NestedArgs_Success()
{
Feature feature = new MockFeature("a", "a", "a");
var sub = new MockFeature("b", "b", "b");
sub.Add(new MockFeature("c", "c", "c"));
feature.Add(sub);
string[] args = ["a", "b", "c"];
int index = 0;
bool actual = feature.ProcessArgs(args, index);
Assert.True(actual);
Assert.Empty(feature.Inputs);
}
/// <summary>
/// Mock Feature implementation for testing
/// </summary>
private class MockFeature : Feature
{
public MockFeature(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
}
public MockFeature(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
}
/// <inheritdoc/>
public override bool VerifyInputs() => true;
/// <inheritdoc/>
public override bool Execute() => true;
}
}
}

View File

@@ -0,0 +1,134 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class BooleanInputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "true"];
int index = -1;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "true"];
int index = 2;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "true"];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
Assert.True(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=true"];
int index = 0;
var input = new BooleanInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.True(input.Value);
}
}
}

View File

@@ -0,0 +1,64 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class FlagInputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new FlagInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.False(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "true"];
int index = -1;
var input = new FlagInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.False(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "true"];
int index = 2;
var input = new FlagInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.False(input.Value);
}
[Fact]
public void ProcessInput_ValidValue_Success()
{
string[] args = ["a"];
int index = 0;
var input = new FlagInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.True(input.Value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class Int16InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
short value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new Int16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
short value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class Int32InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
int value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new Int32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
int value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class Int64InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
long value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new Int64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
long value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class Int8InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
sbyte value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new Int8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
sbyte value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,107 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class StringInputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "value"];
int index = -1;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "value"];
int index = 2;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "value"];
int index = 0;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
Assert.Equal("value", input.Value);
}
[Fact]
public void ProcessInput_Equal_Empty_Success()
{
string[] args = ["a="];
int index = 0;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.NotNull(input.Value);
Assert.Empty(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=value"];
int index = 0;
var input = new StringInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.Equal("value", input.Value);
}
}
}

View File

@@ -0,0 +1,112 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class StringListInputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "true"];
int index = -1;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "true"];
int index = 2;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "value"];
int index = 0;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
Assert.NotNull(input.Value);
var value = Assert.Single(input.Value);
Assert.Equal("value", value);
}
[Fact]
public void ProcessInput_Equal_Empty_Success()
{
string[] args = ["a="];
int index = 0;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.NotNull(input.Value);
var value = Assert.Single(input.Value);
Assert.Empty(value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=value"];
int index = 0;
var input = new StringListInput("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
Assert.NotNull(input.Value);
var value = Assert.Single(input.Value);
Assert.Equal("value", value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class UInt16InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
ushort value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new UInt16Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
ushort value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class UInt32InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
uint value = Assert.NotNull(input.Value);
Assert.Equal(5u, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new UInt32Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
uint value = Assert.NotNull(input.Value);
Assert.Equal(5u, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class UInt64InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
ulong value = Assert.NotNull(input.Value);
Assert.Equal(5u, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new UInt64Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
ulong value = Assert.NotNull(input.Value);
Assert.Equal(5u, value);
}
}
}

View File

@@ -0,0 +1,136 @@
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class UInt8InputTests
{
[Fact]
public void ProcessInput_EmptyArgs_Failure()
{
string[] args = [];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_NegativeIndex_Failure()
{
string[] args = ["a", "5"];
int index = -1;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(-1, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_OverIndex_Failure()
{
string[] args = ["a", "5"];
int index = 2;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(2, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidLength_Failure()
{
string[] args = ["a"];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_InvalidValue_Failure()
{
string[] args = ["a", "ANY"];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Space_ValidValue_Success()
{
string[] args = ["a", "5"];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(1, index);
byte value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
[Fact]
public void ProcessInput_Equal_InvalidLength_Failure()
{
string[] args = ["a="];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_InvalidValue_Failure()
{
string[] args = ["a=ANY"];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.False(actual);
Assert.Equal(0, index);
Assert.Null(input.Value);
}
[Fact]
public void ProcessInput_Equal_ValidValue_Success()
{
string[] args = ["a=5"];
int index = 0;
var input = new UInt8Input("a", "a", "a");
bool actual = input.ProcessInput(args, ref index);
Assert.True(actual);
Assert.Equal(0, index);
byte value = Assert.NotNull(input.Value);
Assert.Equal(5, value);
}
}
}

View File

@@ -0,0 +1,691 @@
using System;
using System.Collections.Generic;
using SabreTools.CommandLine.Inputs;
using Xunit;
namespace SabreTools.CommandLine.Test.Inputs
{
public class UserInputTests
{
[Fact]
public void AddAndRetrieveTest()
{
var input1 = new FlagInput("input1", "--input1", "input1");
var input2 = new FlagInput("input2", "--input2", "input2");
var userInput = new MockUserInput("a", "a", "a");
userInput.Add(input1);
userInput.Add(input2);
var actualInput1 = userInput["input1"];
Assert.NotNull(actualInput1);
Assert.Equal("input1", actualInput1.Name);
var actualInput2 = userInput[input2];
Assert.NotNull(actualInput2);
Assert.Equal("input2", actualInput2.Name);
var actualInput3 = userInput["input3"];
Assert.Null(actualInput3);
}
[Fact]
public void ContainsFlagTest()
{
var userInput = new MockUserInput("a", ["a", "--b"], "a");
bool exactActual = userInput.ContainsFlag("a");
Assert.True(exactActual);
bool equalsActual = userInput.ContainsFlag("--b=");
Assert.True(equalsActual);
bool noMatchActual = userInput.ContainsFlag("-c");
Assert.False(noMatchActual);
}
[Fact]
public void StartsWithTest()
{
var userInput = new MockUserInput("a", ["a", "--b"], "a");
bool exactActual = userInput.StartsWith('a');
Assert.True(exactActual);
bool trimActual = userInput.StartsWith('b');
Assert.True(trimActual);
bool noMatchActual = userInput.StartsWith('c');
Assert.False(noMatchActual);
}
#region GetBoolean
[Fact]
public void GetBoolean_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new BooleanInput("b", "b", "b");
userInput.Add(child);
bool actual = userInput.GetBoolean("c");
Assert.False(actual);
}
[Fact]
public void GetBoolean_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetBoolean("b"));
}
[Fact]
public void GetBoolean_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new BooleanInput("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "true"], ref index);
bool actual = userInput.GetBoolean("b");
Assert.True(actual);
}
[Fact]
public void GetBoolean_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new BooleanInput("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "true"], ref index);
bool actual = userInput.GetBoolean("c");
Assert.True(actual);
}
#endregion
#region GetInt8
[Fact]
public void GetInt8_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int8Input("b", "b", "b");
userInput.Add(child);
sbyte actual = userInput.GetInt8("c");
Assert.Equal(sbyte.MinValue, actual);
}
[Fact]
public void GetInt8_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetInt8("b"));
}
[Fact]
public void GetInt8_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int8Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
sbyte actual = userInput.GetInt8("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetInt8_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new Int8Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
sbyte actual = userInput.GetInt8("c");
Assert.Equal(5, actual);
}
#endregion
#region GetInt16
[Fact]
public void GetInt16_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int16Input("b", "b", "b");
userInput.Add(child);
short actual = userInput.GetInt16("c");
Assert.Equal(short.MinValue, actual);
}
[Fact]
public void GetInt16_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetInt16("b"));
}
[Fact]
public void GetInt16_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int16Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
short actual = userInput.GetInt16("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetInt16_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new Int16Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
short actual = userInput.GetInt16("c");
Assert.Equal(5, actual);
}
#endregion
#region GetInt32
[Fact]
public void GetInt32_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int32Input("b", "b", "b");
userInput.Add(child);
int actual = userInput.GetInt32("c");
Assert.Equal(int.MinValue, actual);
}
[Fact]
public void GetInt32_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetInt32("b"));
}
[Fact]
public void GetInt32_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int32Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
int actual = userInput.GetInt32("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetInt32_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new Int32Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
int actual = userInput.GetInt32("c");
Assert.Equal(5, actual);
}
#endregion
#region GetInt64
[Fact]
public void GetInt64_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int64Input("b", "b", "b");
userInput.Add(child);
long actual = userInput.GetInt64("c");
Assert.Equal(long.MinValue, actual);
}
[Fact]
public void GetInt64_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetInt64("b"));
}
[Fact]
public void GetInt64_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new Int64Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
long actual = userInput.GetInt64("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetInt64_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new Int64Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
long actual = userInput.GetInt64("c");
Assert.Equal(5, actual);
}
#endregion
#region GetString
[Fact]
public void GetString_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new StringInput("b", "b", "b");
userInput.Add(child);
string? actual = userInput.GetString("c");
Assert.Null(actual);
}
[Fact]
public void GetString_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetString("b"));
}
[Fact]
public void GetString_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new StringInput("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "value"], ref index);
string? actual = userInput.GetString("b");
Assert.Equal("value", actual);
}
[Fact]
public void GetString_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new StringInput("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "value"], ref index);
string? actual = userInput.GetString("c");
Assert.Equal("value", actual);
}
#endregion
#region GetStringList
[Fact]
public void GetStringList_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new StringListInput("b", "b", "b");
userInput.Add(child);
List<string> actual = userInput.GetStringList("c");
Assert.Empty(actual);
}
[Fact]
public void GetStringList_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetStringList("b"));
}
[Fact]
public void GetStringList_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new StringListInput("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "value"], ref index);
List<string> actual = userInput.GetStringList("b");
string value = Assert.Single(actual);
Assert.Equal("value", value);
}
[Fact]
public void GetStringList_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new StringListInput("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "value"], ref index);
List<string> actual = userInput.GetStringList("c");
string value = Assert.Single(actual);
Assert.Equal("value", value);
}
#endregion
#region GetUInt8
[Fact]
public void GetUInt8_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt8Input("b", "b", "b");
userInput.Add(child);
byte actual = userInput.GetUInt8("c");
Assert.Equal(byte.MinValue, actual);
}
[Fact]
public void GetUInt8_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetUInt8("b"));
}
[Fact]
public void GetUInt8_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt8Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
byte actual = userInput.GetUInt8("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetUInt8_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new UInt8Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
byte actual = userInput.GetUInt8("c");
Assert.Equal(5, actual);
}
#endregion
#region GetUInt16
[Fact]
public void GetUInt16_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt16Input("b", "b", "b");
userInput.Add(child);
ushort actual = userInput.GetUInt16("c");
Assert.Equal(ushort.MinValue, actual);
}
[Fact]
public void GetUInt16_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetUInt16("b"));
}
[Fact]
public void GetUInt16_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt16Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
ushort actual = userInput.GetUInt16("b");
Assert.Equal(5, actual);
}
[Fact]
public void GetUInt16_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new UInt16Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
ushort actual = userInput.GetUInt16("c");
Assert.Equal(5, actual);
}
#endregion
#region GetUInt32
[Fact]
public void GetUInt32_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt32Input("b", "b", "b");
userInput.Add(child);
uint actual = userInput.GetUInt32("c");
Assert.Equal(uint.MinValue, actual);
}
[Fact]
public void GetUInt32_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetUInt32("b"));
}
[Fact]
public void GetUInt32_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt32Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
uint actual = userInput.GetUInt32("b");
Assert.Equal(5u, actual);
}
[Fact]
public void GetUInt32_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new UInt32Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
uint actual = userInput.GetUInt32("c");
Assert.Equal(5u, actual);
}
#endregion
#region GetUInt64
[Fact]
public void GetUInt64_InvalidKey_DefaultValue()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt64Input("b", "b", "b");
userInput.Add(child);
ulong actual = userInput.GetUInt64("c");
Assert.Equal(ulong.MinValue, actual);
}
[Fact]
public void GetUInt64_Exists_WrongType_Throws()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
Assert.Throws<ArgumentException>(() => _ = userInput.GetUInt64("b"));
}
[Fact]
public void GetUInt64_Exists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new UInt64Input("b", "b", "b");
userInput.Add(child);
int index = 0;
child.ProcessInput(["b", "5"], ref index);
ulong actual = userInput.GetUInt64("b");
Assert.Equal(5u, actual);
}
[Fact]
public void GetUInt64_NestedExists_Returns()
{
UserInput userInput = new MockUserInput("a", "a", "a");
var child = new MockUserInput("b", "b", "b");
userInput.Add(child);
var subChild = new UInt64Input("c", "c", "c");
child.Add(subChild);
int index = 0;
subChild.ProcessInput(["c", "5"], ref index);
ulong actual = userInput.GetUInt64("c");
Assert.Equal(5u, actual);
}
#endregion
/// <summary>
/// Mock UserInput implementation for testing
/// </summary>
private class MockUserInput : UserInput<object?>
{
public MockUserInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
}
public MockUserInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
}
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index) => true;
/// <inheritdoc/>
protected override string FormatFlags() => string.Empty;
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SabreTools.CommandLine\SabreTools.CommandLine.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.CommandLine", "SabreTools.CommandLine\SabreTools.CommandLine.csproj", "{B34354AD-82BD-477F-AE21-9BE0B755A6DE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.CommandLine.Test", "SabreTools.CommandLine.Test\SabreTools.CommandLine.Test.csproj", "{4AB88B62-E927-40B2-B491-9C2673A74589}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|x64.ActiveCfg = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|x64.Build.0 = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|x86.ActiveCfg = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Debug|x86.Build.0 = Debug|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|Any CPU.Build.0 = Release|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|x64.ActiveCfg = Release|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|x64.Build.0 = Release|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|x86.ActiveCfg = Release|Any CPU
{B34354AD-82BD-477F-AE21-9BE0B755A6DE}.Release|x86.Build.0 = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|x64.ActiveCfg = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|x64.Build.0 = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|x86.ActiveCfg = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Debug|x86.Build.0 = Debug|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|Any CPU.Build.0 = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|x64.ActiveCfg = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|x64.Build.0 = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|x86.ActiveCfg = Release|Any CPU
{4AB88B62-E927-40B2-B491-9C2673A74589}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,312 @@
using System;
using System.Collections.Generic;
using SabreTools.CommandLine.Inputs;
namespace SabreTools.CommandLine
{
/// <summary>
/// Represents a logically-grouped set of user inputs
/// </summary>
/// <remarks>
/// It is recommended to use this class as the primary
/// way to address user inputs from the application. It
/// is also recommended that all directly-included
/// inputs are <see cref="Feature"/> unless the implementing
/// program only has a single utility.
/// </remarks>
public class CommandSet
{
#region Private variables
/// <summary>
/// Preamble used when printing help text to console
/// </summary>
private readonly List<string> _header = [];
/// <summary>
/// Trailing lines used when printing help text to console
/// </summary>
private readonly List<string> _footer = [];
/// <summary>
/// Set of all user inputs in this grouping
/// </summary>
/// <remarks>
/// Only the top level inputs need to be defined. All
/// children will be included by default.
/// </remarks>
private readonly Dictionary<string, UserInput> _inputs = [];
#endregion
#region Constructors
/// <summary>
/// Create a new CommandSet with no printable header
/// </summary>
public CommandSet() { }
/// <summary>
/// Create a new CommandSet with a printable header
/// </summary>
/// <param name="header">Custom commandline header to be printed when outputting help</param>
public CommandSet(List<string> header)
{
_header.AddRange(header);
}
/// <summary>
/// Create a new CommandSet with a printable header
/// </summary>
/// <param name="header">Custom commandline header to be printed when outputting help</param>
/// <param name="footer">Custom commandline footer to be printed when outputting help</param>
public CommandSet(List<string> header, List<string> footer)
{
_header.AddRange(header);
_footer.AddRange(footer);
}
#endregion
#region Accessors
public UserInput? this[string name]
{
get
{
if (!_inputs.ContainsKey(name))
return null;
return _inputs[name];
}
}
public UserInput? this[UserInput subfeature]
{
get
{
if (!_inputs.ContainsKey(subfeature.Name))
return null;
return _inputs[subfeature.Name];
}
}
/// <summary>
/// Add a new input to the set
/// </summary>
/// <param name="input">UserInput object to map to</param>
public void Add(UserInput input)
=> _inputs.Add(input.Name, input);
#endregion
#region Inputs
/// <summary>
/// Get the input name for a given flag or short name
/// </summary>
public string GetInputName(string name)
{
// Pre-split the input for efficiency
string[] splitInput = name.Split('=');
foreach (var key in _inputs.Keys)
{
// Validate the name matches
var feature = _inputs[key];
if (feature.Name == splitInput[0])
return key;
// Validate the flag is contained
if (feature.ContainsFlag(splitInput[0]))
return key;
}
// No feature could be found
return string.Empty;
}
/// <summary>
/// Check if a flag is a top-level (main application) flag
/// </summary>
/// <param name="flag">Name of the flag to check</param>
/// <returns>True if the feature was found, false otherwise</returns>
public bool TopLevelFlag(string flag)
=> GetInputName(flag).Length > 0;
#endregion
#region Output
/// <summary>
/// Output top-level features only
/// </summary>
public void OutputGenericHelp()
{
// Start building the output list
List<string> output = [];
// Append the header, if needed
if (_header.Count > 0)
output.AddRange(_header);
// Now append all available top-level flags
output.Add("Available options:");
foreach (string feature in _inputs.Keys)
{
var outputs = _inputs[feature]?.Output(pre: 2, midpoint: 30);
if (outputs != null)
output.AddRange(outputs);
}
// Append the footer, if needed
if (_footer.Count > 0)
output.AddRange(_footer);
// Now write out everything in a staged manner
WriteOutWithPauses(output);
}
/// <summary>
/// Output all features recursively
/// </summary>
public void OutputAllHelp()
{
// Start building the output list
List<string> output = [];
// Append the header first, if needed
if (_header.Count > 0)
output.AddRange(_header);
// Now append all available flags recursively
output.Add("Available options:");
foreach (string feature in _inputs.Keys)
{
var outputs = _inputs[feature]?.OutputRecursive(0, pre: 2, midpoint: 30, includeLongDescription: true);
if (outputs != null)
output.AddRange(outputs);
}
// Append the footer, if needed
if (_footer.Count > 0)
output.AddRange(_footer);
// Now write out everything in a staged manner
WriteOutWithPauses(output);
}
/// <summary>
/// Output a single feature recursively
/// </summary>
/// <param name="featureName">Name of the feature to output information for, if possible</param>
/// <param name="includeLongDescription">True if the long description should be formatted and output, false otherwise</param>
public void OutputIndividualFeature(string? featureName, bool includeLongDescription = false)
{
// Start building the output list
List<string> output = [];
// If the feature name is null, empty, or just consisting of `-` characters, just show everything
if (string.IsNullOrEmpty(featureName?.TrimStart('-')))
{
OutputGenericHelp();
return;
}
// Now try to find the feature that has the name included
string? realname = null;
List<string> startsWith = [];
foreach (string feature in _inputs.Keys)
{
// If we have a match to the feature name somehow
if (feature == featureName)
{
realname = feature;
break;
}
// If we have an invalid feature
else if (!_inputs.ContainsKey(feature) || _inputs[feature] == null)
{
startsWith.Add(feature);
}
// If we have a match within the flags
else if (_inputs[feature]!.ContainsFlag(featureName!))
{
realname = feature;
break;
}
// Otherwise, we want to get features with the same start
else if (_inputs[feature]!.StartsWith(featureName!.TrimStart('-')[0]))
{
startsWith.Add(feature);
}
}
// If we have a real name found, append all available subflags recursively
if (realname != null)
{
output.Add($"Available options for {realname}:");
output.AddRange(_inputs[realname]!.OutputRecursive(0, pre: 2, midpoint: 30, includeLongDescription: includeLongDescription));
}
// If no name was found but we have possible matches, show them
else if (startsWith.Count > 0)
{
output.Add($"\"{featureName}\" not found. Did you mean:");
foreach (string possible in startsWith)
{
output.AddRange(_inputs[possible]!.Output(pre: 2, midpoint: 30, includeLongDescription: includeLongDescription));
}
}
// Now write out everything in a staged manner
WriteOutWithPauses(output);
}
/// <summary>
/// Pause on console output
/// </summary>
private static void Pause()
{
#if NET452_OR_GREATER || NETCOREAPP || NETSTANDARD2_0_OR_GREATER
if (!Console.IsOutputRedirected)
#endif
{
Console.WriteLine();
Console.WriteLine("Press enter to continue...");
Console.ReadLine();
}
}
/// <summary>
/// Write out the help text with pauses, if needed
/// </summary>
private static void WriteOutWithPauses(List<string> helpText)
{
// Now output based on the size of the screen
int i = 0;
for (int line = 0; line < helpText.Count; line++)
{
string help = helpText[line];
Console.WriteLine(help);
i++;
// If we're not being redirected and we reached the size of the screen, pause
if (i == Console.WindowHeight - 3 && line != helpText.Count - 1)
{
i = 0;
Pause();
}
}
Pause();
}
#endregion
}
}

View File

@@ -0,0 +1,84 @@
using System.Collections.Generic;
using SabreTools.CommandLine.Inputs;
namespace SabreTools.CommandLine
{
/// <summary>
/// Represents an application-level feature
/// </summary>
public abstract class Feature : FlagInput
{
#region Fields
/// <summary>
/// List of undefined inputs
/// </summary>
public readonly List<string> Inputs = [];
/// <summary>
/// Indicates if the feature requires inputs to be set
/// </summary>
public bool RequiresInputs { get; protected set; } = false;
#endregion
#region Constructors
public Feature(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
}
public Feature(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
}
#endregion
#region Processing
/// <summary>
/// Process args list based on current feature
/// </summary>
/// <param name="args">Set of arguments to process</param>
/// <param name="index">Starting index into the arguments</param>
/// <returns>True if all arguments were processed correctly, false otherwise</returns>
public virtual bool ProcessArgs(string[] args, int index)
{
// Empty arguments is always successful
if (args.Length == 0)
return true;
// Invalid index values are not processed
if (index < 0 || index >= args.Length)
return false;
for (int i = index; i < args.Length; i++)
{
// Verify that the current flag is proper for the feature
if (ProcessInput(args, ref i))
continue;
// Add all other arguments to the generic inputs
Inputs.Add(item: args[i]);
}
return true;
}
/// <summary>
/// Verify all inputs based on the feature requirements
/// </summary>
/// <returns>True if the inputs verified correctly, false otherwise</returns>
public abstract bool VerifyInputs();
/// <summary>
/// Execute the logic associated with the feature
/// </summary>
/// <returns>True if execution was successful, false otherwise</returns>
public abstract bool Execute();
#endregion
}
}

View File

@@ -0,0 +1,57 @@
namespace SabreTools.CommandLine.Features
{
/// <summary>
/// Default help feature implementation
/// </summary>
public class Help : Feature
{
public const string DisplayName = "Help";
private static readonly string[] _defaultFlags = ["?", "h", "help"];
private const string _description = "Show this help";
private const string _longDescription = "Built-in to most of the programs is a basic help text.";
public Help()
: base(DisplayName, _defaultFlags, _description, _longDescription)
{
RequiresInputs = false;
}
public Help(string[] flags)
: base(DisplayName, flags, _description, _longDescription)
{
RequiresInputs = false;
}
/// <inheritdoc/>
public override bool ProcessArgs(string[] args, int index)
=> ProcessArgs(args, index, null);
/// <inheritdoc cref="ProcessArgs(string[], int)"/>
/// <param name="parentSet">Reference to the enclosing parent set</param>
public bool ProcessArgs(string[] args, int index, CommandSet? parentSet)
{
// If we had something else after help
if (args.Length > 1)
{
parentSet?.OutputIndividualFeature(args[1]);
return true;
}
// Otherwise, show generic help
else
{
parentSet?.OutputGenericHelp();
return true;
}
}
/// <inheritdoc/>
public override bool VerifyInputs() => true;
/// <inheritdoc/>
public override bool Execute() => true;
}
}

View File

@@ -0,0 +1,57 @@
namespace SabreTools.CommandLine.Features
{
/// <summary>
/// Default extended help feature implementation
/// </summary>
public class HelpExtended : Feature
{
public const string DisplayName = "Help (Detailed)";
private static readonly string[] _defaultFlags = ["??", "hd", "help-detailed"];
private const string _description = "Show this detailed help";
private const string _longDescription = "Display a detailed help text to the screen.";
public HelpExtended()
: base(DisplayName, _defaultFlags, _description, _longDescription)
{
RequiresInputs = false;
}
public HelpExtended(string[] flags)
: base(DisplayName, flags, _description, _longDescription)
{
RequiresInputs = false;
}
/// <inheritdoc/>
public override bool ProcessArgs(string[] args, int index)
=> ProcessArgs(args, index, null);
/// <inheritdoc cref="ProcessArgs(string[], int)"/>
/// <param name="parentSet">Reference to the enclosing parent set</param>
public bool ProcessArgs(string[] args, int index, CommandSet? parentSet)
{
// If we had something else after help
if (args.Length > 1)
{
parentSet?.OutputIndividualFeature(args[1], includeLongDescription: true);
return true;
}
// Otherwise, show generic help
else
{
parentSet?.OutputAllHelp();
return true;
}
}
/// <inheritdoc/>
public override bool VerifyInputs() => true;
/// <inheritdoc/>
public override bool Execute() => true;
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Reflection;
namespace SabreTools.CommandLine.Features
{
/// <summary>
/// Default version feature implementation
/// </summary>
public class Version : Feature
{
public const string DisplayName = "Version";
private static readonly string[] _defaultFlags = ["v", "version"];
private const string _description = "Prints version";
private const string _longDescription = "Prints current program version.";
public Version()
: base(DisplayName, _defaultFlags, _description, _longDescription)
{
RequiresInputs = false;
}
public Version(string[] flags)
: base(DisplayName, flags, _description, _longDescription)
{
RequiresInputs = false;
}
/// <inheritdoc/>
public override bool VerifyInputs() => true;
/// <inheritdoc/>
public override bool Execute()
{
Console.WriteLine($"Version: {GetVersion()}");
return true;
}
/// <summary>
/// The current toolset version to be used by all child applications
/// </summary>
private static string? GetVersion()
{
try
{
var assembly = Assembly.GetEntryAssembly();
if (assembly == null)
return null;
var assemblyVersion = Attribute.GetCustomAttribute(assembly, typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute;
return assemblyVersion?.InformationalVersion;
}
catch (Exception ex)
{
return ex.ToString();
}
}
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="bool"/>
/// </summary>
public class BooleanInput : UserInput<bool?>
{
#region Constructors
public BooleanInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public BooleanInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!bool.TryParse(args[index + 1], out bool value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!bool.TryParse(val, out bool value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,64 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a boolean user input
/// </summary>
public class FlagInput : UserInput<bool>
{
#region Constructors
public FlagInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = false;
}
public FlagInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = false;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
Value = true;
return true;
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="short"/>
/// </summary>
public class Int16Input : UserInput<short?>
{
#region Constructors
public Int16Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public Int16Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!short.TryParse(args[index + 1], out short value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!short.TryParse(val, out short value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="int"/>
/// </summary>
public class Int32Input : UserInput<int?>
{
#region Constructors
public Int32Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public Int32Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!int.TryParse(args[index + 1], out int value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!int.TryParse(val, out int value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="long"/>
/// </summary>
public class Int64Input : UserInput<long?>
{
#region Constructors
public Int64Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public Int64Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!long.TryParse(args[index + 1], out long value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!long.TryParse(val, out long value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="sbyte"/>
/// </summary>
public class Int8Input : UserInput<sbyte?>
{
#region Constructors
public Int8Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public Int8Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!sbyte.TryParse(args[index + 1], out sbyte value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!sbyte.TryParse(val, out sbyte value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,84 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a string input with a single instance allowed
/// </summary>
public class StringInput : UserInput<string?>
{
#region Constructors
public StringInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public StringInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
/// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
index++;
Value = args[index];
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
Value = val;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a string input with multiple instances allowed
/// </summary>
public class StringListInput : UserInput<List<string>>
{
#region Constructors
public StringListInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public StringListInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
index++;
Value ??= [];
Value.Add(args[index]);
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
Value ??= [];
Value.Add(val);
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="ushort"/>
/// </summary>
public class UInt16Input : UserInput<ushort?>
{
#region Constructors
public UInt16Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public UInt16Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!ushort.TryParse(args[index + 1], out ushort value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!ushort.TryParse(val, out ushort value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="uint"/>
/// </summary>
public class UInt32Input : UserInput<uint?>
{
#region Constructors
public UInt32Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public UInt32Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!uint.TryParse(args[index + 1], out uint value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!uint.TryParse(val, out uint value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="ulong"/>
/// </summary>
public class UInt64Input : UserInput<ulong?>
{
#region Constructors
public UInt64Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public UInt64Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!ulong.TryParse(args[index + 1], out ulong value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!ulong.TryParse(val, out ulong value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,96 @@
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a user input bounded to the range of <see cref="byte"/>
/// </summary>
public class UInt8Input : UserInput<byte?>
{
#region Constructors
public UInt8Input(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
Value = null;
}
public UInt8Input(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
Value = null;
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override bool ProcessInput(string[] args, ref int index)
{
// If the index is invalid
if (index < 0 || index >= args.Length)
return false;
// Get the current part
string part = args[index];
// If the current flag doesn't match, check to see if any of the subfeatures are valid
if (!ContainsFlag(part))
{
foreach (var kvp in Children)
{
if (kvp.Value.ProcessInput(args, ref index))
return true;
}
return false;
}
// Check for space-separated
if (!part.Contains("="))
{
// Ensure the value exists
if (index + 1 >= args.Length)
return false;
// If the next value is valid
if (!byte.TryParse(args[index + 1], out byte value))
return false;
index++;
Value = value;
return true;
}
// Check for equal separated
else
{
// Split the string, using the first equal sign as the separator
string[] tempSplit = part.Split('=');
string val = string.Join("=", tempSplit, 1, tempSplit.Length - 1);
// Ensure the value exists
if (string.IsNullOrEmpty(val))
return false;
// If the next value is valid
if (!byte.TryParse(val, out byte value))
return false;
Value = value;
return true;
}
}
/// <inheritdoc/>
protected override string FormatFlags()
{
var sb = new StringBuilder();
Flags.ForEach(flag => sb.Append($"{flag}=, "));
return sb.ToString().TrimEnd(' ', ',');
}
#endregion
}
}

View File

@@ -0,0 +1,874 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a single user input which may contain children
/// </summary>
public abstract class UserInput
{
#region Properties
/// <summary>
/// Display name for the feature
/// </summary>
public string Name { get; protected set; }
/// <summary>
/// Set of children associated with this input
/// </summary>
public readonly Dictionary<string, UserInput> Children = [];
#endregion
#region Fields
/// <summary>
/// Set of flags associated with the feature
/// </summary>
protected readonly List<string> Flags = [];
/// <summary>
/// Short description of the feature
/// </summary>
private readonly string _description;
/// <summary>
/// Optional long description of the feature
/// </summary>
private readonly string? _longDescription;
#endregion
#region Constructors
internal UserInput(string name, string flag, string description, string? longDescription = null)
{
Name = name;
Flags.Add(flag);
_description = description;
_longDescription = longDescription;
}
internal UserInput(string name, string[] flags, string description, string? longDescription = null)
{
Name = name;
Flags.AddRange(flags);
_description = description;
_longDescription = longDescription;
}
#endregion
#region Accessors
/// <summary>
/// Directly address a given subfeature
/// </summary>
public UserInput? this[string name]
{
get { return Children.ContainsKey(name) ? Children[name] : null; }
}
/// <summary>
/// Directly address a given subfeature
/// </summary>
public UserInput? this[UserInput subfeature]
{
get { return Children.ContainsKey(subfeature.Name) ? Children[subfeature.Name] : null; }
}
/// <summary>
/// Add a new child input
/// </summary>
public void Add(UserInput input)
=> Children[input.Name] = input;
/// <summary>
/// Returns if a flag exists for the current feature
/// </summary>
/// <param name="name">Name of the flag to check</param>
/// <returns>True if the flag was found, false otherwise</returns>
public bool ContainsFlag(string name)
=> Flags.Exists(f => f == name || name.StartsWith($"{f}="));
/// <summary>
/// Returns if the feature contains a flag that starts with the given character
/// </summary>
/// <param name="c">Character to check against</param>
/// <returns>True if the flag was found, false otherwise</returns>
public bool StartsWith(char c)
=> Flags.Exists(f => f.TrimStart('-').ToLowerInvariant()[0] == c);
#endregion
#region Children
/// <summary>
/// Get a boolean value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public bool GetBoolean(string key, bool defaultValue = false)
{
if (TryGetBoolean(key, out bool value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get an Int8 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public sbyte GetInt8(string key, sbyte defaultValue = sbyte.MinValue)
{
if (TryGetInt8(key, out sbyte value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get an Int16 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public short GetInt16(string key, short defaultValue = short.MinValue)
{
if (TryGetInt16(key, out short value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get an Int32 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public int GetInt32(string key, int defaultValue = int.MinValue)
{
if (TryGetInt32(key, out int value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get an Int64 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public long GetInt64(string key, long defaultValue = long.MinValue)
{
if (TryGetInt64(key, out long value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a string value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public string? GetString(string key, string? defaultValue = null)
{
if (TryGetString(key, out string? value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a string list value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public List<string> GetStringList(string key)
{
if (TryGetStringList(key, out var value))
return value;
return [];
}
/// <summary>
/// Get a UInt8 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public byte GetUInt8(string key, byte defaultValue = byte.MinValue)
{
if (TryGetUInt8(key, out byte value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a UInt16 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public ushort GetUInt16(string key, ushort defaultValue = ushort.MinValue)
{
if (TryGetUInt16(key, out ushort value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a UInt32 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public uint GetUInt32(string key, uint defaultValue = uint.MinValue)
{
if (TryGetUInt32(key, out uint value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a UInt64 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>The value if found, the default value otherwise</returns>
public ulong GetUInt64(string key, ulong defaultValue = ulong.MinValue)
{
if (TryGetUInt64(key, out ulong value, defaultValue))
return value;
return defaultValue;
}
/// <summary>
/// Get a boolean value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetBoolean(string key, out bool value, bool defaultValue = false)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is BooleanInput b)
{
value = b.Value ?? defaultValue;
return true;
}
else if (input is FlagInput f)
{
value = f.Value;
return true;
}
throw new ArgumentException("Feature is not a bool");
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetBoolean(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get an Int8 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetInt8(string key, out sbyte value, sbyte defaultValue = sbyte.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not Int8Input i)
throw new ArgumentException("Feature is not an sbyte");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetInt8(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get an Int16 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetInt16(string key, out short value, short defaultValue = short.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not Int16Input i)
throw new ArgumentException("Feature is not a short");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetInt16(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get an Int32 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetInt32(string key, out int value, int defaultValue = int.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not Int32Input i)
throw new ArgumentException("Feature is not an int");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetInt32(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get an Int64 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetInt64(string key, out long value, long defaultValue = long.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not Int64Input l)
throw new ArgumentException("Feature is not a long");
value = l.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetInt64(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get a string value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetString(string key, out string? value, string? defaultValue = null)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not StringInput s)
throw new ArgumentException("Feature is not a string");
value = s.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetString(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get a string list value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetStringList(string key, out List<string> value)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not StringListInput l)
throw new ArgumentException("Feature is not a list");
value = l.Value ?? [];
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetStringList(key, out value))
return true;
}
value = [];
return false;
}
/// <summary>
/// Get a UInt8 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetUInt8(string key, out byte value, byte defaultValue = byte.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not UInt8Input i)
throw new ArgumentException("Feature is not an byte");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetUInt8(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get a UInt16 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetUInt16(string key, out ushort value, ushort defaultValue = ushort.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not UInt16Input i)
throw new ArgumentException("Feature is not a ushort");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetUInt16(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get a UInt32 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetUInt32(string key, out uint value, uint defaultValue = uint.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not UInt32Input i)
throw new ArgumentException("Feature is not an uint");
value = i.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetUInt32(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
/// <summary>
/// Get a UInt64 value from a named input
/// </summary>
/// <param name="key">Input name to retrieve, if possible</param>
/// <param name="value">Value that was found, default value otherwise</param>
/// <param name="defaultValue">Optional default value if not found</param>
/// <returns>True if the value was found, false otherwise</returns>
public bool TryGetUInt64(string key, out ulong value, ulong defaultValue = ulong.MinValue)
{
// Try to check immediate children
if (Children.TryGetValue(key, out var input))
{
if (input is not UInt64Input l)
throw new ArgumentException("Feature is not a ulong");
value = l.Value ?? defaultValue;
return true;
}
// Check all children recursively
foreach (var child in Children.Values)
{
if (child.TryGetUInt64(key, out value, defaultValue))
return true;
}
value = defaultValue;
return false;
}
#endregion
#region Processing
/// <summary>
/// Validate whether a flag is valid for this feature or not
/// </summary>
/// <param name="args">Set of arguments to parse</param>
/// <param name="index">Reference index into the argument set</param>
/// <returns>True if the flag was valid, false otherwise</returns>
public abstract bool ProcessInput(string[] args, ref int index);
#endregion
#region Output
/// <summary>
/// Output this feature only
/// </summary>
/// <param name="pre">Positive number representing number of spaces to put in front of the feature</param>
/// <param name="midpoint">Positive number representing the column where the description should start</param>
/// <param name="includeLongDescription">True if the long description should be formatted and output, false otherwise</param>
public List<string> Output(int pre = 0, int midpoint = 0, bool includeLongDescription = false)
{
// Create the output list
List<string> outputList = [];
// Build the output string first
var output = new StringBuilder();
// Add the pre-space first
output.Append(CreatePadding(pre));
// Preprocess and add the flags
output.Append(FormatFlags());
// If we have a midpoint set, check to see if the string needs padding
if (midpoint > 0 && output.ToString().Length < midpoint)
output.Append(CreatePadding(midpoint - output.ToString().Length));
else
output.Append(" ");
// Append the description
output.Append(_description);
// Now append it to the list
outputList.Add(output.ToString());
// If we are outputting the long description, format it and then add it as well
if (includeLongDescription)
{
// Get the width of the console for wrapping reference
int width = (Console.WindowWidth == 0 ? 80 : Console.WindowWidth) - 1;
// Prepare the output string
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(pre + 4));
// Now split the input description and start processing
string[]? split = _longDescription?.Split(' ');
if (split == null)
return outputList;
for (int i = 0; i < split.Length; i++)
{
// If we have a newline character, reset the line and continue
if (split[i].Contains("\n"))
{
string[] subsplit = split[i].Replace("\r", string.Empty).Split('\n');
for (int j = 0; j < subsplit.Length - 1; j++)
{
// Add the next word only if the total length doesn't go above the width of the screen
if (output.ToString().Length + subsplit[j].Length < width)
{
output.Append((output.ToString().Length == pre + 4 ? string.Empty : " ") + subsplit[j]);
}
// Otherwise, we want to cache the line to output and create a new blank string
else
{
outputList.Add(output.ToString());
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(pre + 4));
output.Append((output.ToString().Length == pre + 4 ? string.Empty : " ") + subsplit[j]);
}
outputList.Add(output.ToString());
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(pre + 4));
}
output.Append(subsplit[subsplit.Length - 1]);
continue;
}
// Add the next word only if the total length doesn't go above the width of the screen
if (output.ToString().Length + split[i].Length < width)
{
output.Append((output.ToString().Length == pre + 4 ? string.Empty : " ") + split[i]);
}
// Otherwise, we want to cache the line to output and create a new blank string
else
{
outputList.Add(output.ToString());
output.Append(CreatePadding(pre + 4));
output.Append((output.ToString().Length == pre + 4 ? string.Empty : " ") + split[i]);
}
}
// Add the last created output and a blank line for clarity
outputList.Add(output.ToString());
outputList.Add(string.Empty);
}
return outputList;
}
/// <summary>
/// Output this feature and all subfeatures
/// </summary>
/// <param name="tabLevel">Level of indentation for this feature</param>
/// <param name="pre">Positive number representing number of spaces to put in front of the feature</param>
/// <param name="midpoint">Positive number representing the column where the description should start</param>
/// <param name="includeLongDescription">True if the long description should be formatted and output, false otherwise</param>
public List<string> OutputRecursive(int tabLevel, int pre = 0, int midpoint = 0, bool includeLongDescription = false)
{
// Create the output list
List<string> outputList = [];
// Build the output string first
var output = new StringBuilder();
// Normalize based on the tab level
int preAdjusted = pre;
int midpointAdjusted = midpoint;
if (tabLevel > 0)
{
preAdjusted += 4 * tabLevel;
midpointAdjusted += 4 * tabLevel;
}
// Add the pre-space first
output.Append(CreatePadding(preAdjusted));
// Preprocess and add the flags
output.Append(FormatFlags());
// If we have a midpoint set, check to see if the string needs padding
if (midpoint > 0 && output.ToString().Length < midpointAdjusted)
output.Append(CreatePadding(midpointAdjusted - output.ToString().Length));
else
output.Append(" ");
// Append the description
output.Append(_description);
// Now append it to the list
outputList.Add(output.ToString());
// If we are outputting the long description, format it and then add it as well
if (includeLongDescription)
{
// Get the width of the console for wrapping reference
int width = (Console.WindowWidth == 0 ? 80 : Console.WindowWidth) - 1;
// Prepare the output string
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(preAdjusted + 4));
// Now split the input description and start processing
string[]? split = _longDescription?.Split(' ');
if (split == null)
return outputList;
for (int i = 0; i < split.Length; i++)
{
// If we have a newline character, reset the line and continue
if (split[i].Contains("\n"))
{
string[] subsplit = split[i].Replace("\r", string.Empty).Split('\n');
for (int j = 0; j < subsplit.Length - 1; j++)
{
// Add the next word only if the total length doesn't go above the width of the screen
if (output.ToString().Length + subsplit[j].Length < width)
{
output.Append((output.ToString().Length == preAdjusted + 4 ? string.Empty : " ") + subsplit[j]);
}
// Otherwise, we want to cache the line to output and create a new blank string
else
{
outputList.Add(output.ToString());
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(preAdjusted + 4));
output.Append((output.ToString().Length == preAdjusted + 4 ? string.Empty : " ") + subsplit[j]);
}
outputList.Add(output.ToString());
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(preAdjusted + 4));
}
output.Append(subsplit[subsplit.Length - 1]);
continue;
}
// Add the next word only if the total length doesn't go above the width of the screen
if (output.ToString().Length + split[i].Length < width)
{
output.Append((output.ToString().Length == preAdjusted + 4 ? string.Empty : " ") + split[i]);
}
// Otherwise, we want to cache the line to output and create a new blank string
else
{
outputList.Add(output.ToString());
#if NET20 || NET35
output = new();
#else
output.Clear();
#endif
output.Append(CreatePadding(preAdjusted + 4));
output.Append((output.ToString().Length == preAdjusted + 4 ? string.Empty : " ") + split[i]);
}
}
// Add the last created output and a blank line for clarity
outputList.Add(output.ToString());
outputList.Add(string.Empty);
}
// Now let's append all subfeatures
foreach (string feature in Children.Keys)
{
outputList.AddRange(Children[feature]!.OutputRecursive(tabLevel + 1, pre, midpoint, includeLongDescription));
}
return outputList;
}
/// <summary>
/// Pre-format the flags for output
/// </summary>
protected abstract string FormatFlags();
/// <summary>
/// Create a padding space based on the given length
/// </summary>
/// <param name="spaces">Number of padding spaces to add</param>
/// <returns>String with requested number of blank spaces</returns>
private static string CreatePadding(int spaces) => string.Empty.PadRight(spaces);
#endregion
}
}

View File

@@ -0,0 +1,37 @@
namespace SabreTools.CommandLine.Inputs
{
/// <summary>
/// Represents a single user input which may contain children
/// </summary>
public abstract class UserInput<T> : UserInput
{
/// <summary>
/// Typed value provided by the user
/// </summary>
public T? Value { get; protected set; }
#region Constructors
internal UserInput(string name, string flag, string description, string? longDescription = null)
: base(name, flag, description, longDescription)
{
}
internal UserInput(string name, string[] flags, string description, string? longDescription = null)
: base(name, flags, description, longDescription)
{
}
#endregion
#region Instance Methods
/// <inheritdoc/>
public override abstract bool ProcessInput(string[] args, ref int index);
/// <inheritdoc/>
protected override abstract string FormatFlags();
#endregion
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Assembly Properties -->
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<IncludeSymbols>true</IncludeSymbols>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Version>1.2.1</Version>
<!-- Package Properties -->
<Authors>Matt Nadareski</Authors>
<Copyright>Copyright (c)2016-2025 Matt Nadareski</Copyright>
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
<RepositoryUrl>https://github.com/SabreTools/SabreTools</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="SabreTools.CommandLine.Test" />
</ItemGroup>
</Project>

36
publish-nix.sh Executable file
View File

@@ -0,0 +1,36 @@
#! /bin/bash
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
NO_BUILD=false
while getopts "b" OPTION
do
case $OPTION in
b)
NO_BUILD=true
;;
*)
echo "Invalid option provided"
exit 1
;;
esac
done
# Set the current directory as a variable
BUILD_FOLDER=$PWD
# Only build if requested
if [ $NO_BUILD = false ]
then
# Restore Nuget packages for all builds
echo "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.CommandLine/SabreTools.CommandLine.csproj --output $BUILD_FOLDER
fi

26
publish-win.ps1 Normal file
View File

@@ -0,0 +1,26 @@
# This batch file assumes the following:
# - .NET 9.0 (or newer) SDK is installed and in PATH
#
# If any of these are not satisfied, the operation may fail
# in an unpredictable way and result in an incomplete output.
# Optional parameters
param(
[Parameter(Mandatory = $false)]
[Alias("NoBuild")]
[switch]$NO_BUILD
)
# Set the current directory as a variable
$BUILD_FOLDER = $PSScriptRoot
# Only build if requested
if (!$NO_BUILD.IsPresent)
{
# Restore Nuget packages for all builds
Write-Host "Restoring Nuget packages"
dotnet restore
# Create Nuget Package
dotnet pack SabreTools.CommandLine\SabreTools.CommandLine.csproj --output $BUILD_FOLDER
}