安利一款非常好用的命令行参数库:McMaster.Extensions.CommandLineUtils
命令行参数解析想必是每一个命令行程序都难以避开的工程。这工程可小可大,但每次都写始终是在浪费时间。而且,不同人实现也千差万别,使得不同的命令行程序命令参数传入的体验总有差异。
于是安利一款命令行工具库——McMaster.Extensions.CommandLineUtils
,它符合当下各大主流命令行工具的参数体验;而且,代码非常简洁。
更新:
如果你之前阅读过我这篇博客,可能知道我之前推荐的是 Microsoft.Extensions.CommandlineUtils
,是微软出品;不过微软官方已经在 GitHub 上将此命令行项目重定向到了 aspnet/Common
,原有的单独的命令行不复存在。
McMaster.Extensions.CommandLineUtils
是微软官方指定的命令行仓库的正统 Folk 版本。
它的仓库和 NuGet 包:
- GitHub: https://github.com/natemcmaster/CommandLineUtils
- NuGet: https://www.nuget.org/packages/McMaster.Extensions.CommandLineUtils
本文内容
体验超级简洁的代码吧!
我正在自己的项目中采用这款库,项目名为 mdmeta
,用于自动生成 Markdown 前的元数据标签,写博客非常方便。
体验主流的命令行参数体验
# 不带任何参数
mdmeta
# 一个简单的命令
mdmeta echo
# 一个带参数(Argument)的简单的命令
mdmeta echo "Hello!"
# 一个带选项(Option)的简单命令
mdmeta echo --upper
# 一个带参数(Argument)带选项(Option)且选项中带值的简单命令
mdmeta echo "Hello!" -s ", "
# 一个带参数(Argument)带多种选项(Option)且部分选项中带多个值的简单命令
mdmeta echo "Hello!" --repeat-count=3 -s ", " -s "| "
体验库的 Builder API
McMaster.Extensions.CommandLineUtils
使用 Builder API 配出以上的命令,代码非常简洁。
static int Main(string[] args)
{
var app = new CommandLineApplication{Name = "mdmeta"};
app.HelpOption("-?|-h|--help");
app.OnExecute(() =>
{
app.ShowHelp();
return 0;
});
app.Command("echo", command =>
{
command.Description = "输出用户输入的文字。";
command.HelpOption("-?|-h|-help");
var wordsArgument = command.Argument("[words]", "指定需要输出的文字。");
var repeatOption = command.Option("-r|--repeat-count", "指定输出重复次数", CommandOptionType.SingleValue);
var upperOption = command.Option("--upper", "指定是否全部大写", CommandOptionType.NoValue);
var separatorOption = command.Option("-s|--separator", "指定重复输出用户文字时重复之间应该使用的分隔符,可以指定多个,这将依次应用到每一次分割。", CommandOptionType.MultipleValue);
command.OnExecute(() =>
{
// 在这里使用上面各种 Argument 和 Option 的 Value 或 Values 属性拿值。
return 0;
});
});
return app.Execute(new []{"-?"});
}
体验我封装的命令行参数配置
原生库配置命令行参数已经非常方便了,几乎是一行一个功能,但 lambda
表达式嵌套太多是一个问题,会导致代码随着参数种类的增多变得急剧膨胀;于是我针对原生库做了一个基于反射的版本。于是,实现一个命令行参数只需要写这些代码就够啦:
更新:McMaster.Extensions.CommandLineUtils
接手微软之后,也添加了 Attribute
的 API,使用方法与下面的大同小异。
[CommandMetadata("echo", Description = "Output users command at specified format.")]
public sealed class SampleTask : CommandTask
{
private int _repeatCount;
[CommandArgument("[words]", Description = "The words the user wants to output.")]
public string Words { get; set; }
[CommandOption("-r|--repeat-count", Description = "Indicates how many times to output the users words.")]
public string RepeatCountRaw
{
get => _repeatCount.ToString();
set => _repeatCount = value == null ? 1 : int.Parse(value);
}
[CommandOption("--upper", Description = "Indicates that whether all words should be in upper case.")]
public bool UpperCase { get; set; }
[CommandOption("-s|--separator", Description = "Specify a string to split each repeat.")]
public List<string> Separators { get; set; }
public override int Run()
{
// 当用户敲入的命令已准备好,上面的参数准备好,那么这个函数就会在这里执行啦。
return 0;
}
}
你一定会吐槽代码变多了。确实如此!但是,当命令的种类和参数的种类变得急剧膨胀的时候,这种方式可以将各种命令都隔离开来。于是,你只需要专注于实现自己的命令就好啦!
将以下这些文件放入自己的项目中即可立刻写出上面的代码(注意 Main
函数也是需要的,因为它启动了反射):
如果发现这一行的后面不是代码,那么极有可能是被不小心屏蔽了,请手动访问:gitee.com/codes。
using System; | |
namespace Mdmeta.Core | |
{ | |
/// <summary> | |
/// Specify a property to receive argument of command from the user. | |
/// </summary> | |
[AttributeUsage(AttributeTargets.Property)] | |
public sealed class CommandArgumentAttribute : Attribute | |
{ | |
/// <summary> | |
/// Gets the argument name of a command task. | |
/// </summary> | |
public string Name { get; } | |
/// <summary> | |
/// Gets or sets the description of the argument. | |
/// This will be shown when the user typed --help option. | |
/// </summary> | |
public string Description { get; set; } | |
/// <summary> | |
/// Specify a property to receive argument of command from the user. | |
/// </summary> | |
public CommandArgumentAttribute(string argumentName) | |
{ | |
Name = argumentName; | |
} | |
} | |
} |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Reflection; | |
using Mdmeta.Core; | |
using Microsoft.Extensions.CommandLineUtils; | |
namespace Mdmeta | |
{ | |
/// <summary> | |
/// Contains a converter that can reflect an assembly to Specified <see cref="CommandLineApplication"/>. | |
/// </summary> | |
internal static class CommandLineReflector | |
{ | |
/// <summary> | |
/// Reflect an assembly and get all <see cref="CommandTask"/>s to the specified <see cref="CommandLineApplication"/>. | |
/// </summary> | |
/// <param name="app">The <see cref="CommandLineApplication"/> to receive configs.</param> | |
/// <param name="assembly">The Assembly to reflect from.</param> | |
internal static void ReflectFrom(this CommandLineApplication app, Assembly assembly) | |
{ | |
foreach (var ct in assembly.GetTypes() | |
.Where(x => typeof(CommandTask).IsAssignableFrom(x))) | |
{ | |
var commandAttribute = ct.GetCustomAttribute<CommandMetadataAttribute>(); | |
if (commandAttribute == null) | |
{ | |
continue; | |
} | |
app.Command(commandAttribute.Name, command => | |
{ | |
ConfigCommand(command, commandAttribute.Description, ct); | |
}); | |
} | |
} | |
/// <summary> | |
/// Convert a <see cref="CommandTask"/> to <see cref="CommandLineApplication"/> configs. | |
/// </summary> | |
private static void ConfigCommand(CommandLineApplication command, string commandDescription, Type taskType) | |
{ | |
// Config basic info. | |
command.Description = commandDescription; | |
command.HelpOption("-?|-h|--help"); | |
// Store argument list and option list. | |
// so that when the command executed, all properties can be initialized from command lines. | |
var argumentPropertyList = new List<(CommandArgument argument, PropertyInfo property)>(); | |
var optionPropertyList = new List<(CommandOption option, PropertyInfo property)>(); | |
// Enumerate command task properties to get enough metadata to config command. | |
foreach (var property in taskType.GetTypeInfo().DeclaredProperties) | |
{ | |
// Try to get argument and option info. | |
var argumentAttribute = property.GetCustomAttribute<CommandArgumentAttribute>(); | |
var optionAttribute = property.GetCustomAttribute<CommandOptionAttribute>(); | |
if (argumentAttribute != null && property.CanWrite) | |
{ | |
// Try to record argument info. | |
var argument = command.Argument( | |
argumentAttribute.Name, | |
argumentAttribute.Description); | |
argumentPropertyList.Add((argument, property)); | |
} | |
else if (optionAttribute != null && property.CanWrite) | |
{ | |
// Try to record option info. | |
CommandOptionType commandOptionType; | |
if (typeof(IEnumerable<string>).IsAssignableFrom(property.PropertyType)) | |
{ | |
// If this property is a List<string>. | |
commandOptionType = CommandOptionType.MultipleValue; | |
} | |
else if (typeof(string).IsAssignableFrom(property.PropertyType)) | |
{ | |
// If this property is a string. | |
commandOptionType = CommandOptionType.SingleValue; | |
} | |
else if (typeof(bool).IsAssignableFrom(property.PropertyType)) | |
{ | |
// If this property is a bool. | |
commandOptionType = CommandOptionType.NoValue; | |
} | |
else | |
{ | |
continue; | |
} | |
var option = command.Option( | |
optionAttribute.Template, | |
optionAttribute.Description, | |
commandOptionType); | |
optionPropertyList.Add((option, property)); | |
} | |
} | |
// Config how to execute the command. | |
command.OnExecute(() => | |
{ | |
// Create a new instance of CommandTask to call the Run method. | |
var commandTask = (CommandTask) Activator.CreateInstance(taskType); | |
// Initialize the instance with prepared arguments and options. | |
foreach (var (argument, property) in argumentPropertyList) | |
{ | |
property.SetValue(commandTask, argument.Value); | |
} | |
foreach (var (option, property) in optionPropertyList) | |
{ | |
switch (option.OptionType) | |
{ | |
case CommandOptionType.MultipleValue: | |
property.SetValue(commandTask, option.Values.ToList()); | |
break; | |
case CommandOptionType.SingleValue: | |
property.SetValue(commandTask, option.Value()); | |
break; | |
case CommandOptionType.NoValue: | |
property.SetValue(commandTask, option.HasValue()); | |
break; | |
default: | |
continue; | |
} | |
} | |
// Call the Run method. | |
return commandTask.Run(); | |
}); | |
} | |
} | |
} |
using System; | |
namespace Mdmeta.Core | |
{ | |
/// <summary> | |
/// Specify a unique name of a command and when user typped a command | |
/// with this name the Run method of this class will be executed. | |
/// </summary> | |
[AttributeUsage(AttributeTargets.Class)] | |
public sealed class CommandMetadataAttribute : Attribute | |
{ | |
/// <summary> | |
/// Gets the unique name of a command task. | |
/// </summary> | |
public string Name { get; } | |
/// <summary> | |
/// Gets or sets the description of the command task. | |
/// This will be shown when the user typed --help option. | |
/// </summary> | |
public string Description { get; set; } | |
/// <summary> | |
/// Specify a unique name of a command and when user typped a command | |
/// with this name the Run method of this class will be executed. | |
/// </summary> | |
public CommandMetadataAttribute(string commandName) | |
{ | |
Name = commandName; | |
} | |
} | |
} |
using System; | |
namespace Mdmeta.Core | |
{ | |
/// <summary> | |
/// Specify a property to receive an option of command from the user. | |
/// The option template format can be "-n", "--name" or "-n|--name". | |
/// The property type can be bool, string or List{string} (or any other base types). | |
/// </summary> | |
[AttributeUsage(AttributeTargets.Property)] | |
public sealed class CommandOptionAttribute : Attribute | |
{ | |
/// <summary> | |
/// Gets the option template of this option. | |
/// </summary> | |
public string Template { get; } | |
/// <summary> | |
/// Gets or sets the description of the option. | |
/// This will be shown when the user typed --help option. | |
/// </summary> | |
public string Description { get; set; } | |
/// <summary> | |
/// Specify a property to receive an option of command from the user. | |
/// The option template format can be "-n", "--name" or "-n|--name". | |
/// The property type can be bool, string or List{string} (or any other base types). | |
/// </summary> | |
public CommandOptionAttribute(string template) | |
{ | |
Template = template; | |
} | |
} | |
} |
namespace Mdmeta.Core | |
{ | |
/// <summary> | |
/// Provide a base class for all tasks that can run command from command line. | |
/// </summary> | |
public abstract class CommandTask | |
{ | |
/// <summary> | |
/// Run command when derived class override this method. | |
/// </summary> | |
/// <returns> | |
/// Return value of the whole application. | |
/// </returns> | |
public virtual int Run() | |
{ | |
return 0; | |
} | |
} | |
} |
using Mdmeta.Core; | |
using Microsoft.Extensions.CommandLineUtils; | |
namespace Mdmeta | |
{ | |
internal class Program | |
{ | |
private static int Main(string[] args) | |
{ | |
// Initialize basic command options. | |
var app = new CommandLineApplication | |
{ | |
Name = "mdmeta" | |
}; | |
app.HelpOption("-?|-h|--help"); | |
app.VersionOption("--version", "0.1"); | |
app.OnExecute(() => | |
{ | |
// If the user gives no arguments, show help. | |
app.ShowHelp(); | |
return 0; | |
}); | |
// Config command line from command tasks assembly. | |
app.ReflectFrom(typeof(CommandTask).Assembly); | |
// Execute the app. | |
var exitCode = app.Execute(args); | |
return exitCode; | |
} | |
} | |
} |
支持的平台
支持 .Net Standard 1.3,这意味着 .Net Core 可以使用,.NET Framework 4.5.1 及以上即可使用。这意味着可以很随意地跨全平台。
参考资料
- Creating Neat .NET Core Command Line Apps -natemcmaster/CommandLineUtils: Command line parsing and utilities for .NET Core and .NET Framework.
本文会经常更新,请阅读原文: https://blog.walterlv.com/post/mcmaster-extensions-commandlineutils.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。
如果你想持续阅读我的最新博客,请点击 RSS 订阅,或者前往 CSDN 关注我的主页。
本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。欢迎转载、使用、重新发布,但务必保留文章署名
吕毅
(包含链接:
https://blog.walterlv.com
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请
与我联系 (walter.lv@qq.com)
。