Skip to content

Instantly share code, notes, and snippets.

@craigsmitham
Last active August 27, 2025 12:57
Show Gist options
  • Save craigsmitham/a7f8cdbfc267a0a15e74 to your computer and use it in GitHub Desktop.
Save craigsmitham/a7f8cdbfc267a0a15e74 to your computer and use it in GitHub Desktop.
Monadic Command Line Parser w/Linq
using System;
/*
This program illustrates the use of a C# monadic command line parser to provide a
simple, declaritive, composable and strongly typed API that makes the resulting code easy
to reason about what argument flags are being parsed, and how they are being used
to execute program logic. This is done with a minimum (arguably no) boilerplate, improving
on existing "Fluent", POCO/Attribute, and impertive approaches common in C# .NET libraries.
The power and simplicity of this API is a result of using the LINQ query expression syntax
to compose a custom monad. LINQ query expression syntax is relly just a monad comprehension in
disguise (http://bit.ly/1KY2wMd), and also very similar to Scala for-comprhension syntax. We are
NOT simply using LINQ exension methods on the command line arguments (we aren't even using
System.Linq), but instead creating custom LINQ extension methods that operate on custom
types that implement our domain logic. A common misconception among .NET developers is that LINQ
and query expressions rely on an IEnumerable interface, or at least the IEnumerable extensions
methods defined in System.Linq. The latter is partially true, but the reality is that any type
that has appropriate .NET query operators (http://bit.ly/1hhPtfY) directly or via extension
method can support LINQ. View/run on .NET Fiddle: https://dotnetfiddle.net/odTRNi
Enjoy!
Areas for futher improvement:
- Allow composition of multiple commands (right now we're just parsing flags for a single command/exe)
- Extend API to support documentation of flags and commands for display of command reference user
- Error handling
- Auto-complete
*/
public static class MonadicCommandLineParser
{
public static void Main(string[] args) => (
from message in String("message")
from times in Int("times")
from exitWhenDone in Bool("exit")
select () =>
{
for (var i = 0; i < times; i++)
Console.WriteLine($"{i + 1}:\t{message}");
if (!exitWhenDone) Console.ReadLine();
})(new[] { "-message", "Howdy!", "-times", "10", "-exit", "false" } /* or use args */);
// Linq Support
public static CommandLineParser<TNext> Select<T, TNext>(this CommandLineParser<T> parser, Func<T, TNext> selector) => parser.ContinueWith(r => Success(selector(r)));
public static CommandLineParser<TNext> SelectMany<T, TInner, TNext>(this CommandLineParser<T> parser, Func<T, CommandLineParser<TInner>> selector,
Func<T, TInner, TNext> projector) => parser.ContinueWith(t => selector(t).Select(u => projector(t, u)));
public static CommandLineParser<T> SelectMany<T, TInner>(this CommandLineParser<T> parser, Func<T, CommandLineParser<TInner>> selector,
Func<T, TInner, Action> projector) => parser.ContinueWith(t => selector(t).Select(u => Execute<T>(projector(t, u))));
public delegate Result<T> CommandLineParser<T>(string[] args);
public static CommandLineParser<T> Success<T>(T value) => args => Success(value, args);
public static T Execute<T>(Action action)
{
action();
return default(T);
}
public static CommandLineParser<TNext> ContinueWith<T, TNext>(this CommandLineParser<T> initial, Func<T, CommandLineParser<TNext>> following) =>
args => initial(args).IfSuccess(result => following(result.Value)(result.Args));
// Parsers
public static CommandLineParser<string> String(string flag) => args => GetArg(s => Success(s, args), flag)(args);
public static CommandLineParser<bool> Bool(string flag) => args => GetArg(s =>
{
bool value;
return bool.TryParse(s, out value) ? Success(value, args) : Failure<bool>(args);
}, flag)(args);
public static CommandLineParser<int> Int(string flag) => args => GetArg(s =>
{
int value;
return int.TryParse(s, out value) ? Success(value, args) : Failure<int>(args);
}, flag)(args);
public static CommandLineParser<T> GetArg<T>(Func<string, Result<T>> map, string flag) => args =>
{
var flagIndex = Array.IndexOf(args, "-" + flag);
return flagIndex == -1 ? Failure<T>(args) : map(args[flagIndex + 1]);
};
public class Result<T>
{
public Result(bool success, string[] args, T value = default(T))
{
Success = success; Value = value; Args = args;
}
public bool Success { get; }
public T Value { get; }
public string[] Args { get; }
}
// Helpers
public static Result<T> Success<T>(T value, string[] args) => new Result<T>(true, args, value);
public static Result<T> Failure<T>(string[] args) => new Result<T>(false, args);
public static Result<TNext> IfSuccess<T, TNext>(this Result<T> result, Func<Result<T>, Result<TNext>> next) => result.Success ? next(result) : Failure<TNext>(result.Args);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment