Dependencies/Pixie/Pixie/Options/GnuOptionSetParser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using WasmBox.Pixie.Markup;
namespace WasmBox.Pixie.Options {
/// <summary>
/// A parser for GNU-style option sets.
/// </summary>
public sealed class GnuOptionSetParser : OptionSetParser {
/// <summary>
/// Creates a GNU-style option parser from a list of
/// options. The option parser will reject positional
/// arguments.
/// </summary>
/// <param name="options">
/// A list of all options known to this parser.
/// </param>
public GnuOptionSetParser(IReadOnlyList<Option> options)
: this(
options,
new KeyValuePair<Option, OptionForm>[] { }) { }
/// <summary>
/// Creates a GNU-style option parser from a list of
/// options and a default option.
/// </summary>
/// <param name="options">
/// A list of all options known to this parser.
/// </param>
/// <param name="defaultOption">
/// The default option: the option that
/// is implicitly parsed when no other option
/// can accept arguments. Its first form is
/// used to parse arguments.
/// </param>
public GnuOptionSetParser(
IReadOnlyList<Option> options,
Option defaultOption)
: this(
options,
defaultOption,
defaultOption.Forms[0]) { }
/// <summary>
/// Creates a GNU-style option parser from a list of
/// options, a default option and a preferred form
/// for that default option.
/// </summary>
/// <param name="options">
/// A list of all options known to this parser.
/// </param>
/// <param name="defaultOption">
/// The default option: the option that
/// is implicitly parsed when no other option
/// can accept arguments.
/// </param>
/// <param name="defaultForm">
/// The preferred form for the default option.
/// </param>
public GnuOptionSetParser(
IReadOnlyList<Option> options,
Option defaultOption,
OptionForm defaultForm)
: this(
options,
new KeyValuePair<Option, OptionForm>[] {
new KeyValuePair<Option, OptionForm>(defaultOption, defaultForm)
}) { }
/// <summary>
/// Creates a GNU-style option parser from a list of
/// options and a list of positional argument options.
/// </summary>
/// <param name="options">
/// A list of all options known to this parser.
/// </param>
/// <param name="positionalOptions">
/// A list of (Option, OptionForm) pairs that parse
/// positional arguments.
/// </param>
public GnuOptionSetParser(
IReadOnlyList<Option> options,
IReadOnlyList<KeyValuePair<Option, OptionForm>> positionalOptions) {
this.Options = options;
this.PositionalOptions = positionalOptions;
this.shortForms = new TrieNode<char, Option>();
this.longForms = new Dictionary<string, Option>();
this.allForms = new Lazy<string[]>(ComputeAllForms);
PopulateDataStructures();
}
/// <summary>
/// Gets a list of all options known to this parser.
/// </summary>
/// <returns>A list of options.</returns>
public IReadOnlyList<Option> Options { get; private set; }
/// <summary>
/// Gets a list of (Option, OptionForm) pairs that parse
/// positional arguments.
/// </summary>
/// <returns>The positional options.</returns>
public IReadOnlyList<KeyValuePair<Option, OptionForm>> PositionalOptions { get; private set; }
// A trie of short option forms.
internal TrieNode<char, Option> shortForms;
// A mapping of long form strings to options.
internal Dictionary<string, Option> longForms;
// A lazily computed array of all possible option forms, as strings.
// This is used for guessing what the user meant when they type an
// option that doesn't actually exist.
internal Lazy<string[]> allForms;
/// <inheritdoc/>
public override OptionSet Parse(
IReadOnlyList<string> arguments,
ILog log) {
var state = new GnuOptionSetParserState(this, log);
for (int i = PositionalOptions.Count - 1; i >= 0; i--) {
state.StartParsing(PositionalOptions[i].Key, PositionalOptions[i].Value);
}
// Drip-feed the arguments to the state.
int argCount = arguments.Count;
for (int i = 0; i < argCount; i++) {
// Parse an argument.
if (!state.Parse(arguments[i])) {
log.Log(
new LogEntry(
Severity.Error,
"meaningless argument",
new Text("cannot assign a meaning to excessive command-line argument "),
Quotation.CreateBoldQuotation(arguments[i]),
new Text(".")));
break;
}
}
// Finish parsing all options.
state.FinishParsingAll();
return new OptionSet(state.ParsedOptions);
}
private void PopulateDataStructures() {
var optCount = Options.Count;
for (int i = 0; i < optCount; i++) {
var opt = Options[i];
var optForms = opt.Forms;
int formCount = optForms.Count;
for (int j = 0; j < formCount; j++) {
var form = optForms[j];
if (form.IsShort) {
shortForms.AddPath(form.Name, opt);
}
else {
longForms[form.Name] = opt;
}
}
}
}
private string[] ComputeAllForms() {
var results = new List<string>();
var optionCount = Options.Count;
for (int i = 0; i < optionCount; i++) {
var forms = Options[i].Forms;
int formCount = forms.Count;
for (int j = 0; j < formCount; j++) {
results.Add(forms[j].ToString());
}
}
return results.ToArray();
}
}
internal struct GnuOptionSetParserState {
public GnuOptionSetParserState(GnuOptionSetParser parser, ILog log) {
this = default(GnuOptionSetParserState);
this.Parser = parser;
this.Log = log;
this.parsedOpts = new Dictionary<Option, ParsedOption>();
this.parseStack = new Stack<GnuOptionParseState>();
this.endOfOptionsReached = false;
}
/// <summary>
/// Gets the parser that created this state.
/// </summary>
/// <returns>The parser.</returns>
public GnuOptionSetParser Parser { get; private set; }
/// <summary>
/// Gets a log to which messages can be sent while parsing
/// options.
/// </summary>
/// <returns>A log.</returns>
public ILog Log { get; private set; }
/// <summary>
/// Gets the options that have been parsed by this state.
/// </summary>
/// <returns>The options that have been parsed by this state.</returns>
public IReadOnlyDictionary<Option, ParsedOption> ParsedOptions => parsedOpts;
// A dictionary of options that have been parsed.
private Dictionary<Option, ParsedOption> parsedOpts;
// A stack of options that are currently being parsed.
private Stack<GnuOptionParseState> parseStack;
/// <summary>
/// Tells if this state is done parsing.
/// </summary>
public bool IsDone => parseStack.Count == 0;
private bool endOfOptionsReached;
/// <summary>
/// Parses a string argument.
/// </summary>
/// <param name="argument">An argument.</param>
/// <returns><c>true</c> if the argument could be parsed; otherwise, <c>false</c>.</returns>
public bool Parse(string argument) {
if (endOfOptionsReached) {
return ParseArgument(argument);
}
string trimmedArg;
switch (Classify(argument, out trimmedArg)) {
case GnuArgumentType.LongOption:
return ParseLongOption(trimmedArg);
case GnuArgumentType.ShortOption:
return ParseShortOption(trimmedArg);
case GnuArgumentType.EndOfOptions:
endOfOptionsReached = true;
return true;
default:
return ParseArgument(trimmedArg);
}
}
private bool ParseLongOption(string argument) {
string key, value;
if (IsKeyValueOption(argument, out key, out value)) {
// Key-value options like '--help=warnings'
// get special treatment.
Option opt;
if (Parser.longForms.TryGetValue(key, out opt)) {
ParseKeyValueOption(opt, new OptionForm(key, false), value);
}
else {
// Log that the option is unrecognized and early-out.
LogUnrecognized("--" + key);
}
return true;
}
else {
// Non--key-value long-form options never take
// any arguments directly. All we need to do is
// figure out which option the user is referring to
// and push it onto the option stack.
Option opt;
if (!Parser.longForms.TryGetValue(argument, out opt)) {
// Log that the option is unrecognized and early-out.
LogUnrecognized("--" + argument);
return true;
}
// Start parsing the option.
StartParsing(
opt,
new OptionForm(argument, false));
return true;
}
}
private bool ParseShortOption(string argument) {
string key, value;
if (IsKeyValueOption(argument, out key, out value)) {
// Key-value options like '-Wframe-larger-than=len'
// get special treatment.
var opt = Parser.shortForms.Get(key);
ParseKeyValueOption(opt, new OptionForm(key, true), value);
return true;
}
else {
// This is the case of a typical short-form option, e.g.,
//
// -o a.out
// -oa.out
// -Wnoexcept
// -Wnoexcept-type ( <-- this option and -Wnoexcept need to coexist)
// Find the longest match.
var node = Parser.shortForms;
var opt = node.Value;
int longestMatch = 0;
int i = 0;
foreach (var c in argument) {
if (!node.TryGetNext(c, out node)) {
break;
}
i++;
if (node.Value != null) {
opt = node.Value;
longestMatch = i;
}
}
if (opt == null) {
LogUnrecognized("-" + argument);
return true;
}
// Start parsing the option.
StartParsing(
opt,
new OptionForm(
argument.Substring(0, longestMatch),
true));
if (longestMatch == argument.Length) {
// The entire option has been parsed; we're done here.
return true;
}
else {
// Okay, so this is where things get tricky. We've parsed
// an option followed by some junk and we don't know what
// it means.
//
// For example:
//
// -O1
// ^
//
// -xzf source.tar.gz
// ^
//
// We could be dealing with either an argument to the
// option we just parsed or with a new option.
//
// TODO: GCC doesn't accept the second example. Maybe we
// shouldn't be guessing here and instead let a flag
// control what to do when we reach this case.
value = argument.Substring(longestMatch);
var hasParsed = parseStack.Peek().Parser.Parse(value);
if (hasParsed) {
// Successfully parsed as argument.
return true;
}
else {
// Try to parse it as an option.
return ParseShortOption(value);
}
}
}
}
private void ParseKeyValueOption(Option opt, OptionForm form, string value) {
if (opt == null) {
LogUnrecognized(form.ToString());
}
else {
StartParsing(opt, form);
var hasParsed = parseStack.Peek().Parser.Parse(value);
FinishParsing();
if (!hasParsed) {
LogUnexpectedArgument(form.ToString(), value);
}
}
}
private void LogUnexpectedArgument(string key, string value) {
Log.Log(
new LogEntry(
Severity.Error,
"unexpected argument",
Quotation.QuoteEvenInBold(
"command line option ",
key,
" did not expect an argument, but was passed ",
value,
"."),
CreateUsageParagraph(key)));
}
private void LogUnrecognized(string option) {
var suggestion = NameSuggestion.SuggestName(
option,
Parser.allForms.Value);
if (suggestion == null) {
Log.Log(
new LogEntry(
Severity.Error,
"unknown option",
"unrecognized command line option ",
Quotation.CreateBoldQuotation(option),
"."));
}
else {
var diff = Diff.Create( option, suggestion);
Log.Log(
new LogEntry(
Severity.Error,
"unknown option",
"unrecognized command line option ",
Quotation.CreateBoldQuotation(
TextDiff.RenderDeletions(diff)),
"; did you mean ",
Quotation.CreateBoldQuotation(
TextDiff.RenderInsertions(diff)),
"?",
CreateUsageParagraph(suggestion)));
}
}
private Option LookupOption(string form) {
if (form.StartsWith("--")) {
return Parser.longForms[form.Substring(2)];
}
else {
return Parser.shortForms.Get(form.Substring(1));
}
}
private MarkupNode CreateUsageParagraph(string option) {
return new Paragraph(
DecorationSpan.MakeBold(new ColorSpan("usage: ", Colors.Gray)),
new OptionHelp(
LookupOption(option),
GnuOptionPrinter.Instance));
}
private bool ParseArgument(string argument) {
if (IsDone) {
return false;
}
bool accepted = parseStack.Peek().Parser.Parse(argument);
if (accepted) {
return true;
}
else {
// Top-of-stack parser is done parsing. Pop it and move
// to the next parser.
FinishParsing();
return ParseArgument(argument);
}
}
private static GnuArgumentType Classify(
string argument,
out string trimmedArg) {
if (argument.StartsWith("--")) {
if (argument.Length == 2) {
trimmedArg = argument;
return GnuArgumentType.EndOfOptions;
}
else {
trimmedArg = argument.Substring(2);
return GnuArgumentType.LongOption;
}
}
else if (argument.StartsWith("-") && argument.Length > 1) {
trimmedArg = argument.Substring(1);
return GnuArgumentType.ShortOption;
}
else {
trimmedArg = argument;
return GnuArgumentType.Argument;
}
}
/// <summary>
/// Tells if an option is a key-value option, i.e.,
/// it has format 'key=value'.
/// </summary>
/// <param name="option">The (trimmed) option.</param>
/// <param name="key">The key.</param>
/// <param name="value">The value.</param>
/// <returns>
/// <c>true</c> if the option is a key-value option; otherwise, <c>false</c>.
/// </returns>
private static bool IsKeyValueOption(
string option,
out string key,
out string value) {
int index = option.IndexOf('=');
if (index <= 0) {
key = null;
value = null;
return false;
}
else {
key = option.Substring(0, index);
value = option.Substring(index + 1);
return true;
}
}
/// <summary>
/// Starts parsing a particular form of an option.
/// </summary>
/// <param name="option">The option to parse.</param>
/// <param name="form">The form of the option to parse.</param>
public void StartParsing(Option option, OptionForm form) {
parseStack.Push(
new GnuOptionParseState(
option,
form,
option.CreateParser(form)));
}
/// <summary>
/// Finishes parsing the top-of-stack option.
/// </summary>
private void FinishParsing() {
FinishParsing(parseStack.Pop());
}
/// <summary>
/// Finishes parsing an option.
/// </summary>
/// <param name="opt">The option to finish parsing.</param>
private void FinishParsing(GnuOptionParseState opt) {
var newVal = new ParsedOption(
opt.Form,
opt.Parser.GetValue(Log));
ParsedOption oldVal;
if (parsedOpts.TryGetValue(opt.Option, out oldVal)) {
newVal = opt.Option.MergeValues(oldVal, newVal);
}
parsedOpts[opt.Option] = newVal;
}
/// <summary>
/// Finishes parsing all options.
/// </summary>
public void FinishParsingAll() {
// When the last argument has been parsed, we need
// to be careful with the order in which we finish
// parsing options.
//
// For example, consider the following:
//
// gcc -x c++ a.cpp -x c++ b.cpp
// ^
//
// When the parser is at the caret, its option stack
// will look like this:
//
// | -x c++ b.cpp | <-- top of stack
// | -x c++ a.cpp |
// | --source |
//
// If were to finish parsing the top-of-stack option
// first, then the arguments to '-x' will be merged
// like so: 'c++ b.cpp' ++ 'c++ a.cpp'.
//
// But that's not what we want! If the user specifies
// 'a.cpp' first, then the option parser shouldn't
// surreptitiously re-arrange that after the last
// argument has been parsed.
//
// To solve this issue, we finish parsing options in
// the order in which we started parsing them, i.e.,
// in reverse stack order.
var stackArr = parseStack.ToArray();
for (int i = stackArr.Length - 1; i >= 0; i--) {
FinishParsing(stackArr[i]);
}
}
}
internal struct GnuOptionParseState {
public GnuOptionParseState(
Option option,
OptionForm form,
OptionParser parser) {
this = default(GnuOptionParseState);
this.Option = option;
this.Form = form;
this.Parser = parser;
}
/// <summary>
/// Gets the option that is being parsed.
/// </summary>
/// <returns>The option that is being parsed.</returns>
public Option Option { get; private set; }
/// <summary>
/// Gets the form of the option that is being parsed.
/// </summary>
/// <returns>The form of the option that is being parsed.</returns>
public OptionForm Form { get; private set; }
/// <summary>
/// Gets the option parser for the option being parsed.
/// </summary>
/// <returns>The option parser.</returns>
public OptionParser Parser { get; private set; }
}
/// <summary>
/// An enumeration of ways command-line arguments can
/// be classified.
/// </summary>
internal enum GnuArgumentType {
Argument,
ShortOption,
LongOption,
EndOfOptions
}
/// <summary>
/// A simple trie implementation.
/// </summary>
internal sealed class TrieNode<TKey, TValue> {
/// <summary>
/// Creates a trie node.
/// </summary>
public TrieNode() : this(default(TValue)) { }
/// <summary>
/// Creates a trie node from a value.
/// </summary>
/// <param name="value">The trie node's value.</param>
public TrieNode(
TValue value) {
this.Value = value;
this.successors = new Dictionary<TKey, TrieNode<TKey, TValue>>();
}
/// <summary>
/// Gets the value associated with this trie node.
/// </summary>
/// <returns>The value for this node.</returns>
public TValue Value { get; set; }
private Dictionary<TKey, TrieNode<TKey, TValue>> successors;
/// <summary>
/// Gets or sets the next trie node obtained by taking the
/// outgoing edge associated with a particular key.
/// </summary>
public TrieNode<TKey, TValue> this[TKey key] {
get { return successors[key]; }
set { successors[key] = value; }
}
/// <summary>
/// Tries to get the next trie node by taking the outgoing
/// edge associated with a given key.
/// </summary>
/// <param name="key">The key that determines which node to pick.</param>
/// <param name="next">The next node.</param>
/// <returns><c>true</c></returns>
public bool TryGetNext(TKey key, out TrieNode<TKey, TValue> next) {
return successors.TryGetValue(key, out next);
}
/// <summary>
/// Adds a path to this trie that ends in a particular value.
/// Existing values are overwritten.
/// </summary>
/// <param name="path">
/// The keys that constitute the path through this trie.
/// </param>
/// <param name="value">
/// The value at the end of the path.
/// </param>
public void AddPath(IEnumerable<TKey> path, TValue value) {
var curNode = this;
foreach (var key in path) {
TrieNode<TKey, TValue> nextNode;
if (!curNode.TryGetNext(key, out nextNode)) {
nextNode = new TrieNode<TKey, TValue>(default(TValue));
curNode[key] = nextNode;
}
curNode = nextNode;
}
curNode.Value = value;
}
/// <summary>
/// Gets the value obtained by traversing a specific path
/// of keys. The default value is returned if the path
/// is not in this trie.
/// </summary>
/// <param name="path">The path of keys to follow.</param>
/// <returns>The value obtained by traversing the path.</returns>
public TValue Get(IEnumerable<TKey> path) {
var curNode = this;
foreach (var key in path) {
TrieNode<TKey, TValue> nextNode;
if (curNode.TryGetNext(key, out nextNode)) {
curNode = nextNode;
}
else {
curNode = null;
break;
}
}
if (curNode == null) {
return default(TValue);
}
else {
return curNode.Value;
}
}
}
}