// There is no license for this code. You are free to modify and redistribute it. Also, you can delete this comment.
// Original source code: https://github.com/wertrain/command-line-parser-cs (Version 0.2)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
namespace CommandLineParser
{
///
/// Option attribute
///
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class Option : Attribute
{
public Option() { }
public Option(string longName) => LongName = longName;
public Option(char shortName) => ShortName = shortName;
public Option(char shortName, string longName)
{
ShortName = shortName;
LongName = longName;
}
/// long name command option
public string LongName { get; private set; } = string.Empty;
/// short name command option
public char ShortName { get; private set; }
/// command option
public string HelpText { get; set; } = string.Empty;
/// command option
public bool Required { get; set; }
}
///
/// Results type of parse
///
public enum ParserResultType
{
Parsed = 0,
NotParsed
}
///
/// Results of parse
///
///
public class ParserResult
{
internal ParserResult(ParserResultType tag, T value)
{
Tag = tag;
Value = value;
Message = string.Empty;
}
internal ParserResult(ParserResultType tag, T value, string message)
{
Tag = tag;
Value = value;
Message = message;
}
public ParserResultType Tag { get; }
public T Value { get; }
public string Message { get; }
}
///
/// Utility for parser
///
static class ParserUtility
{
///
/// Convert string to object using TypeConverter with nullable support
///
public static object ConvertToType(string input, Type targetType)
{
if (targetType == typeof(string)) return input;
var nonNullableType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (nonNullableType == typeof(bool))
{
if (string.IsNullOrEmpty(input)) return true; // flag only => true
if (bool.TryParse(input, out var b)) return b;
// 1/0 を解釈する
if (input.Equals("1")) return true;
if (input.Equals("0")) return false;
throw new FormatException($"'{input}' cannot be converted to Boolean.");
}
var converter = TypeDescriptor.GetConverter(nonNullableType);
if (converter != null && converter.CanConvertFrom(typeof(string)))
{
var result = converter.ConvertFromString(input);
if (result == null)
{
throw new FormatException($"'{input}' cannot be converted to {nonNullableType.Name}.");
}
return result;
}
// fallback
return Convert.ChangeType(input, nonNullableType);
}
///
/// Set value to a property on the target instance based on the incoming token text.
/// Returns true if a value token was consumed (for non-bool) or successfully set.
///
public static bool SetValueToProperty(PropertyInfo property, T target, string? param)
{
var propertyType = property.PropertyType;
var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
var isBool = underlyingType == typeof(bool);
// Bool: if no explicit value or next token is another option, set true.
if (isBool)
{
if (string.IsNullOrEmpty(param))
{
property.SetValue(target, true);
return false; // no next token consumed
}
// explicit true/false
if (bool.TryParse(param, out var b))
{
property.SetValue(target, b);
return true; // consumed param
}
// If the next token looks like an option (starts with '-' or '--'), treat this as flag only.
if (IsOptionToken(param))
{
property.SetValue(target, true);
return false;
}
throw new FormatException($"'{param}' cannot be converted to Boolean.");
}
// Non-bool types require a value. If missing or next is option, it's an error.
if (string.IsNullOrEmpty(param) || IsOptionToken(param))
{
throw new FormatException($"Option '{property.Name}' requires a value.");
}
var converted = ConvertToType(param!, propertyType);
property.SetValue(target, converted);
return true;
}
public static bool IsOptionToken(string? s)
{
if (s == null || string.IsNullOrEmpty(s) || s == "-") return false;
return s.StartsWith("--", StringComparison.Ordinal) ||
(s.StartsWith("-", StringComparison.Ordinal) && s.Length > 1 && !char.IsDigit(s[1]));
}
}
///
/// Arguments parser
///
public class Parser
{
private const string HelpLongName = "help";
///
/// Parse from a single string (whitespace split)
///
public static ParserResult Parse(string args) where T : new()
{
var enumerable = string.IsNullOrWhiteSpace(args)
? Array.Empty()
: args.Split((string[]?)null, StringSplitOptions.RemoveEmptyEntries);
return Parse(enumerable);
}
///
/// Parse from enumerable of tokens
///
public static ParserResult Parse(IEnumerable args) where T : new()
{
var tokens = (args ?? Enumerable.Empty()).ToArray();
if (tokens.Length == 0)
{
ShowHelp();
return new ParserResult(ParserResultType.NotParsed, new T());
}
var value = new T();
var resultTag = ParserResultType.Parsed;
// Prepare property maps only once
var props = typeof(T)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => new { Prop = p, Opt = p.GetCustomAttribute