Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion PCL.Core/App/Basics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,15 @@ public static class Basics
/// </summary>
public static string ExecutableNameWithoutExtension { get; } = Path.GetFileNameWithoutExtension(ExecutablePath);

/// <summary>
/// 当前进程包括第一个参数(文件名)的完整命令行参数。
/// </summary>
public static string[] FullCommandLineArguments { get; } = Environment.GetCommandLineArgs();

/// <summary>
/// 当前进程不包括第一个参数(文件名)的命令行参数。
/// </summary>
public static string[] CommandLineArguments { get; } = Environment.GetCommandLineArgs().Skip(1).ToArray();
public static string[] CommandLineArguments { get; } = FullCommandLineArguments[1..];

/// <summary>
/// 实时获取的当前目录。若要在可执行文件目录中存放文件等内容,请使用更准确的 <see cref="ExecutableDirectory"/> 而不是这个目录。
Expand Down
8 changes: 8 additions & 0 deletions PCL.Core/App/Cli/ArgumentValueKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PCL.Core.App.Cli;

public enum ArgumentValueKind
{
Bool,
Decimal,
Text,
}
34 changes: 34 additions & 0 deletions PCL.Core/App/Cli/BoolArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Runtime.CompilerServices;

namespace PCL.Core.App.Cli;

public class BoolArgument : CommandArgument<bool>
{
public override ArgumentValueKind ValueKind => ArgumentValueKind.Bool;

protected override bool ParseValueText()
{
var text = ValueText.ToLowerInvariant().Trim();
return text is not ("0" or "false");
}

public override bool TryCastValue<T>(out T value)
{
if (base.TryCastValue(out value)) return true;
var type = typeof(T);
if (type != typeof(sbyte) &&
type != typeof(byte) &&
type != typeof(short) &&
type != typeof(ushort) &&
type != typeof(int) &&
type != typeof(uint) &&
type != typeof(long) &&
type != typeof(ulong) &&
type != typeof(nint) &&
type != typeof(nuint)) return false;
// magic code
var v = Value;
Unsafe.As<T, byte>(ref value) = Unsafe.As<bool, byte>(ref v);
return true;
}
}
89 changes: 89 additions & 0 deletions PCL.Core/App/Cli/CommandArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Runtime.CompilerServices;

namespace PCL.Core.App.Cli;

/// <summary>
/// 无泛型的命令行参数模型
/// </summary>
/// <seealso cref="CommandArgument{TValue}"/>
public abstract class CommandArgument
{
/// <summary>
/// 参数键
/// </summary>
public required string Key { get; init; }

/// <summary>
/// 参数值文本
/// </summary>
public required string ValueText { get; init; }

/// <summary>
/// 参数值类型
/// </summary>
public abstract ArgumentValueKind ValueKind { get; }

/// <summary>
/// 尝试以指定类型获取参数值
/// </summary>
/// <param name="value">参数值,若尝试失败则为该类型默认值</param>
/// <typeparam name="T">参数值的类型</typeparam>
/// <returns>是否成功,若类型不匹配则失败</returns>
public abstract bool TryCastValue<T>(out T? value);

public T? CastValue<T>()
{
var result = TryCastValue(out T? value);
return result ? value : throw new InvalidCastException("Value type mismatch or cannot cast");
}
}

/// <summary>
/// 命令行参数模型
/// </summary>
/// <typeparam name="TValue">参数值的类型</typeparam>
public abstract class CommandArgument<TValue> : CommandArgument
{
/// <summary>
/// 从参数值文本中解析参数类型
/// </summary>
/// <returns>对应类型的参数值</returns>
protected abstract TValue ParseValueText();

private bool _isValueParsed = false;

/// <summary>
/// 参数值
/// </summary>
public TValue Value
{
get
{
if (_isValueParsed) return field;
_isValueParsed = true;
return field = ParseValueText();
}
protected init
{
field = value;
_isValueParsed = true;
}
} = default!;

public override bool TryCastValue<T>(out T value)
{
if (Value is T v)
{
value = v;
return true;
}
value = default!;
if (typeof(T) == typeof(string))
{
Unsafe.As<T, string>(ref value) = ValueText;
return true;
}
return false;
}
}
116 changes: 116 additions & 0 deletions PCL.Core/App/Cli/CommandLine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;

