Skip to content

Instantly share code, notes, and snippets.

@tuscen
Last active June 8, 2020 20:05
Show Gist options
  • Save tuscen/e15c75d03df26b90a33703e553d0f654 to your computer and use it in GitHub Desktop.
Save tuscen/e15c75d03df26b90a33703e553d0f654 to your computer and use it in GitHub Desktop.

Features:

  • Parameter names can contain only alphanumeric characters, hyphen and underscore
  • The type of a variable can be omitted; in that case the parser will fall back to the default variable type
  • Every parameter in a command is required, there's no notion of optionality
  • Repeated parameter names are not allowed
  • There's no real command format validation, be careful
  • The parser is meant to be created once and be reused, so it's better to cache them

Usage example

var variableTypes = new Dictionary<string, IVariableType>
{
    { "string", new StringVariableType() },
    { "int", new IntVariableType() },
    { "long", new LongVariableType() },
    { "bool", new BoolVariableType() },
};

var commandFormat = "/commandName {{argument1:bool}} {{argument2:long}} {{argument3}}";
var variableStartChar = "{{";
var variableEndChar = "}}";
var defaultVariableType = new StringVariableType();

var commandParser = new CommandParser(
    commandFormat,
    variableStartChar, 
    variableEndChar
    variableTypes,
    defaultVariableType
);
var result = commandParser.ParseCommand("/commandName false 123456789 qwerty_-123");

if (result.Successful)
{
    Console.WriteLine(result.Variables["argument1"]); // => "false"
    Console.WriteLine(result.Variables["argument2"]); // => "123456789"
   
    // argument3 implicitly has "default" type since it was ommited in the command
    Console.WriteLine(result.Variables["argument3"]); // => "qwerty_-123"
}

Using parser builder:

var commandFormat = "/commandName {argument1:bool} {argument2:long} {argument3}";

var parser = new CommandParserBuilder()
    .UseDefault()
    .UseCommandFormat(commandFormat)
    .Build();

var result = parser.ParseCommand("/commandName false 123456789 qwerty_-123");

// ...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
public class CommandParser
{
private Regex _regex;
public CommandParser(
string commandFormat,
string variableStartChar,
string variableEndChar,
IReadOnlyDictionary<string, IVariableType> variableTypes,
IVariableType defaultVariableType)
{
if (string.IsNullOrWhutespace(commandFormat))
{
throw new ArgumentNullException(nameof(commandFormat));
}
if (string.IsNullOrWhutespace(variableStartChar))
{
throw new ArgumentNullException(nameof(variableStartChar));
}
if (string.IsNullOrWhutespace(variableEndChar))
{
throw new ArgumentNullException(nameof(variableStartChar));
}
if (variableTypes.Count == 0)
{
throw new ArgumentException(nameof(variableTypes), "Variable types collection is empty");
}
if (defaultVariableType == default)
{
throw new ArgumentNullException(nameof(defaultVariableType));
}
CommandFormat = commandFormat;
VariableTypes = variableTypes;
DefaultVariableType = defaultVariableType;
VariableStartChar = variableStartChar;
VariableEndChar = variableEndChar;
ParseCommandFormat();
}
// the 0 and 1 are used by the string.Format function, they are the start and end characters.
private static readonly string CommandTokenPattern = @"[{0}](?<variable>[a-zA-Z0-9_]+?)(:(?<type>[a-z]+?))?[{1}]";
// the <>'s denote the group name; this is used for reference for the variables later.
private static readonly string VariableTokenPattern = @"(?<{0}>{1})";
/// <summary>
/// This is the command template that values are extracted based on.
/// </summary>
/// <value>
/// A string containing variables denoted by the <see cref="VariableStartChar"/> and the <see cref="VariableEndChar"/>
/// </value>
public string CommandFormat { get; }
/// <summary>
/// Fallback type for variables with omitted types
/// </summary>
public IVariableType DefaultVariableType { get; }
public IReadOnlyDictionary<string, IVariableType> VariableTypes { get; }
/// <summary>
/// This is the character that denotes the beginning of a variable name.
/// </summary>
public string VariableStartChar { get; }
/// <summary>
/// This is the character that denotes the end of a variable name.
/// </summary>
public string VariableEndChar { get; }
/// <summary>
/// A hash set of all variable names parsed from the <see cref="CommandFormat"/>
/// </summary>
public IReadOnlyList<Variable> Variables { get; private set; }
private void ParseCommandFormat()
{
var variableList = new List<Variable>();
var matchCollection = Regex.Matches(
CommandFormat,
string.Format(CommandTokenPattern, VariableStartChar, VariableEndChar),
RegexOptions.IgnoreCase);
foreach (Match match in matchCollection)
{
var variable = CreateVariable(match);
if (variableList.Contains(variable))
{
throw new InvalidOperationException($"Variable name '{match}' is used more than once");
}
variableList.Add(variable);
}
Variables = new HashSet<Variable>(variableList);
var format = Options.CommandFormat;
foreach (var variable in Variables)
{
format = format.Replace(
variable.OriginalPattern,
string.Format(
VariableTokenPattern,
variable.Name,
variable.Type.Pattern
)
);
}
_regex = new Regex($"^{format}$", RegexOptions.IgnoreCase);
}
/// <summary>
/// Extract variable values from a given instance of the command you're trying to parse.
/// </summary>
/// <param name="commandInstance">The command instance.</param>
/// <returns>An instance of <see cref="CommandParserResult"/> indicating success or failure with a dictionary of Variable names mapped to values if success.</returns>
private Variable CreateVariable(Match match)
{
if (!match.Groups["variable"].Success)
{
throw new InvalidOperationException();
}
IVariableType variableType;
var variableTypeName = match.Groups["type"].Success
? match.Groups["type"].Value
: null;
if (string.IsNullOrWhiteSpace(variableTypeName))
{
variableType = DefaultVariableType;
}
else if (VariableTypes.ContainsKey(variableTypeName))
{
variableType = VariableTypes[variableTypeName];
}
else
{
throw new InvalidOperationException($"Invalid variable type '{variableTypeName}'");
}
var variableName = match.Groups["variable"].Value;
return new Variable(variableName, match.Value, variableType);
}
}
using System;
using System.Collections.Generic;
public class CommandParserBuilder
{
private readonly Dictionary<string, IVariableType> _variableTypes =
new Dictionary<string, IVariableType>();
private string _variableStartChar;
private string _variableEndChar;
private string _commandFormat;
private IVariableType _defaultVariableType;
public CommandParserBuilder UseDefaultType<TVariableType>()
where TVariableType : IVariableType, new()
{
var variableType = Activator.CreateInstance<TVariableType>();
return UseDefaultType(variableType);
}
public CommandParserBuilder UseDefaultType(IVariableType variableType)
{
_defaultVariableType = variableType ?? throw new ArgumentNullException(nameof(variableType));
return this;
}
public CommandParserBuilder UseDefaultType(string name)
{
if (!_variableTypes.ContainsKey(name))
{
throw new ArgumentOutOfRangeException(nameof(name), name, $"No variable type named '{name}'");
}
_defaultVariableType = _variableTypes[name];
return this;
}
public CommandParserBuilder UseVariableType<TVariableType>()
where TVariableType : IVariableType, new()
{
var variableType = Activator.CreateInstance<TVariableType>();
return UseVariableType(variableType);
}
public CommandParserBuilder UseVariableType(IVariableType variableType)
{
if (variableType == null)
{
throw new ArgumentNullException(nameof(variableType));
}
_variableTypes[variableType.Name] = variableType;
return this;
}
public CommandParserBuilder UseVariableDelimiters(string start, string end)
{
_variableStartChar = start;
_variableEndChar = end;
return this;
}
public CommandParserBuilder UseDefault()
{
_variableTypes["string"] = new StringVariableType();
_variableTypes["int"] = new IntVariableType();
_variableTypes["long"] = new LongVariableType();
_variableTypes["bool"] = new BoolVariableType();
_defaultVariableType = _variableTypes["string"];
_variableEndChar = "{";
_variableEndChar = "}";
return this;
}
public CommandParserBuilder UseCommandFormat(string commandFormat)
{
if (string.IsNullOrWhiteSpace(commandFormat))
{
throw new ArgumentNullException(nameof(commandFormat));
}
_commandFormat = commandFormat;
return this;
}
public CommandParser Build()
{
if (string.IsNullOrWhiteSpace(_commandFormat))
{
throw new InvalidOperationException("Command format is not set");
}
if (string.IsNullOrWhiteSpace(_variableStartChar))
{
throw new InvalidOperationException("Start variable character is not set");
}
if (string.IsNullOrWhiteSpace(_variableStartChar))
{
throw new InvalidOperationException("End variable character is not set");
}
if (_variableTypes.Count == 0)
{
throw new InvalidOperationException("Variable types is empty");
}
if (_defaultVariableType == null)
{
throw new InvalidOperationException("Default variable type is not set");
}
var variableTypes = new Dictionary<string, IVariableType>();
foreach (var variableType in _variableTypes)
{
if (variableType.Value == null)
{
throw new InvalidOperationException($"'{variableType}' variable type is not set");
}
variableTypes[variableType.Key] = variableType.Value;
}
return new CommandParser(
_commandFormat,
_variableStartChar,
_variableEndChar
variableTypes,
_defaultVariableType
);
}
public class CommandParserResult
{
public IReadOnlyDictionary<string, string> Variables { get; }
public bool Successful { get; }
public string CommandFormat { get; }
public static CommandParserResult Failure(string commandFormat)
=> new CommandParserResult(commandFormat);
public static CommandParserResult Success(IReadOnlyDictionary<string, string> values, string commandFormat)
=> new CommandParserResult(commandFormat, true, values);
private CommandParserResult(
string commandFormat,
bool successful = default,
IReadOnlyDictionary<string, string> variables = default)
{
CommandFormat = commandFormat;
Variables = variables;
Successful = successful;
}
}
public class BoolVariableType : IVariableType
{
public string Pattern => "(true|false)";
public string Name => "bool";
}
public class IntVariableType : IVariableType
{
public string Pattern => "([0]{1}|-?[1-9]{1}[0-9]{0,9})";
public string Name => "int";
}
public class LongVariableType : IVariableType
{
public string Pattern => "([0]{1}|-?[1-9]{1}[0-9]{0,18})";
public string Name => "long";
}
public class StringVariableType : IVariableType
{
public string Pattern => "[a-zA-Z0-9_-]+";
public string Name => "string";
}
public interface IVariableType
{
string Pattern { get; }
string Name { get; }
}
public sealed class Variable : IEquatable<Variable>
{
public string Name { get; }
public string OriginalPattern { get; }
public IVariableType Type { get; }
public Variable(string name, string originalPattern, IVariableType type)
{
Name = name;
OriginalPattern = originalPattern;
Type = type;
}
public bool Equals(Variable other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Variable) obj);
}
public override int GetHashCode()
{
unchecked
{
return (Name != null ? Name.GetHashCode() : 0) * 397;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment