Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluent syntax for command creation #2494

Open
MihailsKuzmins opened this issue Oct 8, 2024 · 0 comments
Open

Fluent syntax for command creation #2494

MihailsKuzmins opened this issue Oct 8, 2024 · 0 comments

Comments

@MihailsKuzmins
Copy link

System.CommandLine is a nice library that convers a lot of edge cases, but in my opinion creating commands (adding handlers) is quite verbose and adds a lot of boilerplate code to the main function.

What I was thinking is that maybe it could be possible to add some extension methods to make creating commands more simple. For example, in my project I have something like this:

private static async Task<int> Main(string[] args)
{
	var rootCommand = new RootCommand
	{
		new CommandBuilder("do-something", "Does something")
			.WithOption("--type", CommandLineUtils.GetType)
			.WithOption<bool>("--run-migrations")
			.Build(DoSomethingAsync),

		new CommandBuilder("do-another-thing", "Does another thing")
			.WithArgument<string>()
			.Build(DoAnotherThingAsync),
	};

	return await rootCommand.InvokeAsync(args)
		.ConfigureAwait(false);
}

private static async Task DoSomethingAsync(SomeType type, bool runMigrations)
{
	await using var services = BuildServiceProvider();

	if (runMigrations)
		services.RunMigrations();

	await services.GetRequiredService<ISomethingService>()
		.DoSomethingAsync(type);
}

private static async Task DoAnotherThingAsync(string arg)
{
	await using var services = BuildServiceProvider();

	await services.GetRequiredService<IAnotherService>()
		.DoSomethingElseAsync(arg);
}

The CommandBuilder is a class that encapsulated the boiler plate code. For example:

namespace System.CommandLine;

public sealed class CommandBuilder
{
	internal readonly string Name;
	internal readonly string Description;

	public CommandBuilder(string name, string description)
	{
		Name = name;
		Description = description;
	}

	public CommandHandler1Builder<T> WithOption<T>(in string name, ParseArgument<T> parseArgument) =>
		new(this, name, ArgType.Option, parseArgument);
}

And CommandHandler1Builder<T>, CommandHandler2Builder<T1, T2>, CommandHandler3Builder<T1, T2, T3>, etc. are used to create handlers, arguments and options. For example:

using System.CommandLine.Binding;

namespace System.CommandLine;

public sealed class CommandHandler1Builder<T> : CommandHandlerBuilderBase
{
	internal readonly CommandBuilder CommandBuilder;
	private readonly string _name;
	private readonly ParseArgument<T>? _parseArgument;

	internal CommandHandler1Builder(in CommandBuilder commandBuilder, in string name, in ArgType argType, in ParseArgument<T>? parseArgument = null)
		: base(argType)
	{
		CommandBuilder = commandBuilder;
		_name = name;
		_parseArgument = parseArgument;
	}

	public Command Build(Func<T, Task> handler)
	{
		var command = new Command(CommandBuilder.Name, CommandBuilder.Description);

		var value = AppendValueDescriptor(command);
		command.SetHandler(handler, value);

		return command;
	}

	public CommandHandler2Builder<T, T2> WithOption<T2>(in string name) =>
		new(this, name, ArgType.Option);

	public CommandHandler2Builder<T, T2> WithArgument<T2>(in string name) =>
		new(this, name, ArgType.Argument);

	internal IValueDescriptor<T> AppendValueDescriptor(in Command command) =>
		AppendValueDescriptor(command, _name, _parseArgument);
}
using System.CommandLine.Binding;

namespace System.CommandLine;

public sealed class CommandHandler2Builder<T1, T2> : CommandHandlerBuilderBase
{
	private readonly CommandHandler1Builder<T1> _commandBuilder;
	private readonly string _name;

	internal CommandHandler2Builder(CommandHandler1Builder<T1> commandBuilder, string name, ArgType argType)
		: base(argType)
	{
		_commandBuilder = commandBuilder;
		_name = name;
	}

	public Command Build(Func<T1, T2, Task> handler)
	{
		var command = new Command(_commandBuilder.CommandBuilder.Name, _commandBuilder.CommandBuilder.Description);

		var value1 = _commandBuilder.AppendValueDescriptor(command);
		var option2 = AppendValueDescriptor(command);
		command.SetHandler(handler, value1, option2);

		return command;
	}

	private IValueDescriptor<T2> AppendValueDescriptor(in Command command) =>
		AppendValueDescriptor<T2>(command, _name);
}

Would it be something that you can be interested in? If yes, I might work on this feature and prepare a PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant