blob: 2d18c7564e49aa11201a5ff3901786d4595722d0 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "garnet/bin/zxdb/console/command_parser.h"
#include <stdio.h>
#include <map>
#include <set>
#include "garnet/bin/zxdb/common/err.h"
#include "garnet/bin/zxdb/console/command.h"
#include "garnet/bin/zxdb/console/nouns.h"
#include "garnet/public/lib/fxl/logging.h"
namespace zxdb {
namespace {
// Returns a sorted list of all possible noun and verb strings that can be
// input.
const std::set<std::string>& GetAllNounVerbStrings() {
static std::set<std::string> strings;
if (strings.empty()) {
for (const auto& noun_pair : GetNouns()) {
for (const auto& alias : noun_pair.second.aliases)
strings.insert(alias);
}
for (const auto& verb_pair : GetVerbs()) {
for (const auto& alias : verb_pair.second.aliases)
strings.insert(alias);
}
}
return strings;
}
// Returns only the canonical version of each noun and verb. Used for
// completions when there is no input and we don't want to cycle through both
// "s" and "step".
const std::set<std::string>& GetCanonicalNounVerbStrings() {
static std::set<std::string> strings;
if (strings.empty()) {
for (const auto& noun_pair : GetNouns())
strings.insert(noun_pair.second.aliases[0]);
for (const auto& verb_pair : GetVerbs())
strings.insert(verb_pair.second.aliases[0]);
}
return strings;
}
bool IsTokenSeparator(char c) { return c == ' '; }
// Finds the record for the switch associated with long switch string (which
// includes the two leading dashes), or null if there is no match.
//
// The token can contain and equals sign. In this case, only the text
// preceding the equals sign counts as the switch, and the index of the equals
// sign is placed into *equals_index. Otherwise *equals_index will be set to
// std::string::npos. This is to handle the fact that long switches can be
// expressed as either "--foo=bar" and "--foo bar".
const SwitchRecord* FindLongSwitch(const std::string& str,
const std::vector<SwitchRecord>& switches,
size_t* equals_index) {
// Should have two leading dashes.
FXL_DCHECK(str.size() >= 2 && str.substr(0, 2) == "--");
// Extract the switch value (varing depend on presence of '='), not counting
// the two leading dashes.
*equals_index = str.find('=');
std::string switch_str;
if (*equals_index == std::string::npos) {
switch_str = str.substr(2);
} else {
switch_str = str.substr(2, *equals_index - 2);
}
for (const auto& sr : switches) {
if (sr.name == switch_str)
return &sr;
}
return nullptr;
}
const SwitchRecord* FindSwitch(char ch,
const std::vector<SwitchRecord>& switches) {
for (const auto& sr : switches) {
if (sr.ch == ch)
return &sr;
}
return nullptr;
}
bool StartsWith(const std::string& str, const std::string& starts_with) {
if (str.size() < starts_with.size())
return false;
for (size_t i = 0; i < starts_with.size(); i++) {
if (str[i] != starts_with[i])
return false;
}
return true;
}
// Returns true if the string is all numeric digits that mean it's an index
// token.
bool IsIndexToken(const std::string& token) {
for (char c : token) {
if (c < '0' || c > '9')
return false;
}
return true;
}
// Consumes the next noun (and optional following integer) in the input at
// *token_index. If valid, fills the information into the given command and
// advances *token_index to the next unused token.
//
// *consumed will be set if any nouns were consumed (to disambiguate the
// "error parsing" case and the "the next thing wasn't a noun" case).
Err ConsumeNoun(const std::vector<std::string>& tokens, size_t* token_index,
Command* output, bool* consumed) {
*consumed = false;
const auto& nouns = GetStringNounMap();
auto found = nouns.find(tokens[*token_index]);
if (found == nouns.end())
return Err(); // Not a noun, but that's not an error.
Noun noun = found->second;
if (output->HasNoun(noun))
return Err("Noun \"" + NounToString(noun) + "\" specified twice.");
// Advance to the next token.
(*token_index)++;
// Consume optional following index if it's all integers. For example, it
// could be "process 2 run" (with index) or "process run" (without).
size_t noun_index = Command::kNoIndex;
if ((*token_index) < tokens.size() && IsIndexToken(tokens[*token_index])) {
if (sscanf(tokens[*token_index].c_str(), "%zu", &noun_index) != 1) {
return Err("Invalid index \"" + tokens[*token_index] + "\" for \"" +
NounToString(noun) + "\".");
}
(*token_index)++;
}
*consumed = true;
output->SetNoun(noun, noun_index);
return Err();
}
// Continue to consume nouns from the token stream until either no more tokens
// have been found or we reached the end of tokens.
//
// If successful, it will add the nouns to the command and will update the
// token_index to the next token to be evaluated.
Err ConsumeNouns(const std::vector<std::string>& tokens, size_t* token_index,
Command* output) {
bool found_noun = false; // Whether the next token(s) were nouns
do {
Err err = ConsumeNoun(tokens, token_index, output, &found_noun);
if (err.has_error())
return err;
} while (found_noun && (*token_index < tokens.size()));
return Err();
}
// Consumes the next token expecting to find a verb. If valid it will register
// the verb into the command and will advance the token_index variable.
//
// It's possible there's no verb in which case this will put null into
// *verb_record and return success.
//
// Will update a given pointer to the respective VerbRecord. We return by
// pointer because VerbRecords are unique and non-copyable/movable.
Err ConsumeVerb(const std::vector<std::string>& tokens, size_t* token_index_ptr,
Command* output, const VerbRecord** verb_record) {
// Reference makes the code easier to understand
size_t& token_index = *token_index_ptr;
const std::string& token = tokens[token_index];
// Consume the verb.
const auto& verb_strings = GetStringVerbMap();
auto found_verb_str = verb_strings.find(token);
if (found_verb_str == verb_strings.end()) {
return Err("The string \"" + token + "\" is not a valid verb.");
}
output->set_verb(found_verb_str->second);
token_index++;
// Find the verb record.
const auto& verbs = GetVerbs();
auto found_verb = verbs.find(output->verb());
FXL_DCHECK(found_verb != verbs.end()); // Valid verb should always be found.
*verb_record = &found_verb->second;
return Err();
}
// Consumes tokens and interprets them as switches. Each verb has a particular
// set of switches associated to it. The appearance of another switch means
// the command is erroneous.
//
// If successful, it will set the switches to the command and will update the
// token_index to the next token to be evaluated.
Err ConsumeSwitches(const std::vector<std::string>& tokens,
size_t* token_index_ptr, Command* output,
const std::vector<SwitchRecord>& switches) {
// Reference makes the code easier to understand
size_t& token_index = *token_index_ptr;
// Look for switches.
while (token_index < tokens.size()) {
const std::string& token = tokens[token_index];
// "--" marks the end of switches.
if (token == "--") {
token_index++;
break;
}
// Not a switch, everything else is an arg.
if (token[0] != '-')
break;
if (token.size() == 1)
return Err("Invalid switch \"-\".");
const SwitchRecord* sw_record = nullptr;
std::string value;
bool next_token_is_value = false;
if (token[1] == '-') {
// Two-hyphen (--) switch.
size_t equals_index = std::string::npos;
sw_record = FindLongSwitch(token, switches, &equals_index);
if (!sw_record)
return Err("Unknown switch \"" + token + "\".");
if (equals_index == std::string::npos) {
// "--foo bar" format.
next_token_is_value = sw_record->has_value;
} else {
// "--foo=bar" format.
if (sw_record->has_value) {
// Extract the token following the equals sign.
value = token.substr(equals_index + 1);
} else {
return Err("The switch " + token.substr(0, equals_index) +
" does not take a value.");
}
}
} else {
// Single-dash token means one character.
char switch_char = token[1];
sw_record = FindSwitch(switch_char, switches);
if (!sw_record)
return Err(std::string("Unknown switch \"-") + switch_char + "\".");
if (token.size() > 2) {
// Single character switch with stuff after it: it's the argument "-a4"
if (!sw_record->has_value) {
return Err(std::string("Extra characters after \"-") + switch_char +
"\".");
}
value = token.substr(2);
} else {
next_token_is_value = sw_record->has_value;
}
}
// Expecting a value as the next token.
if (next_token_is_value) {
if (token_index == tokens.size() - 1) {
// No more tokens to consume.
return Err(std::string("Parameter needed for \"") + token + "\".");
} else {
token_index++;
value = tokens[token_index];
}
}
output->SetSwitch(sw_record->id, std::move(value));
token_index++;
}
return Err();
}
} // namespace
Err TokenizeCommand(const std::string& input,
std::vector<std::string>* result) {
result->clear();
// TODO(brettw) this will probably need some kind of quoting and escaping
// logic.
size_t cur = 0;
while (true) {
// Skip separators
while (cur < input.size() && IsTokenSeparator(input[cur])) {
++cur;
}
if (cur == input.size())
break;
size_t token_begin = cur;
// Skip to end of token.
while (cur < input.size() && !IsTokenSeparator(input[cur])) {
++cur;
}
if (cur == token_begin)
break; // Got to end of input.
// Emit token.
result->push_back(input.substr(token_begin, cur - token_begin));
}
// This returns an Err() to allow for adding escaping errors in the future.
return Err();
}
Err ParseCommand(const std::string& input, Command* output) {
*output = Command();
std::vector<std::string> tokens;
Err err = TokenizeCommand(input, &tokens);
if (err.has_error() || tokens.empty())
return err;
return ParseCommand(tokens, output);
}
Err ParseCommand(const std::vector<std::string>& tokens, Command* output) {
*output = Command();
if (tokens.empty())
return Err();
// Keep track of the next token to evaluate
size_t token_index = 0;
// We look for all the possible nouns within the tokens
Err noun_err = ConsumeNouns(tokens, &token_index, output);
if (noun_err.has_error())
return noun_err;
// If no more tokens, then no verb was specified (for example "process 2").
if (token_index == tokens.size())
return Err();
// Check for verb and get a reference to its record if there is one.
const VerbRecord* verb_record = nullptr;
if (tokens[token_index].size() >= 1 && tokens[token_index][0] != '-') {
// Not a switch, read verb.
Err verb_err = ConsumeVerb(tokens, &token_index, output, &verb_record);
if (verb_err.has_error())
return verb_err;
}
// Switches.
const std::vector<SwitchRecord>& switches =
verb_record ? verb_record->switches : GetNounSwitches();
Err switch_err = ConsumeSwitches(tokens, &token_index, output, switches);
if (switch_err.has_error())
return switch_err;
// Every token left is an argument to the command
std::vector<std::string> args(tokens.begin() + token_index, tokens.end());
output->set_args(std::move(args));
return Err();
}
// It would be nice to do more context-aware completions. For now, just
// complete based on all known nouns and verbs.
std::vector<std::string> GetCommandCompletions(const std::string& input) {
std::vector<std::string> result;
std::vector<std::string> tokens;
Err err = TokenizeCommand(input, &tokens);
if (err.has_error())
return result;
// The no input or following a space, cycle through all possibilities.
if (input.empty() || tokens.empty() || input.back() == ' ') {
for (const auto& str : GetCanonicalNounVerbStrings())
result.push_back(input + str);
return result;
}
// Compute the string of stuff that stays constant for each completion.
std::string prefix;
if (!tokens.empty()) {
// All tokens but the last one.
for (size_t i = 0; i < tokens.size() - 1; i++) {
prefix += tokens[i];
prefix.push_back(' ');
}
}
// Cycle through matching prefixes.
const std::string token = tokens.back();
const std::set<std::string>& possibilities = GetAllNounVerbStrings();
auto found = possibilities.lower_bound(token);
while (found != possibilities.end() && StartsWith(*found, token)) {
result.push_back(prefix + *found);
++found;
}
return result;
}
} // namespace zxdb