• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

Cysharp/ConsoleAppFramework: Micro-framework for console applications to buildin ...

原作者: [db:作者] 来自: 网络 收藏 邀请

开源软件名称:

Cysharp/ConsoleAppFramework

开源软件地址:

https://github.com/Cysharp/ConsoleAppFramework

开源编程语言:

C# 99.9%

开源软件介绍:

ConsoleAppFramework

GitHub Actions Releases

ConsoleAppFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multi batch application. You can create full feature of command line tool on only one-line.

image

This simplicity is by C# 10.0 and .NET 6 new features, similar as ASP.NET Core 6.0 Minimal APIs.

Most minimal API is one-line(with top-level-statements, global-usings).

ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));

Of course, ConsoleAppFramework has extensibility.

// Regsiter two commands(use short-name, argument)
// hello -m
// sum [x] [y]
var app = ConsoleApp.Create(args);
app.AddCommand("hello", ([Option("m", "Message to display.")] string message) => Console.WriteLine($"Hello {message}"));
app.AddCommand("sum", ([Option(0)] int x, [Option(1)] int y) => Console.WriteLine(x + y));
app.Run();

You can register public method as command. This provides a simple way to registering multiple commands.

// AddCommands register as command.
// echo --msg --repeat(default = 3)
// sum [x] [y]
var app = ConsoleApp.Create(args);
app.AddCommands<Foo>();
app.Run();

public class Foo : ConsoleAppBase
{
    public void Echo(string msg, int repeat = 3)
    {
        for (var i = 0; i < repeat; i++)
        {
            Console.WriteLine(msg);
        }
    }

    public void Sum([Option(0)]int x, [Option(1)]int y)
    {
        Console.WriteLine((x + y).ToString());
    }
}

If you have many commands, you can define class separetely and use AddAllCommandType to register all commands one-line.

// Register `Foo` and `Bar` as SubCommands(You can also use AddSubCommands<T> to register manually).
// foo echo --msg
// foo sum [x] [y]
// bar hello2
var app = ConsoleApp.Create(args);
app.AddAllCommandType();
app.Run();

public class Foo : ConsoleAppBase
{
    public void Echo(string msg)
    {
        Console.WriteLine(msg);
    }

    public void Sum([Option(0)]int x, [Option(1)]int y)
    {
        Console.WriteLine((x + y).ToString());
    }
}

public class Bar : ConsoleAppBase
{
    public void Hello2()
    {
        Console.WriteLine("H E L L O");
    }
}

ConsoleAppFramework is built on .NET Generic Host, you can use configuration, logging, DI, lifetime management by Microsoft.Extensions packages. ConsoleAppFramework do parameter binding from string args, routing many commands, dotnet style help builder, etc.

image

Here is the full-sample of power of ConsoleAppFramework.

// You can use full feature of Generic Host(same as ASP.NET Core).

var builder = ConsoleApp.CreateBuilder(args);
builder.ConfigureServices((ctx,services) =>
{
    // Register EntityFramework database context
    services.AddDbContext<MyDbContext>();

    // Register appconfig.json to IOption<MyConfig>
    services.Configure<MyConfig>(ctx.Configuration);

    // Using Cysharp/ZLogger for logging to file
    services.AddLogging(logging =>
    {
        logging.AddZLoggerFile("log.txt");
    });
});

var app = builder.Build();

// setup many command, async, short-name/description option, subcommand, DI
app.AddCommand("calc-sum", (int x, int y) => Console.WriteLine(x + y));
app.AddCommand("sleep", async ([Option("t", "seconds of sleep time.")] int time) =>
{
    await Task.Delay(TimeSpan.FromSeconds(time));
});
app.AddSubCommand("verb", "childverb", () => Console.WriteLine("called via 'verb childverb'"));

// You can insert all public methods as sub command => db select / db insert
// or AddCommand<T>() all public methods as command => select / insert
app.AddSubCommands<DatabaseApp>();

// some argument from DI.
app.AddRootCommand((ConsoleAppContext ctx, IOptions<MyConfig> config, string name) => { });

app.Run();

// ----

[Command("db")]
public class DatabaseApp : ConsoleAppBase, IAsyncDisposable
{
    readonly ILogger<DatabaseApp> logger;
    readonly MyDbContext dbContext;
    readonly IOptions<MyConfig> config;

    // you can get DI parameters.
    public DatabaseApp(ILogger<DatabaseApp> logger,IOptions<MyConfig> config, MyDbContext dbContext)
    {
        this.logger = logger;
        this.dbContext = dbContext;
        this.config = config;
    }

    [Command("select")]
    public async Task QueryAsync(int id)
    {
        // select * from...
    }

    // also allow defaultValue.
    [Command("insert")]
    public async Task InsertAsync(string value, int id = 0)
    {
        // insert into...
    }

    // support cleanup(IDisposable/IAsyncDisposable)
    public async ValueTask DisposeAsync()
    {
        await dbContext.DisposeAsync();
    }
}

