Skip to content

Instantly share code, notes, and snippets.

@walterlv
Created October 21, 2017 01:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save walterlv/0a2257c30e8c175cae657b0058f5421c to your computer and use it in GitHub Desktop.
Save walterlv/0a2257c30e8c175cae657b0058f5421c to your computer and use it in GitHub Desktop.
Provide a reflection based wrapper for `Microsoft.Extensions.CommandlineUtils` Library.
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;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment