using System.Collections.Generic; using System.Text; namespace Unity.Pipeline { /// /// Splits a raw command line into argv tokens for /api/exec's commandLine form. /// /// The dialect is POSIX-ish and is the pipeline's own — not the caller's shell, and /// identical on every OS, so the same string tokenizes the same way from cmd.exe, bash, an /// MCP client or a chat box. It is documented as a versioned contract in /// Documentation~/connectivity.md; CommandLineTokenizerTests pins it. /// /// Rules: whitespace separates tokens; "…" groups with \ escaping the next /// character; '…' groups with no escapes at all; \ escapes outside /// quotes; adjacent spans concatenate (a"b c"d → ab cd); "" yields one /// empty token. There is no expansion of any kind — no $VAR, globbing, ~, /// comments or operators; && and | are ordinary characters. /// /// POSIX-ish rather than Windows CommandLineToArgvW because single quotes are /// load-bearing here — eval 'obj.name = "x";' and batch --operations '[{…}]' /// need a form that passes double quotes through untouched, and Windows semantics have no /// single-quote concept. The usual argument for Windows rules (\ is the path /// separator) does not apply: every path this API accepts is a forward-slashed Unity asset /// path. /// internal static class CommandLineTokenizer { /// /// Tokenizes . Returns false and sets /// on an unbalanced quote or a dangling backslash. /// A null, empty or all-whitespace input succeeds with zero tokens — a lexer has no /// opinion about needing a command name, so the caller reports that (keeping the message /// identical to the structured path). /// public static bool TryTokenize(string commandLine, out List tokens, out string error) { tokens = new List(); error = null; if (string.IsNullOrEmpty(commandLine)) return true; var current = new StringBuilder(); // Distinguishes `""` (one empty token) from whitespace (no token at all). var hasToken = false; var inSingle = false; var inDouble = false; for (var i = 0; i < commandLine.Length; i++) { var c = commandLine[i]; if (inSingle) { if (c == '\'') inSingle = false; else current.Append(c); continue; } if (inDouble) { if (c == '"') { inDouble = false; } else if (c == '\\') { if (i + 1 >= commandLine.Length) { error = "Dangling backslash at end of command line"; return false; } current.Append(commandLine[++i]); } else { current.Append(c); } continue; } if (c == '\'') { inSingle = true; hasToken = true; } else if (c == '"') { inDouble = true; hasToken = true; } else if (c == '\\') { if (i + 1 >= commandLine.Length) { error = "Dangling backslash at end of command line"; return false; } current.Append(commandLine[++i]); hasToken = true; } else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') { if (hasToken) { tokens.Add(current.ToString()); current.Length = 0; hasToken = false; } } else { current.Append(c); hasToken = true; } } if (inSingle) { error = "Unbalanced single quote in command line"; return false; } if (inDouble) { error = "Unbalanced double quote in command line"; return false; } if (hasToken) tokens.Add(current.ToString()); return true; } } }