在某一天,我突然发现.Net控制台应用的参数是可以传递的;额,理论上所有应用都支持命令行传参。
命令行参数无非两种:
(1)命令+参数
(2)选项+参数
通常命令行工具都使用空格区分;选项格式是--name或-n,分别对应选项全名和简写;命令行需要对参数、命令、选项进行解释,标配-v(打印版本),-h(打印帮助信息);有这些个思路,就可以开干了。
项目仓库:https://github.com/hedonghua/Cracker.CommandLine
我们只需要建立一个类,缓存命令、选项;当参数传入时,内部进行解析,再匹配到相应的命令,调用命令类的执行方法,例如:
var app = new CommandApp();
app.Add<GuidCommand>("uuid").WithDescription("生成UUID");
app.Add<TestCommand>("test").WithDescription("测试泛型命令");
await app.StartAsync(args);
CommandApp类详解:
(1)Add方法进行挂载命令,缓存到字典
(2)StartAsync方法启动,解析参数并执行
如何新建一个命令类?
(1)类必须继承CommandBase.cs
基类
(2)实现ExecuteAsync抽象方法
(3)CommandContext参数是挂载命令时传入的
如何新建一个命令参数接收类?
(1)属性使用CliArgumentAttribute标记参数
(2)属性使用CliOptionAttribute标记选项
(3)作为泛型参数传入CommandBase
(4)子类使用Receive属性调用参数类
核心代码:
CommandApp.cs
using Cracker.CommandLine.Attributes;
using Cracker.CommandLine.Helpers;
using Cracker.CommandLine.Models;
using System.Reflection;
namespace Cracker.CommandLine
{
public class CommandApp
{
private readonly IDictionary<string, CommandConfiguration> _cmds;
private readonly string[] _helpOptionNames = ["-h", "--help"];
private readonly string[] _versionOptionNames = ["-v", "--version"];
private const string VERSION = "v1.0.1";
public CommandApp()
{
_cmds = new Dictionary<string, CommandConfiguration>();
}
/// <summary>
/// 添加命令
/// </summary>
/// <typeparam name="TCommand">命令</typeparam>
/// <param name="name">命令名称,唯一</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public CommandConfiguration Add<TCommand>(string name) where TCommand : CommandBase, new()
{
if (_cmds.ContainsKey(name)) throw new Exception($"已存在{name}的命令");
var conf = new CommandConfiguration(name, new TCommand());
_cmds.TryAdd(name, conf);
return conf;
}
/// <summary>
/// 启动
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
/// <exception cref="DirectoryNotFoundException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public async Task StartAsync(string[] args)
{
try
{
if (args.Length > 0)
{
if (args[0].Contains('-'))
{
if (_helpOptionNames.Contains(args[0]))
{
Console.WriteLine("选项:");
Console.WriteLine("\t{0,-20}\t{1}", string.Join(',', _helpOptionNames), "打印帮助信息");
Console.WriteLine("\t{0,-20}\t{1}", string.Join(',', _versionOptionNames), "打印版本信息");
Console.WriteLine("命令:");
foreach (var item in _cmds)
{
Console.WriteLine("\t{0,-20}\t{1}", item.Key, item.Value.Description);
}
}
else if (_versionOptionNames.Contains(args[0]))
{
Console.WriteLine(VERSION);
}
}
else
{
if (!_cmds.TryGetValue(args[0], out CommandConfiguration? value)) throw new DirectoryNotFoundException($"命令{args[0]}找不到");
var type = value.Instance.GetType();
var skipArgs = args.Skip(1).ToArray();
var arguments = new List<string>();
var options = new Dictionary<string, string>();
for (var i = 0; i < skipArgs.Length; i++)
{
if (skipArgs[i].StartsWith('-'))
{
if (_helpOptionNames.Contains(skipArgs[i]))
{
// 打印帮助信息
await Console.Out.WriteLineAsync(value.HelpInformation);
return;
}
if (i + 1 < skipArgs.Length)
{
options.TryAdd(skipArgs[i], skipArgs[i + 1]);
break; // 选项只取一个值,后面的值不算参数不管
}
}
else
{
arguments.Add(skipArgs[i]);
}
}
CommandBase instance = value.Instance;
// 泛型命令
if (type.BaseType!.IsGenericType)
{
var genericType = type.BaseType.GetGenericArguments()[0];
var genericInstance = Activator.CreateInstance(genericType);
if (genericInstance != null)
{
LoadParams(arguments, options, genericType, genericInstance);
var receiveProp = type.GetProperty(nameof(CommandBase<object>.Receive));
receiveProp?.SetValue(instance, genericInstance);
}
}
else
{
LoadParams(arguments, options, type, instance);
}
await instance.ExecuteAsync(value.Context ?? new CommandContext());
}
}
else
{
Console.WriteLine("欢迎使用自定义命令行!");
}
}
catch (Exception ex)
{
await Console.Out.WriteLineAsync(ex.Message);
}
}
/// <summary>
/// 加载参数
/// </summary>
/// <param name="arguments"></param>
/// <param name="options"></param>
/// <param name="type"></param>
/// <param name="instance"></param>
/// <exception cref="ArgumentNullException"></exception>
private static void LoadParams(List<string> arguments, Dictionary<string, string> options, Type type, object instance)
{
foreach (var prop in type.GetProperties())
{
var optionAttr = prop.GetCustomAttribute<CliOptionAttribute>();
if (optionAttr != null && options.TryGetValue(optionAttr.Name, out var optionValue))
{
var val = ConvertHelper.To(optionValue, prop.PropertyType);
prop.SetValue(instance, val);
continue;
}
var argAttr = prop.GetCustomAttribute<CliArgumentAttribute>();
if (argAttr != null)
{
if (arguments.Count >= 1 + argAttr.Position)
{
if (prop.PropertyType.IsArray)
{
// 数组参数取值:从位置开始直至末尾
string[] strs = [.. arguments[argAttr.Position..]];
prop.SetValue(instance, strs);
}
else
{
var val = ConvertHelper.To(arguments[argAttr.Position], prop.PropertyType);
prop.SetValue(instance, val);
}
}
else if (argAttr.Required)
{
throw new ArgumentNullException(prop.Name, "参数是必须的");
}
}
}
}
}
}
CommandConfiguration.cs
using Cracker.CommandLine.Attributes;
using System.Reflection;
using System.Text;
namespace Cracker.CommandLine.Models
{
public class CommandConfiguration
{
public CommandConfiguration(string name, CommandBase instance)
{
Name = name;
Instance = instance;
HelpInformation = GetCommandHelpInformation();
}
public CommandConfiguration WithDescription(string description)
{
Description = description;
return this;
}
public CommandConfiguration WithData(object data)
{
Context = new CommandContext { Data = data };
return this;
}
/// <summary>
/// 命令名称
/// </summary>
public string Name { get; private set; }
/// <summary>
/// 命令实例
/// </summary>
public CommandBase Instance { get; private set; }
/// <summary>
/// 命令描述
/// </summary>
public string Description { get; private set; }
/// <summary>
/// 上下文
/// </summary>
public CommandContext Context { get; private set; }
/// <summary>
/// 帮助信息
/// </summary>
public string HelpInformation { get; private set; }
private string GetCommandHelpInformation()
{
var type = Instance.GetType();
if (type.BaseType != null && type.BaseType.IsGenericType)
{
type = type.BaseType!.GetGenericArguments()[0]!;
}
var options = new Dictionary<string, string>();
var arguments = new Dictionary<int, string>();
var sb = new StringBuilder();
foreach (var prop in type.GetProperties())
{
var optionAttr = prop.GetCustomAttribute<CliOptionAttribute>();
if (optionAttr != null) options.TryAdd(optionAttr.Name, optionAttr.Description);
var argAttr = prop.GetCustomAttribute<CliArgumentAttribute>();
if (argAttr != null)
{
if (arguments.ContainsKey(argAttr.Position)) throw new Exception("参数位置不能重复");
arguments.TryAdd(argAttr.Position, argAttr.Description);
}
var arrayType = typeof(string[]);
if (prop.PropertyType.IsArray && prop.PropertyType != arrayType)
{
throw new ArgumentException("数组参数只支持" + arrayType);
}
}
if (arguments.Count > 0)
{
sb.AppendLine("参数:");
foreach (var item in arguments.OrderBy(x => x.Key))
{
sb.AppendFormat("\t[{0}]\t{1}\r\n", item.Key + 1, item.Value);
}
}
if (options.Count > 0)
{
sb.AppendLine("选项:");
foreach (var item in options)
{
sb.AppendFormat("\t{0,-20}\t{1}\r\n", item.Key, item.Value);
}
}
return sb.ToString();
}
}
}