namespace PCL.Core.App.Cli;

/// <summary>
/// 命令行模型
/// </summary>
public class CommandLine
{
/// <summary>
/// 命令文本
/// </summary>
public required string CommandText { get; init; }

/// <summary>
/// 子命令
/// </summary>
public CommandLine? Subcommand { get; init; } = null;

/// <summary>
/// 子命令文本
/// </summary>
public string? SubcommandText => Subcommand?.CommandText;

/// <summary>
/// 参数字典
/// </summary>
public required IReadOnlyDictionary<string, CommandArgument> Arguments { get; init; }

/// <summary>
/// 尝试获取参数值
/// </summary>
/// <param name="key">参数键</param>
/// <param name="value">参数值,若获取失败则为对应类型默认值</param>
/// <typeparam name="TValue">参数值的类型</typeparam>
/// <returns>是否获取成功,若不存在该键或值类型不匹配则失败</returns>
public bool TryGetArgumentValue<TValue>(string key, out TValue? value)
{
var result = Arguments.TryGetValue(key, out var arg);
if (result && arg!.TryCastValue(out TValue? typedValue))
{
value = typedValue;
return true;
}
value = default;
return false;
}

/// <summary>
/// 尝试获取参数值
/// </summary>
/// <param name="key">参数键</param>
/// <typeparam name="TValue">参数值的类型</typeparam>
/// <returns>参数值</returns>
/// <exception cref="InvalidCastException">不存在该键或值类型不匹配</exception>
public TValue? GetArgumentValue<TValue>(string key)
{
var result = TryGetArgumentValue(key, out TValue? value);
return result ? value : throw new InvalidCastException($"Key '{key}' not found or value type mismatch");
}

/// <summary>
/// 解析参数数组,第一个元素会被视为主命令
/// </summary>
/// <param name="args">参数数组</param>
/// <param name="subcommands">各级子命令列表</param>
/// <returns>命令行模型实例</returns>
public static CommandLine Parse(ReadOnlySpan<string> args, IEnumerable<SubcommandDefinition>? subcommands = null)
{
subcommands ??= [];
SubcommandDefinition root = (args[0], subcommands);
return CommandLineParser.Parse(args, root);
}
}

file static class CommandLineParser
{
private static (CommandArgument, bool) _ParseArgument(string key, string possibleValueText)
{
if (possibleValueText.Length == 0 || possibleValueText.StartsWith("--"))
return (new BoolArgument { Key = key, ValueText = string.Empty }, false);
if (possibleValueText.ToLowerInvariant() is "true" or "false")
return (new BoolArgument { Key = key, ValueText = possibleValueText }, true);
if (decimal.TryParse(possibleValueText, out var d))
return (new DecimalArgument { Key = key, ValueText = possibleValueText, Value = d }, true);
return (new TextArgument { Key = key, ValueText = possibleValueText }, true);
}

public static CommandLine Parse(ReadOnlySpan<string> args, SubcommandDefinition subcommands)
{
if (args.IsEmpty) throw new ArgumentException("The argument span must contain at least 1 element", nameof(args));
var i = 1;
var commandText = args[0];
var argumentList = new Dictionary<string, CommandArgument>();
CommandLine? subcommand = null;
while (i < args.Length)
{
var currentText = args[i];
if (subcommands.Contains(currentText))
{
subcommand = Parse(args[i..], subcommands.SubcommandMap[currentText]);
break;
}
var (commandArgument, hasValueText) = _ParseArgument(currentText, (i == args.Length - 1) ? "" : args[i + 1]);
argumentList[commandArgument.Key] = commandArgument;
i += hasValueText ? 2 : 1;
}
return new CommandLine
{
CommandText = commandText,
Arguments = argumentList.AsReadOnly(),
Subcommand = subcommand
};
}
}
44 changes: 44 additions & 0 deletions PCL.Core/App/Cli/DecimalArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Runtime.CompilerServices;

namespace PCL.Core.App.Cli;

public class DecimalArgument : CommandArgument<decimal>
{
public override ArgumentValueKind ValueKind => ArgumentValueKind.Decimal;

protected override decimal ParseValueText() => decimal.Parse(ValueText);

public new decimal Value
{
get => base.Value;
init => base.Value = value;
}

public override bool TryCastValue<T>(out T value)
{
if (base.TryCastValue(out value)) return true;
var type = typeof(T);
try
{
if (type == typeof(int)) Unsafe.As<T, int>(ref value) = Convert.ToInt32(Value);
else if (type == typeof(long)) Unsafe.As<T, long>(ref value) = Convert.ToInt64(Value);
else if (type == typeof(double)) Unsafe.As<T, double>(ref value) = Convert.ToDouble(Value);
else if (type == typeof(float)) Unsafe.As<T, float>(ref value) = Convert.ToSingle(Value);
else if (type == typeof(short)) Unsafe.As<T, short>(ref value) = Convert.ToInt16(Value);
else if (type == typeof(sbyte)) Unsafe.As<T, sbyte>(ref value) = Convert.ToSByte(Value);
else if (type == typeof(ulong)) Unsafe.As<T, ulong>(ref value) = Convert.ToUInt64(Value);
else if (type == typeof(uint)) Unsafe.As<T, uint>(ref value) = Convert.ToUInt32(Value);
else if (type == typeof(ushort)) Unsafe.As<T, ushort>(ref value) = Convert.ToUInt16(Value);
else if (type == typeof(byte)) Unsafe.As<T, byte>(ref value) = Convert.ToByte(Value);
else if (type == typeof(nint)) Unsafe.As<T, nint>(ref value) = checked((nint)Convert.ToInt64(Value));
else if (type == typeof(nuint)) Unsafe.As<T, nuint>(ref value) = checked((nuint)Convert.ToUInt64(Value));
else return false;
return true;
}
catch (Exception ex) when (ex is OverflowException or InvalidCastException or FormatException)
{
return false;
}
}
}
45 changes: 45 additions & 0 deletions PCL.Core/App/Cli/SubcommandDefinition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Collections.Immutable;

namespace PCL.Core.App.Cli;

public class SubcommandDefinition
{
public required string CommandText { get; init; }

public required IEnumerable<SubcommandDefinition> Subcommands { private get; init; }

public IReadOnlyDictionary<string, SubcommandDefinition> SubcommandMap
{
get
{
if (field != null) return field;
var map = new Dictionary<string, SubcommandDefinition>();
foreach (var c in Subcommands) map[c.CommandText] = c;
return field = map.AsReadOnly();
}
} = null!;

public bool Contains(string subcommandText)
{
return SubcommandMap.ContainsKey(subcommandText);
}

public static implicit operator SubcommandDefinition((string commandText, IEnumerable<SubcommandDefinition> subcommands) tuple)
{
return new SubcommandDefinition
{
CommandText = tuple.commandText,
Subcommands = tuple.subcommands.ToImmutableHashSet()
};
}

public static implicit operator SubcommandDefinition(string commandText)
{
return new SubcommandDefinition
{
CommandText = commandText,
Subcommands = ImmutableHashSet<SubcommandDefinition>.Empty
};
}
}
8 changes: 8 additions & 0 deletions PCL.Core/App/Cli/TextArgument.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PCL.Core.App.Cli;

public class TextArgument : CommandArgument<string>
{
public override ArgumentValueKind ValueKind => ArgumentValueKind.Text;

protected override string ParseValueText() => ValueText;
}
Loading
Loading