private static Token NextTokenFromArg(string s, ref int idx) { switch (state) { case ScannerState.NeedsNextBare: state = ScannerState.InBareArg; break; case ScannerState.NeedsNextQuote: tokenBuilder.Append(' '); state = ScannerState.InQuoteString; break; case ScannerState.InShortArg: tokenBuilder.Length = 0; break; default: state = ScannerState.Unstarted; tokenBuilder.Length = 0; break; } Token token; while (idx < s.Length) { char c = s[idx]; switch (state) { case ScannerState.Unstarted: switch (c) { case tokenOptionChar: state = ScannerState.InOptionToken; idx++; break; case doubleQuoteChar: case singleQuoteChar: idx++; state = ScannerState.InQuoteString; break; default: state = ScannerState.InBareArg; break; } break; case ScannerState.InOptionToken: switch (c) { case tokenOptionChar: state = ScannerState.InLongArg; idx++; break; default: if (Char.IsWhiteSpace(c)) { // If we get a single '-' followed by whitespace, figure it's a bare arg // (like a stdin placeholder). idx++; return(new Token(TokenType.BareArg, c)); } else { state = ScannerState.InShortArg; } break; } break; case ScannerState.InShortArg: if (Char.IsLetterOrDigit(c)) { idx++; return(new Token(TokenType.ShortOption, c)); } else { throw new Exception(string.Format( "Unexpected character '{0}' at position {1}. Short options must be letters or digits.", c, idx )); } case ScannerState.InLongArg: if (Char.IsWhiteSpace(c) || c == assignmentOpChar) { idx++; // The token is done, return it. token = new Token(TokenType.LongOption, tokenBuilder.ToString()); tokenBuilder.Length = 0; return(token); } else { // Keep adding string data to the token. tokenBuilder.Append(c); idx++; } break; case ScannerState.InQuoteString: switch (c) { case doubleQuoteChar: case singleQuoteChar: idx++; return(new Token(TokenType.BareArg, tokenBuilder.ToString())); case escapeNextChar: state = ScannerState.InEscapeChar; escapeReturnState = ScannerState.InQuoteString; break; default: idx++; tokenBuilder.Append(c); break; } break; case ScannerState.InBareArg: switch (c) { case escapeNextChar: state = ScannerState.InEscapeChar; escapeReturnState = ScannerState.InBareArg; break; default: // Keep adding string data to the token until the end of the arg. tokenBuilder.Append(c); idx++; break; } break; case ScannerState.InEscapeChar: string escapeSeq = s.SafeSubstring(idx, 5); if (idx == (s.Length - 1)) { escapeSeq = string.Concat(escapeSeq, ' '); state = ScannerState.NeedsNextBare; } else { state = escapeReturnState; } try { int unescapedLen = escapeSeq.Length; escapeSeq = Regex.Unescape(escapeSeq); int escapedLen = unescapedLen - escapeSeq.Length; idx += escapedLen; tokenBuilder.Append(escapeSeq[0]); } catch (ArgumentException) { tokenBuilder.Append(c); } idx++; break; } } // If we get here, we've hit the end of the string. // If the token builder has zero length, something is malformed. if (tokenBuilder.Length < 1) { throw new Exception(string.Format("Unexpected end of arg while parsing {0}.", state.ToString())); } switch (state) { case ScannerState.InQuoteString: state = ScannerState.NeedsNextQuote; return(new Token()); case ScannerState.NeedsNextBare: return(new Token()); case ScannerState.InBareArg: return(new Token(TokenType.BareArg, tokenBuilder.ToString())); case ScannerState.InShortArg: return(new Token(TokenType.ShortOption, tokenBuilder.ToString())); case ScannerState.InLongArg: return(new Token(TokenType.LongOption, tokenBuilder.ToString())); default: throw new Exception(string.Format("Unexpected end of arg while parsing {0}.", state.ToString())); } }