Useful POSIX command line arguments parser for dotNet. Hierarchical configurations Store for app.
Supported:
- dotNet 6 (since v1.3+)
- dotNet Core 3.1 (since v1.1+)
- dotNet Standard 2.1+ (since v1.1+)
dotNet 4.8+ [?] (NOT SURE)
NOTED .NET 5 has been ignored.
PM> Install-Package HzNS.Cmdr.Core -Version 1.0.29
# Or CLI
$ dotnet add package HzNS.Cmdr.Core --version 1.0.29
Please replace 1.0.29
with the newest version (stable or pre-release), see the nuget badge icon.
Cmdr.Core
has rich features:
- POSIX Compatible (Unix getopt(3))
- IEEE Standard Compartiblities
- builds multi-level command and sub-commands
- builds short, long and alias options with kinds of data types
- defines commands and options via fluent api style
- full featured
Options Store
for hosting any application configurations- watchable external config file and child directory
conf.d
. - watchable option value merging event: while option value modified in external config file, it'll be loaded and merged automatically.
- watchable option value modifying event: while option value modified (from config file, or programmatically)
- connectable with external configuration-center
- watchable external config file and child directory
-
Unix getopt(3) representation but without its programmatic interface.
- Options with short names (
-h
) - Options with long names (
--help
) - Options with aliases (
--helpme
,--usage
,--info
) - Options with and without arguments (bool v.s. other type)
- Options with optional arguments and default values
- Multiple option groups each containing a set of options
- Supports the compat short options
-aux
==-a -u -x
,-vvv
==-v -v -v
(HitCount=3) - Supports namespaces for (nested) option groups see also: option store and hierarchical data
- Options with short names (
-
Supports for
-D+
,-D-
to enable/disable a bool option. -
Supports for PassThrough by
--
. (Passing remaining command line arguments after -- (optional)) -
Automatic help screen generation (Generates and prints well-formatted help message)
-
Predefined commands and flags:
- Help:
-h
,-?
,--help
,--info
,--usage
,--helpme
, ... - Version & Build Info:
--version
/--ver
/-V
,--build-info
/-#
- Simulating version at runtime with
—version-sim 1.9.1
- generally,
conf.AppName
andconf.Version
are originally. --tree
: list all commands and sub-commands.--config <location>
: specify the location of the root config file.version
command available.
- Simulating version at runtime with
- Verbose & Debug:
—verbose
/-v
,—debug
/-D
,—quiet
/-q
- Help:
-
Sortable commands and options/flags: sorted by alphabetic order or not (
worker.SortByAlphabeticAscending
). -
Grouped commands and options/flags.
Group Title may have a non-displayable prefix for sorting, separated by '.'.
Sortable group name can be
[0-9A-Za-z]+\..+
format typically, or any string tailed '.', eg:1001.c++
,1100.golang
,1200.java
, …;abcd.c++
,b999.golang
,zzzz.java
, …;
-
Supports for unlimited multi-level sub-commands.
-
Overrides by environment variables.
priority level:
defaultValue -> config-file -> env-var -> command-line opts
-
Option Store
- Unify option value extraction. -
Walkable
- Customizable
Painter
interface to loop each command and flag. - Walks on all commands with
Walk(from, commandWalker, flagWalker)
.
- Customizable
-
Supports
-I/usr/include -I=/usr/include
-I /usr/include -I:/usr
option argument specifications Automatically allows those formats (applied to long option too):-I file
,-Ifile
, and-I=files
-I 'file'
,-I'file'
, and-I='files'
-I "file"
,-I"file"
, and-I="files"
-
Supports for PassThrough by
--
. (Passing remaining command line arguments after -- (optional)) -
Predefined external config file locations:
-
/etc/<appname>/<appname>.yml
andconf.d
sub-directory. -
/usr/local/etc/<appname>/<appname>.yml
andconf.d
sub-directory. -
$HOME/.config/<appname>/<appname>.yml
andconf.d
sub-directory. -
$HOME/.<appname>/<appname>.yml
andconf.d
sub-directory. -
the predefined locations are:
predefinedLocations: []string{ "./ci/etc/%s/%s.yml", // for developer "/etc/%s/%s.yml", // regular location: /etc/$APPNAME/$APPNAME.yml "/usr/local/etc/%s/%s.yml", // regular macOS HomeBrew location "$HOME/.config/%s/%s.yml", // per user: $HOME/.config/$APPNAME/$APPNAME.yml "$HOME/.%s/%s.yml", // ext location per user "$THIS/%s.yml", // executable's directory "%s.yml", // current directory },
-
Watch
conf.d
directory, the name is customizable (worker.). -
RegisterExternalConfigurationsLoader(loader, ...)
-
-
Handlers
- Global Handlers:
RootCommand.OnPre/Post/Action(), OnSet()
will be triggered before/after the concreteCommand.OnPre/Post/Action()/OnSet()
- Command Actions:
Command.OnPreAction/OnAction/OnPostAction(), OnSet
- Flag Actions:
Flag.OnPreAction/OnAction/OnPostAction(), OnSet
- Parsing Events:
bool OnDuplicatedCommandChar(worker, cmd, isShort, matchingString)
bool OnDuplicatedFlagChar(worker, cmd, flag, isShort, matchingString)
bool OnCommandCannotMatched(ICommand parsedCommand, string matchingArg)
bool OnCommandCannotMatched(ICommand parsingCommand, string fragment, bool isShort, string matchingArg)
bool OnSuggestingForCommand(object worker, Dictionary<string, ICommand> dataset, string token)
bool OnSuggestingForFlag(object worker, Dictionary<string, IFlag> dataset, string token)
- ...
- More...
- Global Handlers:
-
Unhandled Exception
cmdr
handledAppDomain.CurrentDomain.UnhandledException
for better display. But you can override it always:static int Main(string[] args) { AppDomain.CurrentDomain.UnhandledException+=(sender,e)=>{}; Cmdr.NewWorker(...).Run(); }
-
Smart suggestions for wrong command and flags
based on Jaro-Winkler distance.
Standard primitive types and non-primitive types.
var exists = Cmdr.Instance.Store.HasKeys("tags.mode.s1.s2");
var exists = Cmdr.Instance.Store.HasKeys(new string[] { "tags", "mode", "s1", "s2" });
var exists = Cmdr.Instance.Store.HasKeysWithoutPrefix(new string[] { "app", "tags", "mode", "s1", "s2" });
Console.WriteLine(Cmdr.Instance.Store.Prefix);
var (slot, valueKey) = Cmdr.Instance.Store.FindBy("tags.mode.s1.s2");
if (slot != null){
if (string.IsNullOrWhiteSpace(valueKey)) {
// a child slot node matched
} else {
// a value entry matched, inside a slot node
}
}
return a SlotEntries
map so that you can yaml it:
// NOTE: Cmdr.Instance.Store == worker.OptionsStore
var map = worker.OptionsStore.GetAsMap("tags.mode");
// worker.log.Information("tag.mode => {OptionsMap}", map);
{
var serializer = new SerializerBuilder().Build();
var yaml = serializer.Serialize(map);
Console.WriteLine(yaml);
}
enable Store entries dumping at the end of help screen.
To prevent the store dump, or hit options dump.
= Worker.EnableCmdrLogDebug
allows the display output in defaultOnSet
.
= Worker.EnableCmdrLogTrace
allows the worker logDebug()
.
allows more logging output.
Basically, the Main program looks like:
static int Main(string[] args) =>
Cmdr.NewWorker(RootCommand.New(
new AppInfo(), // your app information, desc, ...
buildRootCmd(), // to attach the sub-commands and options to the RootCommand
workerOpts, // to customize the Cmdr Worker
))
.Run(args, postRun);
Your first app with Cmdr.Core
could be:
Expand to source codes
namespace Simple
{
class Program
{
static int Main(string[] args) => Cmdr.NewWorker(
#region RootCmd Definitions
RootCommand.New(
new AppInfo
{
AppName = "tag-tool",
Author = "hedzr",
Copyright = "Copyright © Hedzr Studio, 2020. All Rights Reserved.",
},
(root) =>
{
root.Description = "description here";
root.DescriptionLong = "long description here";
root.Examples = "examples here";
// for "dz"
_a = 0;
root.AddCommand(new Command
{
Long = "dz", Short = "dz", Description = "test divide by zero",
Action = (worker, opt, remainArgs) => { Console.WriteLine($"{B / _a}"); },
})
.AddCommand(new Command {Short = "t", Long = "tags", Description = "tags operations"}
.AddCommand(new TagsAddCmd())
.AddCommand(new TagsRemoveCmd())
// .AddCommand(new TagsAddCmd { }) // for dup-test
.AddCommand(new TagsListCmd())
.AddCommand(new TagsModifyCmd())
.AddCommand(new TagsModeCmd())
.AddCommand(new TagsToggleCmd())
.AddFlag(new Flag<string>
{
DefaultValue = "consul.ops.local",
Long = "addr", Short = "a", Aliases = new[] {"address", "host"},
Description = "Consul IP/Host and/or Port: HOST[:PORT] (No leading 'http(s)://')",
PlaceHolder = "HOST[:PORT]",
Group = "Consul",
})
.AddFlag(new Flag<string>
{
DefaultValue = "",
Long = "cacert", Short = "", Aliases = new string[] {"ca-cert"},
Description = "Consul Client CA cert)",
PlaceHolder = "FILE",
Group = "Consul",
})
.AddFlag(new Flag<string>
{
DefaultValue = "",
Long = "cert", Short = "", Aliases = new string[] { },
Description = "Consul Client Cert)",
PlaceHolder = "FILE",
Group = "Consul",
})
.AddFlag(new Flag<bool>
{
DefaultValue = false,
Long = "insecure", Short = "k", Aliases = new string[] { },
Description = "Ignore TLS host verification",
Group = "Consul",
})
);
root.OnSet = (worker, flag, oldValue, newValue) =>
{
if (worker.OptionStore.GetAs<bool>("quiet")) return;
if (Cmdr.Instance.Store.GetAs<bool>("verbose") &&
flag.Root?.FindFlag("verbose")?.HitCount > 1)
Console.WriteLine($"--> [{Cmdr.Instance.Store.GetAs<bool>("quiet")}][root.onSet] {flag} set: {oldValue?.ToStringEx()} -> {newValue?.ToStringEx()}");
};
}
), // <- RootCmd Definitions
#endregion
#region Options for Worker
(w) =>
{
//
// w.UseSerilog((configuration) => configuration.WriteTo.Console().CreateLogger())
//
// w.EnableCmdrGreedyLongFlag = true;
// w.EnableDuplicatedCharThrows = true;
// w.EnableEmptyLongFieldThrows = true;
w.RegisterExternalConfigurationsLoader(ExternalConfigLoader);
w.OnDuplicatedCommandChar = (worker, command, isShort, matchingArg) => false;
w.OnDuplicatedFlagChar = (worker, command, flag, isShort, matchingArg) => false;
w.OnCommandCannotMatched = (parsedCommand, matchingArg) => false;
w.OnFlagCannotMatched = (parsingCommand, fragment, isShort, matchingArg) => false;
w.OnSuggestingForCommand = (worker, dataset, token) => false;
w.OnSuggestingForFlag = (worker, dataset, token) => false;
}
#endregion
)
.Run(args, () =>
{
// Postrun here
// Wait for the user to quit the program.
// Console.WriteLine($" AssemblyVersion: {VersionUtil.AssemblyVersion}");
// Console.WriteLine($" FileVersion: {VersionUtil.FileVersion}");
// Console.WriteLine($" InformationalVersion: {VersionUtil.InformationalVersion}");
// Console.WriteLine($"AssemblyProductAttribute: {VersionUtil.AssemblyProductAttribute}");
// Console.WriteLine($" FileProductVersion: {VersionUtil.FileVersionInfo.ProductVersion}");
// Console.WriteLine();
// Console.WriteLine("Press 'q' to quit the sample.");
// while (Console.Read() != 'q')
// {
// //
// }
return 0;
});
private static void ExternalConfigLoader(IBaseWorker w, IRootCommand root)
{
// throw new NotImplementedException();
}
private static int _a = 9;
private const int B = 10;
}
}
Since v1.0.139, we added the declarative API for compatibility with some others command-line argument parser libraries.
A sample at: SimpleAttrs.
The codes might be:
Expand to source codes
class Program
{
static int Main(string[] args) => Cmdr.Compile<SampleAttrApp>(args);
}
[CmdrAppInfo(appName: "SimpleAttrs", author: "hedzr", copyright: "copyright")]
public class SampleAttrApp
{
[CmdrOption(longName: "count", shortName: "c", "cnt")]
[CmdrDescriptions(description: "a counter", descriptionLong: "", examples: "")]
[CmdrRange(min: 0, max: 10)]
[CmdrRequired]
public int Count { get; }
[CmdrCommand(longName: "tags", shortName: "t")]
[CmdrGroup(@group: "")]
[CmdrDescriptions(description: "tags operations")]
public class TagsCmd
{
[CmdrCommand(longName: "mode", shortName: "m")]
[CmdrDescriptions(description: "set tags' mode", descriptionLong: "", examples: "")]
public class ModeCmd
{
[CmdrAction]
public void Execute(IBaseWorker w, IBaseOpt cmd, IEnumerable<string> remainArgs)
{
Console.WriteLine($"Hit: {cmd}, Remains: {remainArgs}. Count: {Cmdr.Instance.Store.GetAs<int>(key: "count")}");
}
[CmdrOption(longName: "count2", shortName: "c2", "cnt2")]
[CmdrDescriptions(description: "a counter", descriptionLong: "", examples: "", placeHolder: "COUNT")]
public int Count { get; }
[CmdrOption(longName: "ok", shortName: "ok")]
[CmdrDescriptions(description: "boolean option", descriptionLong: "", examples: "")]
[CmdrHidden]
public bool OK { get; }
[CmdrOption(longName: "addr", shortName: "a", "address")]
[CmdrDescriptions(description: "string option", descriptionLong: "", examples: "", placeHolder: "HOST[:PORT]")]
public string Address { get; }
}
}
}
The external logger has been removed from Cmdr.Core
.
But you can always enable one or customize yours. In the HzNS.Cmdr.Logger.Serilog
package/project, we've given an implements and it's simple to use:
- Add
HzNS.Cmdr.Logger.Serilog
at first:
dotnet add package HzNS.Cmdr.Logger.Serilog --version 1.0.6
- Modify the program entry:
Cmdr.NewWorker(RootCommand.New(new AppInfo {AppName = "mdxTool", AppVersion = "1.0.0"}, (root) =>
{
root.AddCommand(new Command {Short = "t", Long = "tags", Description = "tags operations"});
}), // <- RootCmd
// Options ->
(w) =>
{
w.SetLogger(HzNS.Cmdr.Logger.Serilog.SerilogBuilder.Build((logger) =>
{
// These following flags will be loaded from envvars such as '$CMDR_TRACE', ...
// logger.EnableCmdrLogInfo = false;
// logger.EnableCmdrLogTrace = false;
}));
// w.EnableDuplicatedCharThrows = true;
})
.Run(args);
I have to copy some codes from Colorify for the dotnetcore devenv.
There's some reason. But I will be pleasure to re-integrate the original or put an issue later (soon).
JODL (JetBrains OpenSource Development License) is good:
MIT