public class MyConfig
{
    public string FooValue { get; set; } = default!;
    public string BarValue { get; set; } = default!;
}

ConsoleAppFramework can create easily to many command application. Also enable to use GenericHost configuration is best way to share configuration/workflow when creating batch application for other .NET web app. If tool is for CI, git pull and run by dotnet run -- [Command] [Option] is very helpful.

dotnet's standard CommandLine api - System.CommandLine is low level, require many boilerplate codes. ConsoleAppFramework is like ASP.NET Core in CLI Applications, no needs boilerplate. However, with the power of Generic Host, it is simple and easy, but much more powerful.

Table of Contents

Getting Started

NuGet: ConsoleAppFramework

Install-Package ConsoleAppFramework

If you are using .NET 6, automatically enabled implicit global using ConsoleAppFramework;. So you can write one line code.

ConsoleApp.Run(args, (string name) => Console.WriteLine($"Hello {name}"));

You can execute command like sampletool --name "foo".

The Option parser is no longer needed. You can also use the OptionAttribute to describe the parameter and set short-name.

ConsoleApp.Run(args, ([Option("n", "name of send user.")] string name) => Console.WriteLine($"Hello {name}"));
Usage: sampletool [options...]

Options:
  -n, --name <String>    name of user. (Required)

Commands:
  help       Display help.
  version    Display version.

Method parameter will be required parameter, optional parameter will be oprional parameter with default value. Also support boolean flag, if parameter is bool, in default it will be optional parameter and with --foo set true to parameter.

// lambda expression does not support default value so require to use local function
static void Hello([Option("m")]string message, [Option("e")] bool end, [Option("r")] int repeat = 3)
{
    for (int i = 0; i < repeat; i++)
    {
        Console.WriteLine(message);
    }
    if (end)
    {
        Console.WriteLine("END");
    }
}

ConsoleApp.Run(args, Hello);
Options:
  -m, --message <String>     (Required)
  -e, --end                  (Optional)
  -r, --repeat <Int32>       (Default: 3)

help command (or no argument to pass) and version command is enabled in default(You can disable this in options or can override by add same name of command). Also enables command --help option. This help format is similar as dotnet command, version command shows AssemblyInformationalVersion or AssemblylVersion.

> sampletool help
Usage: sampletool [options...]

Options:
  -n, --name <String>     name of user. (Required)
  -r, --repeat <Int32>    repeat count. (Default: 3)

Commands:
  help          Display help.
  version       Display version.
> sampletool version
1.0.0

You can use Run<T> or AddCommands<T> to add multi commands easily.

ConsoleApp.Run<MyCommands>(args);

// require to inherit ConsoleAppBase
public class MyCommands : ConsoleAppBase
{
    //  You can receive DI services in constructor.

    // All public methods is registred.

    // Using [RootCommand] attribute will be root-command
    [RootCommand]
    public void Hello(
        [Option("n", "name of send user.")] string name,
        [Option("r", "repeat count.")] int repeat = 3)
    {
        for (int i = 0; i < repeat; i++)
        {
            Console.WriteLine($"Hello My ConsoleApp from {name}");
        }
    }

    // [Option(int)] describes that parameter is passed by index
    [Command("escape")]
    public void UrlEscape([Option(0)] string input)
    {
        Console.WriteLine(Uri.EscapeDataString(input));
    }

    // define async method returns Task
    [Command("timer")]
    public async Task Timer([Option(0)] uint waitSeconds)
    {
        Console.WriteLine(waitSeconds + " seconds");
        while (waitSeconds != 0)
        {
            // ConsoleAppFramework does not stop immediately on terminate command(Ctrl+C)
            // for allows gracefully shutdown(keeping safe cleanup)
            // so you should pass Context.CancellationToken to async method.
            // If not, abort timeout by HostOptions.ShutdownTimeout(default is 00:00:05).
            await Task.Delay(TimeSpan.FromSeconds(1), Context.CancellationToken);
            waitSeconds--;
            Console.WriteLine(waitSeconds + " seconds");
        }
    }
}

You can call like

sampletool -n "foo" -r 3
sampletool escape http://foo.bar/
sampletool timer 10

This is recommended way to register multi commands.

If you omit [Command] attribute, command and option name is used by there name and convert to kebab-case in default.

// Command is url-escape
// Option  is --input-file
public void UrlEscape(string inputFile)
{
}

This converting behaviour can configure by ConsoleAppOptions.NameConverter.

ConsoleApp / ConsoleAppBuilder

ConsoleApp is an entrypoint of creating ConsoleAppFramework app. It has three APIs, Create, CreateBuilder, CreateFromHostBuilder and Run.

// Create is shorthand of CraeteBuilder(args).Build();
var app = ConsoleApp.Create(args);

// Builder returns IHost so you can configure application hosting option.
var app = ConsoleApp.CreateBuilder(args)
    .ConfigureServices(services =>
    {
    })
    .Build();

