// ----------------------------------------------------------------------------- // file="XmlCli" // Project: StgSharp // Copyright (c) Nitload. // SPDX-License-Identifier: MIT // ----------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Xml; using System.Xml.Linq; using System.Xml.XPath; using StgSharp.GenerateGL.Analysis; using StgSharp.GenerateGL.Generation; using StgSharp.GenerateGL.Registry; using StgSharp.GenerateGL.Tree; namespace StgSharp.GenerateGL { internal static class XmlCli { internal const int LoadErrorExitCode = 3; private const int DefaultDepth = 2; private const int ShowAllResults = int.MaxValue; private const int ReadableDepth = 6; private const int SearchContextParentChildThreshold = 20; private const int SearchReadableDepth = 4; private const int QueryErrorExitCode = 1; private const int SuccessExitCode = 0; private const int UsageErrorExitCode = 2; public static int Run( string[] args ) { ArgumentNullException.ThrowIfNull(args); if (!TryReadInvocation(args, out CliInvocation invocation, out string? error)) { return WriteUsageError(error); } if (invocation.ShowHelp) { WriteHelp(); return SuccessExitCode; } CliSession session = LoadSession(invocation.XmlPath); if (invocation.Interactive) { return RunInteractive(session); } return ExecuteQuery(session, invocation.Command, invocation.Arguments); } private static int ExecuteQuery( CliSession session, string command, IReadOnlyList arguments ) { try { return command switch { "LIST" or "LS" when arguments.Count != 0 && string.Equals( arguments[0], "files", StringComparison.OrdinalIgnoreCase) => GenerateCli.ListFiles( session.GeneratedFiles, arguments.Skip(1).ToArray()), "SUMMARY" or "REGISTRY" or "MODEL" or "LIST" or "LS" or "FIND" or "SEARCH" or "SHOW" or "INFO" => RegistryCli.Execute( session.Registry, command, arguments), "GENERATE" or "GEN" => QueryGenerate(session, arguments), "TARGET" => TargetRegistryCli.Execute( session.TargetRegistry, arguments), "FAMILY" or "FAMILIES" => FunctionFamilyCli.Execute( session.FunctionFamilies, arguments), "PREVIEW" => GenerateCli.Preview( session.GeneratedFiles, arguments), "XML" => ExecuteXmlQuery(session.Document, arguments), "CLEAR" or "CLS" => WriteUsageError( "clear is only available in the interactive shell."), "RAW" or "TREE" or "SELECT" or "TEXT" or "ATTRIBUTES" or "ATTRS" => WriteMovedXmlQuery(command), _ => WriteUnknownQuery(command), }; } catch (XPathException exception) { return WriteUsageError($"Invalid XPath: {exception.Message}"); } catch (InvalidDataException exception) { Console.Error.WriteLine($"error: {exception.Message}"); return QueryErrorExitCode; } } private static int QueryGenerate( CliSession session, IReadOnlyList arguments ) { int result = GenerateCli.Generate( session.TargetRegistry, arguments, out GeneratedFileSet? generatedFiles); if (generatedFiles is not null) { session.GeneratedFiles = generatedFiles; } return result; } private static int ExecuteXmlQuery( XmlTreeDocument document, IReadOnlyList arguments ) { if (arguments.Count == 0) { WriteXmlHelp(); return SuccessExitCode; } string command = arguments[0].ToUpperInvariant(); string[] queryArguments = arguments.Skip(1).ToArray(); return command switch { "SUMMARY" => QueryXmlSummary(document, queryArguments), "RAW" => QueryRaw(document, queryArguments), "TREE" => QueryTree(document, queryArguments), "FIND" or "SEARCH" => QueryFind(document, queryArguments), "SHOW" => QueryShow(document, queryArguments), "SELECT" => QuerySelect(document, queryArguments), "TEXT" => QueryText(document, queryArguments), "ATTRIBUTES" or "ATTRS" => QueryAttributes( document, queryArguments), _ => WriteUsageError($"Unknown XML command '{command}'."), }; } private static int QueryFind( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadSearchOptions(arguments, out SearchOptions options, out string? error)) { return WriteUsageError(error); } SearchMatch[] matches = FindMatches(document, options).ToArray(); if (matches.Length == 0) { Console.Error.WriteLine($"No XML keys or values contain '{options.Text}'."); return QueryErrorExitCode; } int displayedCount = Math.Min(matches.Length, options.Limit); for (int index = 0; index < displayedCount; index++) { SearchMatch match = matches[index]; XElement context = FindSearchContext(match, options.ParentLevels); if (index != 0) { Console.WriteLine(); } Console.WriteLine( $"[{index + 1}] {match.Field}: {Preview(match.Value, 120)}"); Console.WriteLine($" match: {GetElementPath(match.Element)}"); if (context != match.Element) { Console.WriteLine($" context: {GetElementPath(context)}"); } WriteReadableElement( context, 0, SearchReadableDepth, ShowAllResults); } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static int QueryShow( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadSelectionOptions( "show", arguments, out SelectionOptions options, out string? error)) { return WriteUsageError(error); } XElement[] matches = SelectElements(document, options.XPath); if (matches.Length == 0) { return WriteNoMatches(options.XPath); } int displayedCount = Math.Min(matches.Length, options.Limit); for (int index = 0; index < displayedCount; index++) { XElement element = matches[index]; if (index != 0) { Console.WriteLine(); } Console.WriteLine($"[{index + 1}] {GetElementPath(element)}"); WriteReadableElement(element, 0, ReadableDepth, ShowAllResults); } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static int RunInteractive( CliSession initialSession ) { CliSession session = initialSession; Console.WriteLine($"Loaded {session.Document.SourcePath}"); Console.WriteLine("Type 'help' for commands, or 'exit' to leave."); while (true) { if (!Console.IsInputRedirected) { Console.Write($"{Path.GetFileName(session.Document.SourcePath)}> "); } string? line = Console.ReadLine(); if (line is null) { return SuccessExitCode; } if (!TrySplitCommandLine(line, out string[] values, out string? error)) { Console.Error.WriteLine($"error: {error}"); continue; } if (values.Length == 0) { continue; } string command = values[0].ToUpperInvariant(); string[] arguments = values.Skip(1).ToArray(); switch (command) { case "EXIT" or "QUIT": return SuccessExitCode; case "HELP" or "?": WriteHelp(arguments); break; case "CLEAR" or "CLS": if (arguments.Length != 0) { Console.Error.WriteLine( "error: clear does not accept arguments."); break; } ClearConsole(); break; case "LOAD" or "OPEN": if (arguments.Length != 1) { Console.Error.WriteLine("error: load requires one XML path."); break; } if (TryLoadInteractive(arguments[0], out CliSession loaded)) { session = loaded; Console.WriteLine($"Loaded {session.Document.SourcePath}"); } break; case "RELOAD": if (arguments.Length != 0) { Console.Error.WriteLine("error: reload does not accept arguments."); break; } if (TryLoadInteractive( session.Document.SourcePath, out CliSession reloaded)) { session = reloaded; Console.WriteLine($"Reloaded {session.Document.SourcePath}"); } break; default: ExecuteQuery(session, command, arguments); break; } } } private static int QueryXmlSummary( XmlTreeDocument document, IReadOnlyList arguments ) { if (!ExpectNoArguments("xml summary", arguments)) { return UsageErrorExitCode; } XElement root = document.Tree.Root!; XElement[] elements = root.DescendantsAndSelf().ToArray(); XText[] textNodes = document.Tree.DescendantNodes() .OfType() .Where(value => value is not XCData) .ToArray(); int maxDepth = elements.Max(value => value.Ancestors().Count()); Console.WriteLine("XML tree"); WriteProperty("source", document.SourcePath); WriteProperty("source characters", document.SourceText.Length); WriteProperty("root", root.Name.ToString()); WriteOptionalProperty("declaration version", document.Tree.Declaration?.Version); WriteOptionalProperty("declaration encoding", document.Tree.Declaration?.Encoding); WriteOptionalProperty("declaration standalone", document.Tree.Declaration?.Standalone); WriteProperty("elements", elements.Length); WriteProperty("attributes", elements.Sum(value => value.Attributes().Count())); WriteProperty("text nodes", textNodes.Length); WriteProperty( "non-whitespace text nodes", textNodes.Count(value => !string.IsNullOrWhiteSpace(value.Value))); WriteProperty( "CDATA nodes", document.Tree.DescendantNodes().OfType().Count()); WriteProperty( "comments", document.Tree.DescendantNodes().OfType().Count()); WriteProperty("maximum element depth", maxDepth); Console.WriteLine(" root element children"); foreach (IGrouping group in root.Elements() .GroupBy(value => value.Name.ToString()) .OrderBy( value => value.Key, StringComparer.Ordinal)) { Console.WriteLine($" {group.Key}: {group.Count()}"); } return SuccessExitCode; } private static int QueryRaw( XmlTreeDocument document, IReadOnlyList arguments ) { if (!ExpectNoArguments("raw", arguments)) { return UsageErrorExitCode; } Console.Write(document.SourceText); return SuccessExitCode; } private static int QueryTree( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadTreeOptions(arguments, out TreeOptions options, out string? error)) { return WriteUsageError(error); } XElement[] matches = SelectElements(document, options.XPath); if (matches.Length == 0) { return WriteNoMatches(options.XPath); } int displayedCount = Math.Min(matches.Length, options.Limit); for (int index = 0; index < displayedCount; index++) { XElement element = matches[index]; if (index != 0) { Console.WriteLine(); } Console.WriteLine($"[{index + 1}] {GetElementPath(element)}"); WriteTree( element, 0, options.Depth, options.Limit, options.IncludeWhitespace); } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static int QuerySelect( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadSelectionOptions( "select", arguments, out SelectionOptions options, out string? error)) { return WriteUsageError(error); } XElement[] matches = SelectElements(document, options.XPath); if (matches.Length == 0) { return WriteNoMatches(options.XPath); } int displayedCount = Math.Min(matches.Length, options.Limit); for (int index = 0; index < displayedCount; index++) { XElement element = matches[index]; if (index != 0) { Console.WriteLine(); } Console.WriteLine( $"[{index + 1}] {GetElementPath(element)} [line {ReadLine(element)}]"); Console.WriteLine(element.ToString(SaveOptions.DisableFormatting)); } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static int QueryText( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadSelectionOptions( "text", arguments, out SelectionOptions options, out string? error)) { return WriteUsageError(error); } XElement[] matches = SelectElements(document, options.XPath); if (matches.Length == 0) { return WriteNoMatches(options.XPath); } int displayedCount = Math.Min(matches.Length, options.Limit); foreach (XElement element in matches.Take(displayedCount)) { Console.WriteLine(element.Value); } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static int QueryAttributes( XmlTreeDocument document, IReadOnlyList arguments ) { if (!TryReadSelectionOptions( "attributes", arguments, out SelectionOptions options, out string? error)) { return WriteUsageError(error); } XElement[] matches = SelectElements(document, options.XPath); if (matches.Length == 0) { return WriteNoMatches(options.XPath); } int displayedCount = Math.Min(matches.Length, options.Limit); for (int index = 0; index < displayedCount; index++) { XElement element = matches[index]; Console.WriteLine( $"[{index + 1}] {GetElementPath(element)} [line {ReadLine(element)}]"); XAttribute[] attributes = element.Attributes().ToArray(); if (attributes.Length == 0) { Console.WriteLine(" (no attributes)"); continue; } foreach (XAttribute attribute in attributes) { Console.WriteLine($" @{attribute.Name} = {attribute.Value}"); } } WriteTruncation(matches.Length, displayedCount); return SuccessExitCode; } private static XElement[] SelectElements( XmlTreeDocument document, string xpath ) { return document.Tree.XPathSelectElements(xpath).ToArray(); } private static IEnumerable FindMatches( XmlTreeDocument document, SearchOptions options ) { XElement root = document.Tree.Root!; foreach (XElement element in root.DescendantsAndSelf()) { if (MatchesSearch(element.Name.LocalName, options)) { yield return new SearchMatch(element, "element", element.Name.LocalName); continue; } XAttribute? attribute = element.Attributes().FirstOrDefault(value => MatchesSearch(value.Name.LocalName, options) || MatchesSearch(value.Value, options)); if (attribute is not null) { yield return new SearchMatch( element, $"@{attribute.Name.LocalName}", attribute.Value); continue; } string text = ReadDirectText(element); if (text.Length != 0 && MatchesSearch(text, options)) { yield return new SearchMatch(element, "$text", text); } } } private static bool MatchesSearch( string value, SearchOptions options ) { return options.Exact ? string.Equals(value, options.Text, StringComparison.OrdinalIgnoreCase) : value.Contains(options.Text, StringComparison.OrdinalIgnoreCase); } private static XElement FindSearchContext( SearchMatch match, int? parentLevels ) { XElement context = match.Element; int levels = parentLevels ?? (match.Field == "$text" ? 2 : 0); for (int index = 0; index < levels; index++) { if (context.Parent is not XElement parent) { break; } if (parentLevels is null && parent.Elements() .Take( SearchContextParentChildThreshold + 1) .Count() > SearchContextParentChildThreshold) { break; } context = parent; } return context; } private static void WriteReadableElement( XElement element, int depth, int maximumDepth, int childLimit ) { string padding = new string(' ', depth * 2); Console.WriteLine($"{padding}{element.Name.LocalName}: # line {ReadLine(element)}"); foreach (XAttribute attribute in element.Attributes()) { Console.WriteLine( $"{padding} @{attribute.Name.LocalName}: {Preview(attribute.Value, 200)}"); } string directText = ReadDirectText(element); if (directText.Length != 0) { Console.WriteLine($"{padding} $text: {Preview(directText, 200)}"); } foreach (XComment comment in element.Nodes().OfType()) { Console.WriteLine($"{padding} $comment: {Preview(comment.Value.Trim(), 200)}"); } XElement[] children = element.Elements().ToArray(); if (children.Length == 0) { return; } if (depth >= maximumDepth) { Console.WriteLine($"{padding} ...: {children.Length} child element(s)"); return; } Dictionary totals = children.GroupBy(value => value.Name) .ToDictionary( value => value.Key, value => value.Count()); Dictionary indices = new Dictionary(); int displayedCount = Math.Min(children.Length, childLimit); foreach (XElement child in children.Take(displayedCount)) { indices.TryGetValue(child.Name, out int currentIndex); currentIndex++; indices[child.Name] = currentIndex; string name = totals[child.Name] == 1 ? child.Name.LocalName : $"{child.Name.LocalName}[{currentIndex}]"; if (!child.HasElements && !child.HasAttributes) { Console.WriteLine( $"{padding} {name}: {Preview(child.Value.Trim(), 200)}"); continue; } WriteReadableChild(child, name, depth + 1, maximumDepth, childLimit); } if (displayedCount != children.Length) { Console.WriteLine( $"{padding} ...: {children.Length - displayedCount} more child element(s)"); } } private static void WriteReadableChild( XElement element, string name, int depth, int maximumDepth, int childLimit ) { string padding = new string(' ', depth * 2); Console.WriteLine($"{padding}{name}: # line {ReadLine(element)}"); foreach (XAttribute attribute in element.Attributes()) { Console.WriteLine( $"{padding} @{attribute.Name.LocalName}: {Preview(attribute.Value, 200)}"); } string directText = ReadDirectText(element); if (directText.Length != 0) { Console.WriteLine($"{padding} $text: {Preview(directText, 200)}"); } XElement[] children = element.Elements().ToArray(); if (children.Length == 0) { return; } if (depth >= maximumDepth) { Console.WriteLine($"{padding} ...: {children.Length} child element(s)"); return; } Dictionary totals = children.GroupBy(value => value.Name) .ToDictionary( value => value.Key, value => value.Count()); Dictionary indices = new Dictionary(); int displayedCount = Math.Min(children.Length, childLimit); foreach (XElement child in children.Take(displayedCount)) { indices.TryGetValue(child.Name, out int currentIndex); currentIndex++; indices[child.Name] = currentIndex; string childName = totals[child.Name] == 1 ? child.Name.LocalName : $"{child.Name.LocalName}[{currentIndex}]"; if (!child.HasElements && !child.HasAttributes) { Console.WriteLine( $"{padding} {childName}: {Preview(child.Value.Trim(), 200)}"); continue; } WriteReadableChild( child, childName, depth + 1, maximumDepth, childLimit); } if (displayedCount != children.Length) { Console.WriteLine( $"{padding} ...: {children.Length - displayedCount} more child element(s)"); } } private static string ReadDirectText( XElement element ) { return string.Join( ' ', element.Nodes() .OfType() .Where(value => value is not XCData && !string.IsNullOrWhiteSpace(value.Value)) .Select(value => value.Value.Trim())); } private static void WriteTree( XElement element, int depth, int maximumDepth, int childLimit, bool includeWhitespace ) { string padding = new string(' ', depth * 2); string attributes = string.Join( string.Empty, element.Attributes().Select(value => $" {value.Name}=\"{Preview(value.Value, 80)}\"")); Console.WriteLine( $"{padding}<{element.Name}{attributes}> [line {ReadLine(element)}]"); XNode[] children = element.Nodes() .Where(value => IsVisible(value, includeWhitespace)) .ToArray(); if (children.Length == 0) { return; } if (depth >= maximumDepth) { Console.WriteLine($"{padding} ... {children.Length} child node(s)"); return; } int displayedCount = Math.Min(children.Length, childLimit); foreach (XNode child in children.Take(displayedCount)) { switch (child) { case XElement childElement: WriteTree( childElement, depth + 1, maximumDepth, childLimit, includeWhitespace); break; case XCData cdata: WriteNodeValue("#cdata", cdata.Value, depth + 1); break; case XText text: WriteNodeValue("#text", text.Value, depth + 1); break; case XComment comment: WriteNodeValue("#comment", comment.Value, depth + 1); break; case XProcessingInstruction instruction: WriteNodeValue( $"?{instruction.Target}", instruction.Data, depth + 1); break; } } if (displayedCount != children.Length) { Console.WriteLine( $"{padding} ... {children.Length - displayedCount} more child node(s)"); } } private static bool IsVisible( XNode node, bool includeWhitespace ) { return includeWhitespace || node is not XText text || !string.IsNullOrWhiteSpace(text.Value); } private static void WriteNodeValue( string kind, string value, int depth ) { string padding = new string(' ', depth * 2); Console.WriteLine($"{padding}{kind} \"{Preview(value, 120)}\""); } private static string Preview( string value, int maximumLength ) { string escaped = value.Replace("\r", "\\r", StringComparison.Ordinal) .Replace("\n", "\\n", StringComparison.Ordinal) .Replace("\t", "\\t", StringComparison.Ordinal) .Replace("\"", "\\\"", StringComparison.Ordinal); return escaped.Length <= maximumLength ? escaped : $"{escaped[..maximumLength]}..."; } private static string GetElementPath( XElement element ) { List segments = new List(); foreach (XElement current in element.AncestorsAndSelf().Reverse()) { string segment = current.Name.LocalName; if (current.Parent is XElement parent) { XElement[] siblings = parent.Elements(current.Name).ToArray(); if (siblings.Length > 1) { int index = Array.IndexOf(siblings, current) + 1; segment = $"{segment}[{index}]"; } } segments.Add(segment); } return $"/{string.Join('/', segments)}"; } private static int ReadLine( XObject value ) { return value is IXmlLineInfo lineInfo && lineInfo.HasLineInfo() ? lineInfo.LineNumber : 0; } private static bool TryLoadInteractive( string path, out CliSession session ) { try { session = LoadSession(path); return true; } catch (IOException exception) { return WriteLoadError(exception, out session); } catch (XmlException exception) { return WriteLoadError(exception, out session); } catch (InvalidDataException exception) { return WriteLoadError(exception, out session); } catch (UnauthorizedAccessException exception) { return WriteLoadError(exception, out session); } catch (ArgumentException exception) { return WriteLoadError(exception, out session); } catch (NotSupportedException exception) { return WriteLoadError(exception, out session); } } private static CliSession LoadSession( string path ) { XmlTreeDocument document = XmlTreeReader.Load(path); GlRegistryModel registry = GlRegistryReader.Read(document); return new CliSession(document, registry); } private static bool WriteLoadError( Exception exception, out CliSession session ) { session = null!; Console.Error.WriteLine($"error: {exception.Message}"); return false; } private static void ClearConsole() { if (Console.IsOutputRedirected) { return; } try { Console.Clear(); } catch (IOException exception) { Console.Error.WriteLine($"error: {exception.Message}"); } catch (PlatformNotSupportedException exception) { Console.Error.WriteLine($"error: {exception.Message}"); } } private static bool TrySplitCommandLine( string line, out string[] values, out string? error ) { List result = new List(); StringBuilder current = new StringBuilder(); bool hasToken = false; bool isQuoted = false; for (int index = 0; index < line.Length; index++) { char value = line[index]; if (value == '"') { isQuoted = !isQuoted; hasToken = true; continue; } if (value == '\\' && isQuoted && index + 1 < line.Length && line[index + 1] == '"') { current.Append('"'); index++; hasToken = true; continue; } if (char.IsWhiteSpace(value) && !isQuoted) { if (hasToken) { result.Add(current.ToString()); current.Clear(); hasToken = false; } continue; } current.Append(value); hasToken = true; } if (isQuoted) { values = Array.Empty(); error = "Unterminated double quote."; return false; } if (hasToken) { result.Add(current.ToString()); } values = result.ToArray(); error = null; return true; } private static bool TryReadInvocation( string[] args, out CliInvocation invocation, out string? error ) { string xmlPath = Path.Combine(AppContext.BaseDirectory, "xml", "gl.xml"); List remaining = new List(args.Length); for (int index = 0; index < args.Length; index++) { string argument = args[index]; if (argument == "--xml") { if (++index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal)) { invocation = null!; error = "--xml requires a file path."; return false; } xmlPath = args[index]; continue; } remaining.Add(argument); } bool showHelp = remaining.Count == 1 && (remaining[0] is "--help" or "-h" || string.Equals( remaining[0], "help", StringComparison.OrdinalIgnoreCase)); bool interactive = remaining.Count == 0 || remaining.Count == 1 && (string.Equals( remaining[0], "shell", StringComparison.OrdinalIgnoreCase) || string.Equals( remaining[0], "repl", StringComparison.OrdinalIgnoreCase)); invocation = new CliInvocation( xmlPath, showHelp || interactive ? string.Empty : remaining[0].ToUpperInvariant(), showHelp || interactive ? Array.Empty() : remaining.Skip(1).ToArray(), showHelp, interactive); error = null; return true; } private static bool TryReadTreeOptions( IReadOnlyList arguments, out TreeOptions options, out string? error ) { string xpath = "/*"; int depth = DefaultDepth; int limit = ShowAllResults; bool includeWhitespace = false; int index = 0; if (arguments.Count != 0 && !arguments[0].StartsWith("--", StringComparison.Ordinal)) { xpath = arguments[0]; index++; } while (index < arguments.Count) { string argument = arguments[index++]; if (argument == "--whitespace") { includeWhitespace = true; continue; } if (argument == "--depth" && TryReadNonNegativeInt( arguments, ref index, out depth)) { continue; } if ((argument == "--showcount" || argument == "--limit") && TryReadPositiveInt( arguments, ref index, out limit)) { continue; } options = null!; error = $"Unknown or invalid tree option '{argument}'."; return false; } options = new TreeOptions(xpath, depth, limit, includeWhitespace); error = null; return true; } private static bool TryReadSearchOptions( IReadOnlyList arguments, out SearchOptions options, out string? error ) { if (arguments.Count == 0 || arguments[0].StartsWith("--", StringComparison.Ordinal)) { options = null!; error = "find requires a search string."; return false; } string text = arguments[0]; int limit = ShowAllResults; int? parentLevels = null; bool exact = false; int index = 1; while (index < arguments.Count) { string argument = arguments[index++]; if (argument == "--exact") { exact = true; continue; } if ((argument == "--showcount" || argument == "--limit") && TryReadPositiveInt( arguments, ref index, out limit)) { continue; } if (argument == "--up" && TryReadNonNegativeInt( arguments, ref index, out int levels)) { parentLevels = levels; continue; } options = null!; error = $"Unknown or invalid find option '{argument}'."; return false; } options = new SearchOptions(text, limit, parentLevels, exact); error = null; return true; } private static bool TryReadSelectionOptions( string query, IReadOnlyList arguments, out SelectionOptions options, out string? error ) { if (arguments.Count == 0 || arguments[0].StartsWith("--", StringComparison.Ordinal)) { options = null!; error = $"{query} requires an XPath expression."; return false; } string xpath = arguments[0]; int limit = ShowAllResults; int index = 1; while (index < arguments.Count) { string argument = arguments[index++]; if ((argument == "--showcount" || argument == "--limit") && TryReadPositiveInt( arguments, ref index, out limit)) { continue; } options = null!; error = $"Unknown or invalid {query} option '{argument}'."; return false; } options = new SelectionOptions(xpath, limit); error = null; return true; } private static bool TryReadPositiveInt( IReadOnlyList arguments, ref int index, out int value ) { return TryReadInt(arguments, ref index, out value) && value > 0; } private static bool TryReadNonNegativeInt( IReadOnlyList arguments, ref int index, out int value ) { return TryReadInt(arguments, ref index, out value) && value >= 0; } private static bool TryReadInt( IReadOnlyList arguments, ref int index, out int value ) { if (index >= arguments.Count) { value = 0; return false; } return int.TryParse( arguments[index++], NumberStyles.None, CultureInfo.InvariantCulture, out value); } private static bool ExpectNoArguments( string query, IReadOnlyCollection arguments ) { if (arguments.Count == 0) { return true; } Console.Error.WriteLine($"error: {query} does not accept arguments."); return false; } private static int WriteNoMatches( string xpath ) { Console.Error.WriteLine($"XPath selected no elements: {xpath}"); return QueryErrorExitCode; } private static int WriteUnknownQuery( string query ) { Console.Error.WriteLine($"error: unknown query '{query}'."); Console.Error.WriteLine( "Run with --help, or type help in the interactive shell."); return UsageErrorExitCode; } private static int WriteMovedXmlQuery( string query ) { string command = query switch { "RAW" => "raw", "TREE" => "tree", "SELECT" => "select", "TEXT" => "text", "ATTRIBUTES" or "ATTRS" => "attributes", _ => query, }; Console.Error.WriteLine( $"error: '{command}' is an XML diagnostic command; use 'xml {command}'."); return UsageErrorExitCode; } private static int WriteUsageError( string? error ) { Console.Error.WriteLine($"error: {error}"); Console.Error.WriteLine( "Run with --help, or type help in the interactive shell."); return UsageErrorExitCode; } private static void WriteTruncation( int totalCount, int displayedCount ) { if (displayedCount != totalCount) { Console.WriteLine($"... {totalCount - displayedCount} more match(es)"); } } private static void WriteProperty( string name, object value ) { Console.WriteLine($" {name}: {value}"); } private static void WriteOptionalProperty( string name, string? value ) { if (value is not null) { WriteProperty(name, value); } } private static void WriteHelp( string[]? arguments = null ) { if (arguments is { Length: 1 } && string.Equals( arguments[0], "generate", StringComparison.OrdinalIgnoreCase)) { GenerateCli.WriteHelp(); return; } if (arguments is { Length: 1 } && ( string.Equals( arguments[0], "family", StringComparison.OrdinalIgnoreCase) || string.Equals( arguments[0], "families", StringComparison.OrdinalIgnoreCase))) { FunctionFamilyCli.WriteHelp(); return; } if (arguments is { Length: > 0 }) { Console.Error.WriteLine( "error: help accepts only the 'generate' or 'families' topic."); return; } Console.WriteLine("Inspect the structural OpenGL registry model."); Console.WriteLine(); Console.WriteLine("Usage:"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] # interactive shell"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] shell"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] summary"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] list " + "[filter] [--showcount ]"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] find " + "[--exact] [--showcount ]"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] show " + "[--showcount ]"); Console.WriteLine( " StgSharp.GenerateGL [--xml ] generate [output-directory]"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] target summary"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] families "); Console.WriteLine(" StgSharp.GenerateGL [--xml ] list files"); Console.WriteLine(" StgSharp.GenerateGL [--xml ] preview "); Console.WriteLine(" StgSharp.GenerateGL [--xml ] xml [...]"); Console.WriteLine(); Console.WriteLine("Registry categories:"); Console.WriteLine( " types, kinds, groups, enums, commands, features, extensions"); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" StgSharp.GenerateGL summary"); Console.WriteLine(" StgSharp.GenerateGL list commands BindBuffer"); Console.WriteLine(" StgSharp.GenerateGL find glBindBuffer"); Console.WriteLine(" StgSharp.GenerateGL show command glBindBuffer"); Console.WriteLine(" StgSharp.GenerateGL show extension GL_KHR_debug"); Console.WriteLine(" StgSharp.GenerateGL target summary"); Console.WriteLine(" StgSharp.GenerateGL families show VertexAttribP"); Console.WriteLine(" StgSharp.GenerateGL generate all"); Console.WriteLine( " StgSharp.GenerateGL generate all src/StgSharp.Graphics/OpenGL/Generated"); Console.WriteLine(" StgSharp.GenerateGL list files"); Console.WriteLine(" StgSharp.GenerateGL preview glconst.cs"); Console.WriteLine(" StgSharp.GenerateGL xml tree --depth 1"); Console.WriteLine(); Console.WriteLine("Interactive commands:"); Console.WriteLine(" help, load , reload, clear, exit"); Console.WriteLine(); Console.WriteLine("Use 'xml' to list the raw XML diagnostic commands."); Console.WriteLine("Registry values and XML attributes remain strings."); Console.WriteLine("The default XML is copied beside the executable at xml/gl.xml."); } private static void WriteXmlHelp() { Console.WriteLine("Raw XML diagnostic commands:"); Console.WriteLine(" xml summary"); Console.WriteLine(" xml raw"); Console.WriteLine( " xml tree [xpath] [--depth ] [--showcount ] [--whitespace]"); Console.WriteLine( " xml find [--exact] [--up ] [--showcount ]"); Console.WriteLine(" xml show [--showcount ]"); Console.WriteLine(" xml select [--showcount ]"); Console.WriteLine(" xml text [--showcount ]"); Console.WriteLine(" xml attributes [--showcount ]"); } private sealed class CliSession { public CliSession( XmlTreeDocument document, GlRegistryModel registry ) { Document = document; Registry = registry; TargetRegistry = DesktopGlCoreRegistryFilter.Apply(registry); FunctionFamilies = GlFunctionFamilyAnalyzer.Analyze(TargetRegistry); } public XmlTreeDocument Document { get; } public GlRegistryModel Registry { get; } public GlRegistryProjection TargetRegistry { get; } public GlFunctionFamilyAnalysis FunctionFamilies { get; } public GeneratedFileSet GeneratedFiles { get; set; } = GeneratedFileSet.Empty; } private sealed record CliInvocation( string XmlPath, string Command, IReadOnlyList Arguments, bool ShowHelp, bool Interactive ); private sealed record SelectionOptions( string XPath, int Limit ); private sealed record SearchMatch( XElement Element, string Field, string Value ); private sealed record SearchOptions( string Text, int Limit, int? ParentLevels, bool Exact ); private sealed record TreeOptions( string XPath, int Depth, int Limit, bool IncludeWhitespace ); } }