using System; using System.Collections.Generic; using System.Globalization; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace PCL.Core.App.Cli; /// /// 命令行模型 /// [JsonConverter(typeof(CommandLineJsonConverter))] public class CommandLine { /// /// 命令文本 /// public required string CommandText { get; init; } /// /// 子命令 /// public CommandLine? Subcommand { get; init; } = null; /// /// 子命令文本 /// public string? SubcommandText => Subcommand?.CommandText; /// /// 参数字典 /// public required IReadOnlyDictionary Arguments { get; init; } /// /// 尝试获取参数值 /// /// 参数键 /// 参数值,若获取失败则为对应类型默认值 /// 参数值的类型 /// 是否存在该键; 存在该键时值的类型是否匹配 public (bool exists, bool isTypeMatch) TryGetArgumentValue(string key, out TValue? value) { var exists = Arguments.TryGetValue(key, out var arg); var isTypeMatch = false; if (exists && (isTypeMatch = arg!.TryCastValue(out TValue? typedValue))) { value = typedValue; return (true, true); } value = default; return (exists, isTypeMatch); } /// /// 解析参数数组,第一个元素会被视为主命令 /// /// 参数数组 /// 各级子命令列表 /// 命令行模型实例 public static CommandLine Parse(ReadOnlySpan args, IEnumerable? subcommands = null) { subcommands ??= []; SubcommandDefinition root = (args[0], subcommands); return CommandLineParser.Parse(args, root); } public override string ToString() { var sb = new StringBuilder(); sb.Append(CommandText).Append(" ["); if (Arguments.Count > 0) sb.AppendLine(); foreach (var arg in Arguments.Values) { sb.Append(" --").Append(arg.Key); var value = arg.ValueKind switch { ArgumentValueKind.Bool => arg.CastValue() ? "true" : "false", ArgumentValueKind.Decimal => arg.CastValue().ToString(CultureInfo.InvariantCulture), ArgumentValueKind.Text => arg.ValueText, _ => null }; if (value is not null) sb.Append(": ").Append(value); sb.AppendLine(); } sb.Append(']'); if (Subcommand is not null) sb.AppendLine().Append("-> ").Append(Subcommand.ToString().Replace("\n", "\n ")); return sb.ToString(); } } file static class CommandLineParser { private static (CommandArgument, bool) _ParseArgument(string key, string possibleValueText) { if (key.StartsWith("--")) key = key[2..]; 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 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(); 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) || (subcommands.Contains(args[i + 1])) ? "" : args[i + 1]); argumentList[commandArgument.Key] = commandArgument; i += hasValueText ? 2 : 1; } return new CommandLine { CommandText = commandText, Arguments = argumentList.AsReadOnly(), Subcommand = subcommand }; } } /// /// 用于 的 JSON 转换器 /// // Generated by gpt-5.3-codex (20260218) public sealed class CommandLineJsonConverter : JsonConverter { public override CommandLine? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) return null; if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected object for CommandLine."); string? commandText = null; CommandLine? subcommand = null; var arguments = new Dictionary(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) break; if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected property name."); var propName = reader.GetString(); if (!reader.Read()) throw new JsonException("Unexpected end of json."); switch (propName) { case "cmd": commandText = reader.GetString() ?? throw new JsonException("cmd cannot be null."); break; case "sub": subcommand = JsonSerializer.Deserialize(ref reader, options); break; case "args": _ReadArguments(ref reader, arguments); break; default: // 忽略未知字段,提升前向兼容 reader.Skip(); break; } } if (string.IsNullOrEmpty(commandText)) throw new JsonException("Missing required property: cmd."); return new CommandLine { CommandText = commandText, Arguments = arguments.AsReadOnly(), Subcommand = subcommand }; } public override void Write(Utf8JsonWriter writer, CommandLine value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteString("cmd", value.CommandText); if (value.Arguments.Count > 0) { writer.WritePropertyName("args"); writer.WriteStartArray(); foreach (var arg in value.Arguments.Values) { writer.WriteStartObject(); writer.WriteString("k", arg.Key); writer.WriteNumber("t", (int)arg.ValueKind); writer.WriteString("v", arg.ValueText); writer.WriteEndObject(); } writer.WriteEndArray(); } if (value.Subcommand is not null) { writer.WritePropertyName("sub"); JsonSerializer.Serialize(writer, value.Subcommand, options); } writer.WriteEndObject(); } private static void _ReadArguments(ref Utf8JsonReader reader, Dictionary target) { if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("args must be an array."); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndArray) return; if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Argument item must be an object."); string? key = null; ArgumentValueKind? kind = null; string? valueText = null; while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndObject) break; if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected argument property name."); var name = reader.GetString(); if (!reader.Read()) throw new JsonException("Unexpected end of json."); switch (name) { case "k": key = reader.GetString(); break; case "t": kind = (ArgumentValueKind)reader.GetInt32(); break; case "v": valueText = reader.GetString() ?? string.Empty; break; default: reader.Skip(); break; } } if (string.IsNullOrEmpty(key)) throw new JsonException("Argument missing key(k)."); if (kind is null) throw new JsonException("Argument missing type(t)."); valueText ??= string.Empty; CommandArgument arg = kind.Value switch { ArgumentValueKind.Bool => new BoolArgument { Key = key, ValueText = valueText }, ArgumentValueKind.Decimal => new DecimalArgument { Key = key, ValueText = valueText }, ArgumentValueKind.Text => new TextArgument { Key = key, ValueText = valueText }, _ => throw new JsonException($"Unsupported argument type: {(int)kind.Value}") }; target[arg.Key] = arg; } throw new JsonException("args array is not closed."); } }