// Run is shorthand of Create(args).AddRootCommand(rootCommand).Run();
// If you want to create simple app, this API is most fast.
ConsoleApp.Run(args, /* lambda expression */);

// Run<T> is shorthand of Create(args).AddCommands<T>().Run();
// AddCommands<T> is recommend option to register many commands.
ConsoleApp.Run<MyCommands>(args);

When calling Create/CreateBuilder/CreateFromHostBuilder, also configure ConsoleAppOptions. Full option details, see ConsoleAppOptions section.

var app = ConsoleApp.Create(args, options =>
{
    options.ShowDefaultCommand = false;
    options.NameConverter = x => x.ToLower();
});

Advanced API of ConsoleApp, CreateFromHostBuilder creates ConsoleApp from IHostBuilder.

// Setup services outside of ConsoleAppFramework.
var hostBuilder = Host.CreateDefaultBuilder()
    .ConfigureServices();
    
var app = ConsoleApp.CreateFromHostBuilder(hostBuilder);

ConsoleAppBuilder itself is IHostBuilder so you can use any configuration methods like ConfigureServices, ConfigureLogging, etc. If method chain is not returns ConsoleAppBuilder(for example, using external lib's extension methods), can not get ConsoleApp directly. In that case, use BuildAsConsoleApp() instead of Build().

ConsoleApp exposes some utility properties.

  • IHost Host
  • ILogger<ConsoleApp> Logger
  • IServiceProvider Services
  • IConfiguration Configuration
  • IHostEnvironment Environment
  • IHostApplicationLifetime Lifetime

Run() and RunAsync(CancellationToken) to finally invoke application. Run is shorthand of RunAsync().GetAwaiter().GetResult() so receives same result of await RunAsync(). On Entrypoint, there is not much need to do await RunAsync(). Therefore, it is usually a good to choose Run().

Delegate convention

AddCommand accepts Delegate in argument. In C# 10.0 allows naturaly syntax of lambda expressions.

app.AddCommand("no-argument", () => { });
app.AddCommand("any-arguments",  (int x, string y, TimeSpan z) => { });
app.AddCommand("instance", new MyClass().Cmd);
app.AddCommand("async", async () => { });
app.AddCommand("attribute", ([Option("msg")]string message) => { });

static void Hello1() { }
app.AddCommand("local-static", Hello1);

void Hello2() { }
app.AddCommand("local-method", Hello2);

async Task Async() { }
app.AddCommand("async-method", Async);

void OptionalParameter(int x = 10, int y = 20) { }
app.AddCommand("optional", OptionalParameter);

public class MyClass
{
    public void Cmd()
    {
        Console.WriteLine("OK");
    }
}

lambda expressions can not use optional parameter so if you want to need it, using local/static functions.

Delegate(both lambda and method) allows to receive ConsoleAppContext or any your DI types. DI types is ignored as parameter.

// option is --param1, --param2
app.AddCommand("di", (ConsoleAppContext ctx, ILogger logger, int param1, int param2) => { });

AddCommand

AddRootCommand

RootCommand means default(no command name) command of application. ConsoleApp.Run(Delegate) uses root command.

AddCommand / AddCommands<T>

AddCommand requires first argument as command-name. AddCommands<T> allows to register many command via ConsoleAppBase ConsoleAppBase has Context, it has executing information and CancellationToken.

// Commands:
//   hello
//   world
app.AddCommands<MyCommands>();
app.Run();

// Inherit ConsoleAPpBase
public class MyCommands : ConsoleAppBase, IDisposable
{
    readonly ILogger<MyCommands> logger;

    //  You can receive DI services in constructor.
    public MyCommands(ILogger<MyCommands> logger)
    {
        this.logger = logger;
    }

    // All public methods is registred.

    // Using [RootCommand] attribute will be root-command
    [RootCommand]
    public void Hello() 
    {
        // Context has any useful information.
        Console.WriteLine(this.Context.Timestamp);
    }

    public async Task World() 
    {
        await Task.Delay(1000, this.Context.CancellationToken);
    }

    // If implements IDisposable, called for cleanup
    public void Dispose()
    {
    }
}

AddSubCommand / AddSubCommands<T>

AddSubCommand(string parentCommandName, string commandName, Delegate command) registers nested command.

// Commands:
//   foo bar1
//   foo bar2
//   foo bar3
app.AddSubCommand("foo", "bar1", () => { });
app.AddSubCommand("foo", "bar2", () => { });
app.AddSubCommand("foo", "bar3", () => { });

AddSubCommands<T> is similar as AddCommands<T> but used type-name(or [Command] name) as parentCommandName.

// Commands:
//   my-commands hello
//   my-commands world
app.AddSubCommands<MyCommands>();

AddAllCommandType

AddAllCommandType searches all ConsoleAppBase type in assembly and register by AddSubCommands<T>.


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
elastic/eui: Elastic UI Framework 发布时间:2022-06-07
下一篇:
framework7io/framework7-cli: Framework7 command line utility发布时间:2022-06-07
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap