hedzr / Cmdr.Core

Useful POSIX command line arguments parser for .Net. Hierarchical-configuration Store for app.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cmdr.Core

CircleCI Nuget (with prereleases)

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.

NuGet

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.

Features

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

More

  • 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
  • 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 and conf.Version are originally.
      • --tree: list all commands and sub-commands.
      • --config <location>: specify the location of the root config file.
      • version command available.
    • Verbose & Debug: —verbose/-v, —debug/-D, —quiet/-q
  • 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).
  • 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 and conf.d sub-directory.

    • /usr/local/etc/<appname>/<appname>.yml and conf.d sub-directory.

    • $HOME/.config/<appname>/<appname>.yml and conf.d sub-directory.

    • $HOME/.<appname>/<appname>.yml and conf.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 concrete Command.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&lt;string, ICommand&gt; dataset, string token)
      • bool OnSuggestingForFlag(object worker, Dictionary&lt;string, IFlag&gt; dataset, string token)
      • ...
    • More...
  • Unhandled Exception cmdr handled AppDomain.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.

Option Store - Hierarchical Configurations Store

Standard primitive types and non-primitive types.

Get(), GetAs<T>()

Set<T>(), SetWithoutPrefix<T>()

Delete()

HasKeys(), HasKeysWithoutPrefix()

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);

FindBy()

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
  }
}

Walk()

GetAsMap()

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);
  }

CMDR EnvVars

CMDR_DUMP

enable Store entries dumping at the end of help screen.

CMDR_DUMP_NO_STORE, CMDR_DUMP_NO_HIT

To prevent the store dump, or hit options dump.

CMDR_DEBUG

= Worker.EnableCmdrLogDebug

allows the display output in defaultOnSet.

CMDR_TRACE

= Worker.EnableCmdrLogTrace

allows the worker logDebug().

CMDR_VERBOSE

allows more logging output.

Getting Start

Fluent API

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;
    }
}

Declarative API

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; }
        }
    }
}

Logger

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:

  1. Add HzNS.Cmdr.Logger.Serilog at first:
dotnet add package HzNS.Cmdr.Logger.Serilog --version 1.0.6
  1. 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);

ACKNOWNLEDGES

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).

Thanks to JODL

JODL (JetBrains OpenSource Development License) is good:

rider jetbrains

LICENSE

MIT