//////////////////////////////////////////////////////////////////////////////// // // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // https://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include // cmake.h include header must come first #include #include #include #include #include #include #include #include #include #include #include #include // Overridden by rc.abbreviation.minimum. int CLI2::minimumMatchLength = 3; // Alias expansion limit. Any more indicates some kind of error. static int safetyValveDefault = 10; //////////////////////////////////////////////////////////////////////////////// A2::A2(const std::string& raw, Lexer::Type lextype) { _lextype = lextype; attribute("raw", raw); } //////////////////////////////////////////////////////////////////////////////// A2::A2(const A2& other) = default; //////////////////////////////////////////////////////////////////////////////// A2& A2::operator=(const A2& other) = default; //////////////////////////////////////////////////////////////////////////////// bool A2::hasTag(const std::string& tag) const { return std::find(_tags.begin(), _tags.end(), tag) != _tags.end(); } //////////////////////////////////////////////////////////////////////////////// void A2::tag(const std::string& tag) { if (!hasTag(tag)) _tags.push_back(tag); } //////////////////////////////////////////////////////////////////////////////// void A2::unTag(const std::string& tag) { for (auto i = _tags.begin(); i != _tags.end(); ++i) if (*i == tag) { _tags.erase(i); break; } } //////////////////////////////////////////////////////////////////////////////// // Accessor for attributes. void A2::attribute(const std::string& name, const std::string& value) { _attributes[name] = value; if (name == "raw") decompose(); } //////////////////////////////////////////////////////////////////////////////// // Accessor for attributes. const std::string A2::attribute(const std::string& name) const { // Prevent autovivification. auto i = _attributes.find(name); if (i != _attributes.end()) return i->second; return ""; } //////////////////////////////////////////////////////////////////////////////// const std::string A2::getToken() const { auto i = _attributes.find("canonical"); if (i == _attributes.end()) i = _attributes.find("raw"); return i->second; } //////////////////////////////////////////////////////////////////////////////// void A2::decompose() { if (_lextype == Lexer::Type::tag) { std::string raw = _attributes["raw"]; attribute("name", raw.substr(1)); attribute("sign", raw.substr(0, 1)); } else if (_lextype == Lexer::Type::substitution) { // if (Directory (raw).exists ()) // return; std::string from; std::string to; std::string flags; if (Lexer::decomposeSubstitution(_attributes["raw"], from, to, flags)) { attribute("from", from); attribute("to", to); attribute("flags", flags); } } else if (_lextype == Lexer::Type::pair) { std::string name; std::string mod; std::string sep; std::string value; if (Lexer::decomposePair(_attributes["raw"], name, mod, sep, value)) { attribute("name", name); attribute("modifier", mod); attribute("separator", sep); attribute("value", value); if (name == "rc") { if (mod != "") tag("CONFIG"); else tag("RC"); } } } else if (_lextype == Lexer::Type::pattern) { // if (Directory (raw).exists ()) // return; std::string pattern; std::string flags; if (Lexer::decomposePattern(_attributes["raw"], pattern, flags)) { attribute("pattern", pattern); attribute("flags", flags); } } } //////////////////////////////////////////////////////////////////////////////// const std::string A2::dump() const { auto output = Lexer::typeToString(_lextype); // Dump attributes. std::string atts; for (const auto& a : _attributes) atts += a.first + "='\033[33m" + a.second + "\033[0m' "; // Dump tags. std::string tags; for (const auto& tag : _tags) { if (tag == "BINARY") tags += "\033[1;37;44m" + tag + "\033[0m "; else if (tag == "CMD") tags += "\033[1;37;46m" + tag + "\033[0m "; else if (tag == "FILTER") tags += "\033[1;37;42m" + tag + "\033[0m "; else if (tag == "MODIFICATION") tags += "\033[1;37;43m" + tag + "\033[0m "; else if (tag == "MISCELLANEOUS") tags += "\033[1;37;45m" + tag + "\033[0m "; else if (tag == "RC") tags += "\033[1;37;41m" + tag + "\033[0m "; else if (tag == "CONFIG") tags += "\033[1;37;101m" + tag + "\033[0m "; else if (tag == "?") tags += "\033[38;5;255;48;5;232m" + tag + "\033[0m "; else tags += "\033[32m" + tag + "\033[0m "; } return output + ' ' + atts + tags; } //////////////////////////////////////////////////////////////////////////////// static const char* getValue(int argc, const char** argv, std::string arg) { const auto is_arg = [&](std::string s) { return s.size() > arg.size() + 1 && (s[arg.size()] == ':' || s[arg.size()] == '=') && s.compare(0, arg.size(), arg) == 0; }; // find last argument before -- auto last = std::make_reverse_iterator(argv); auto first = std::make_reverse_iterator(std::find(argv, argv + argc, std::string("--"))); auto it = std::find_if(first, last, is_arg); if (it == last) return nullptr; // return the string after : or = return *it + arg.size() + 1; } //////////////////////////////////////////////////////////////////////////////// // Static method. bool CLI2::getOverride(int argc, const char** argv, File& rc) { const char* value = getValue(argc, argv, "rc"); if (value == nullptr) return false; rc = File(value); return true; } //////////////////////////////////////////////////////////////////////////////// // Look for CONFIG data.location and initialize a Path object. // Static method. bool CLI2::getDataLocation(int argc, const char** argv, Path& data) { const char* value = getValue(argc, argv, "rc.data.location"); if (value == nullptr) { std::string location = Context::getContext().config.get("data.location"); if (location != "") data = location; return false; } data = Directory(value); return true; } //////////////////////////////////////////////////////////////////////////////// // Static method. void CLI2::applyOverrides(int argc, const char** argv) { auto& context = Context::getContext(); auto last = std::find(argv, argv + argc, std::string("--")); auto is_override = [](const std::string& s) { return s.compare(0, 3, "rc.") == 0; }; auto get_sep = [&](const std::string& s) { if (is_override(s)) return s.find_first_of(":=", 3); return std::string::npos; }; auto override_settings = [&](std::string raw) { auto sep = get_sep(raw); if (sep == std::string::npos) return; std::string name = raw.substr(3, sep - 3); std::string value = raw.substr(sep + 1); context.config.set(name, value); }; auto display_overrides = [&](std::string raw) { if (is_override(raw)) context.footnote(format("Configuration override {1}", raw)); }; std::for_each(argv, last, override_settings); if (context.verbose("override")) std::for_each(argv, last, display_overrides); } //////////////////////////////////////////////////////////////////////////////// void CLI2::alias(const std::string& name, const std::string& value) { _aliases[name] = value; } //////////////////////////////////////////////////////////////////////////////// void CLI2::entity(const std::string& category, const std::string& name) { // Walk the list of entities for category. auto c = _entities.equal_range(category); for (auto e = c.first; e != c.second; ++e) if (e->second == name) return; // The category/name pair was not found, therefore add it. _entities.emplace(category, name); } //////////////////////////////////////////////////////////////////////////////// // Capture a single argument. void CLI2::add(const std::string& argument) { A2 arg(Lexer::trim(argument), Lexer::Type::word); arg.tag("ORIGINAL"); _original_args.push_back(arg); // Adding a new argument invalidates prior analysis. _args.clear(); } //////////////////////////////////////////////////////////////////////////////// // Capture a set of arguments, inserted immediately after arguments // after the binary.. void CLI2::add(const std::vector& arguments, int offset /* = 0 */) { std::vector replacement{_original_args.begin(), _original_args.begin() + offset + 1}; for (const auto& arg : arguments) replacement.emplace_back(arg, Lexer::Type::word); for (unsigned int i = 1 + offset; i < _original_args.size(); ++i) replacement.push_back(_original_args[i]); _original_args = replacement; // Adding a new argument invalidates prior analysis. _args.clear(); } //////////////////////////////////////////////////////////////////////////////// // Arg0 is the first argument, which is the name and potentially a relative or // absolute path to the invoked binary. // // The binary name is 'task', but if the binary is reported as 'cal' or // 'calendar' then it was invoked via symbolic link, in which case capture the // first argument as 'calendar'. void CLI2::handleArg0() { // Capture arg0 separately, because it is the command that was run, and could // need special handling. auto raw = _original_args[0].attribute("raw"); A2 a(raw, Lexer::Type::word); a.tag("BINARY"); std::string basename = "task"; auto slash = raw.rfind('/'); if (slash != std::string::npos) basename = raw.substr(slash + 1); a.attribute("basename", basename); if (basename == "cal" || basename == "calendar") { _args.push_back(a); A2 cal("calendar", Lexer::Type::word); _args.push_back(cal); } else { _args.push_back(a); } } //////////////////////////////////////////////////////////////////////////////// // All arguments must be individually and wholly recognized by the Lexer. Any // argument not recognized is considered a Lexer::Type::word. // // As a side effect, tags all arguments after a terminator ('--') with // TERMINATED. void CLI2::lexArguments() { // Note: Starts iterating at index 1, because ::handleArg0 has already // processed it. bool terminated = false; for (unsigned int i = 1; i < _original_args.size(); ++i) { bool quoted = Lexer::wasQuoted(_original_args[i].attribute("raw")); // Process single-token arguments. std::string lexeme; Lexer::Type type; Lexer lex(_original_args[i].attribute("raw")); if (lex.token(lexeme, type) && (lex.isEOS() || // Token goes to EOS (quoted && type == Lexer::Type::pair))) // Quoted pairs automatically go to EOS { if (!terminated && type == Lexer::Type::separator) terminated = true; else if (terminated) type = Lexer::Type::word; A2 a(_original_args[i].attribute("raw"), type); if (terminated) a.tag("TERMINATED"); if (quoted) a.tag("QUOTED"); if (_original_args[i].hasTag("ORIGINAL")) a.tag("ORIGINAL"); _args.push_back(a); } // Process multiple-token arguments. else { const std::string quote = "'"; // Escape unescaped single quotes std::string escaped = ""; // For performance reasons. The escaped string is as long as the original. escaped.reserve(_original_args[i].attribute("raw").size()); std::string::size_type cursor = 0; bool nextEscaped = false; while (int num = utf8_next_char(_original_args[i].attribute("raw"), cursor)) { std::string character = utf8_character(num); if (!nextEscaped && (character == "\\")) nextEscaped = true; else { if (character == quote && !nextEscaped) escaped += "\\"; nextEscaped = false; } escaped += character; } cursor = 0; std::string word; if (Lexer::readWord(quote + escaped + quote, quote, cursor, word)) { Lexer::dequote(word); A2 unknown(word, Lexer::Type::word); if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED"); if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL"); _args.push_back(unknown); } // This branch may have no use-case. else { A2 unknown(_original_args[i].attribute("raw"), Lexer::Type::word); unknown.tag("UNKNOWN"); if (lex.wasQuoted(_original_args[i].attribute("raw"))) unknown.tag("QUOTED"); if (_original_args[i].hasTag("ORIGINAL")) unknown.tag("ORIGINAL"); _args.push_back(unknown); } } } if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze lexArguments")); } //////////////////////////////////////////////////////////////////////////////// // [1] Scan all args for the 'add' and 'log' commands, and demote any // Lexer::Type::Tag args with sign '-' to Lexer::Type::word. // [2] Convert any pseudo args name:value into config settings, and erase. void CLI2::demotion() { bool changes = false; std::vector replacement; std::string canonical; for (auto& a : _args) { if (a._lextype == Lexer::Type::tag && a.attribute("sign") == "-") { std::string command = getCommand(); if (command == "add" || command == "log") { a._lextype = Lexer::Type::word; changes = true; } } else if (a._lextype == Lexer::Type::pair && canonicalize(canonical, "pseudo", a.attribute("name"))) { Context::getContext().config.set(canonical, a.attribute("value")); changes = true; // Equivalent to erasing 'a'. continue; } replacement.push_back(a); } if (changes && Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze demotion")); } //////////////////////////////////////////////////////////////////////////////// // Intended to be called after ::add() to perform the final analysis. void CLI2::analyze() { if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze")); // Process _original_args. _args.clear(); handleArg0(); lexArguments(); // Process _args. aliasExpansion(); if (!findCommand()) { defaultCommand(); if (!findCommand()) throw std::string("You must specify a command or a task to modify."); } demotion(); canonicalizeNames(); // Determine arg types: FILTER, MODIFICATION, MISCELLANEOUS. categorizeArgs(); parenthesizeOriginalFilter(); // Cache frequently looked up items _command = getCommand(); } //////////////////////////////////////////////////////////////////////////////// // Process raw filter string. // Insert filter arguments (wrapped in parentheses) immediatelly after the binary. void CLI2::addFilter(const std::string& arg) { if (arg.length()) { std::vector filter; filter.push_back("("); std::string lexeme; Lexer::Type type; Lexer lex(arg); while (lex.token(lexeme, type)) filter.push_back(lexeme); filter.push_back(")"); add(filter); analyze(); } } //////////////////////////////////////////////////////////////////////////////// // Process raw modification string. // Insert modification arguments immediatelly after the command (i.e. 'add') void CLI2::addModifications(const std::string& arg) { if (arg.length()) { std::vector mods; std::string lexeme; Lexer::Type type; Lexer lex(arg); while (lex.token(lexeme, type)) mods.push_back(lexeme); // Determine at which argument index does the task modification command // reside unsigned int cmdIndex = 0; for (; cmdIndex < _args.size(); ++cmdIndex) { // Command found, stop iterating. if (_args[cmdIndex].hasTag("CMD")) break; } // Insert modifications after the command. add(mods, cmdIndex); analyze(); } } //////////////////////////////////////////////////////////////////////////////// // There are situations where a context filter is applied. This method // determines whether one applies, and if so, applies it. Disqualifiers include: // - filter contains ID or UUID void CLI2::addContext(bool readable, bool writeable) { // Recursion block. if (_context_added) return; // Detect if any context is set, and bail out if not std::string contextString; if (readable) // Empty string is treated as "currently selected context" contextString = Context::getContext().getTaskContext("read", ""); else if (writeable) contextString = Context::getContext().getTaskContext("write", ""); else return; // If context is empty, bail out too if (contextString.empty()) return; // For readable contexts: Detect if UUID or ID is set, and bail out if (readable) for (auto& a : _args) { if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number || a._lextype == Lexer::Type::set) { Context::getContext().debug( format("UUID/ID argument found '{1}', not applying context.", a.attribute("raw"))); return; } } // Apply the context. Readable (filtering) takes precedence. Also set the // block now, since addFilter calls analyze(), which calls addContext(). _context_added = true; if (readable) addFilter(contextString); else if (writeable) addModifications(contextString); // Inform the user about the application of context if (Context::getContext().verbose("context")) Context::getContext().footnote(format("Context '{1}' set. Use 'task context none' to remove.", Context::getContext().config.get("context"))); } //////////////////////////////////////////////////////////////////////////////// // Parse the command line, identifiying filter components, expanding syntactic // sugar as necessary. void CLI2::prepareFilter() { // Clear and re-populate. _id_ranges.clear(); _uuid_list.clear(); _context_added = false; // Remove all the syntactic sugar for FILTERs. lexFilterArgs(); findIDs(); findUUIDs(); insertIDExpr(); desugarFilterPlainArgs(); findStrayModifications(); desugarFilterTags(); desugarFilterAttributes(); desugarFilterPatterns(); insertJunctions(); // Deliberately after all desugar calls. if (Context::getContext().verbose("filter")) { std::string combined; for (const auto& a : _args) { if (a.hasTag("FILTER")) { if (combined != "") combined += ' '; combined += a.attribute("raw"); } } if (combined.size()) Context::getContext().footnote(std::string("Filter: ") + combined); } } //////////////////////////////////////////////////////////////////////////////// // Return all the MISCELLANEOUS args as strings. const std::vector CLI2::getWords() { std::vector words; for (const auto& a : _args) if (a.hasTag("MISCELLANEOUS")) words.push_back(a.attribute("raw")); if (Context::getContext().config.getInteger("debug.parser") >= 2) { Color colorOrigArgs("gray10 on gray4"); std::string message = " "; for (const auto& word : words) message += colorOrigArgs.colorize(word) + ' '; Context::getContext().debug("CLI2::getWords" + message); } return words; } //////////////////////////////////////////////////////////////////////////////// // Return all the MISCELLANEOUS args. const std::vector CLI2::getMiscellaneous() { std::vector misc; for (const auto& a : _args) if (a.hasTag("MISCELLANEOUS")) misc.push_back(a); return misc; } //////////////////////////////////////////////////////////////////////////////// // Search for 'value' in _entities category, return canonicalized value. bool CLI2::canonicalize(std::string& canonicalized, const std::string& category, const std::string& value) { // Utilize a cache mapping of (category, value) -> canonicalized value. // This cache does not need to be invalidated, because entities are defined // only once per initialization of the Context object. int cache_key = 31 * std::hash{}(category) + std::hash{}(value); auto cache_result = _canonical_cache.find(cache_key); if (cache_result != _canonical_cache.end()) { canonicalized = cache_result->second; return true; } // Extract a list of entities for category. std::vector options; auto c = _entities.equal_range(category); for (auto e = c.first; e != c.second; ++e) { // Shortcut: if an exact match is found, success. if (value == e->second) { canonicalized = value; _canonical_cache[cache_key] = value; return true; } options.push_back(e->second); } // Match against the options, throw away results. std::vector matches; if (autoComplete(value, options, matches, minimumMatchLength) == 1) { canonicalized = matches[0]; _canonical_cache[cache_key] = matches[0]; return true; } return false; } //////////////////////////////////////////////////////////////////////////////// std::string CLI2::getBinary() const { if (_args.size()) return _args[0].attribute("raw"); return ""; } //////////////////////////////////////////////////////////////////////////////// std::string CLI2::getCommand(bool canonical) const { // Shortcut if analysis has been finalized if (_command != "") return _command; for (const auto& a : _args) if (a.hasTag("CMD")) return a.attribute(canonical ? "canonical" : "raw"); return ""; } //////////////////////////////////////////////////////////////////////////////// const std::string CLI2::dump(const std::string& title) const { std::stringstream out; out << "\033[1m" << title << "\033[0m\n" << " _original_args\n "; Color colorArgs("gray10 on gray4"); Color colorFilter("black on rgb311"); for (auto i = _original_args.begin(); i != _original_args.end(); ++i) { if (i != _original_args.begin()) out << ' '; if (i->hasTag("ORIGINAL")) out << colorArgs.colorize(i->attribute("raw")); else out << colorFilter.colorize(i->attribute("raw")); } out << '\n'; if (_args.size()) { out << " _args\n"; for (const auto& a : _args) out << " " << a.dump() << '\n'; } if (_id_ranges.size()) { out << " _id_ranges\n "; for (const auto& range : _id_ranges) { if (range.first != range.second) out << colorArgs.colorize(range.first + "-" + range.second) << ' '; else out << colorArgs.colorize(range.first) << ' '; } out << '\n'; } if (_uuid_list.size()) { out << " _uuid_list\n "; for (const auto& uuid : _uuid_list) out << colorArgs.colorize(uuid) << ' '; out << '\n'; } return out.str(); } //////////////////////////////////////////////////////////////////////////////// // If any aliases are found in un-TERMINATED arguments, replace the alias with // a set of Lexed tokens from the configuration. void CLI2::aliasExpansion() { bool changes = false; bool action; int counter = 0; do { action = false; std::vector reconstructed; std::string raw; for (const auto& i : _args) { raw = i.attribute("raw"); if (i.hasTag("TERMINATED")) { reconstructed.push_back(i); } else if (_aliases.find(raw) != _aliases.end()) { std::string lexeme; Lexer::Type type; Lexer lex(_aliases[raw]); while (lex.token(lexeme, type)) reconstructed.emplace_back(lexeme, type); action = true; changes = true; } else { reconstructed.push_back(i); } } _args = reconstructed; std::vector reconstructedOriginals; bool terminated = false; for (const auto& i : _original_args) { if (i.attribute("raw") == "--") terminated = true; if (terminated) { reconstructedOriginals.push_back(i); } else if (_aliases.find(i.attribute("raw")) != _aliases.end()) { std::string lexeme; Lexer::Type type; Lexer lex(_aliases[i.attribute("raw")]); while (lex.token(lexeme, type)) reconstructedOriginals.emplace_back(lexeme, type); action = true; changes = true; } else { reconstructedOriginals.push_back(i); } } _original_args = reconstructedOriginals; } while (action && counter++ < safetyValveDefault); if (counter >= safetyValveDefault) Context::getContext().debug(format("Nested alias limit of {1} reached.", safetyValveDefault)); if (changes && Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze aliasExpansion")); } //////////////////////////////////////////////////////////////////////////////// // Scan all arguments and canonicalize names that need it. void CLI2::canonicalizeNames() { bool changes = false; for (auto& a : _args) { if (a._lextype == Lexer::Type::pair) { std::string raw = a.attribute("raw"); if (raw.substr(0, 3) != "rc:" && raw.substr(0, 3) != "rc.") { std::string name = a.attribute("name"); std::string canonical; if (canonicalize(canonical, "pseudo", name) || canonicalize(canonical, "attribute", name)) { a.attribute("canonical", canonical); } else { a._lextype = Lexer::Type::word; } changes = true; } } } if (changes && Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze canonicalizeNames")); } //////////////////////////////////////////////////////////////////////////////// // Categorize FILTER, MODIFICATION and MISCELLANEOUS args, based on CMD DNA. void CLI2::categorizeArgs() { // Context is only applied for commands that request it. std::string command = getCommand(); Command* cmd = Context::getContext().commands[command]; // Determine if the command uses Context. CmdCustom and CmdTimesheet need to // be handled separately, as they override the parent Command::use_context // method, and this is a pointer to Command class. // // All Command classes overriding uses_context () getter need to be specified // here. bool uses_context; if (dynamic_cast(cmd)) uses_context = (dynamic_cast(cmd))->uses_context(); else if (dynamic_cast(cmd)) uses_context = (dynamic_cast(cmd))->uses_context(); else if (cmd) uses_context = cmd->uses_context(); // Apply the context, if applicable if (cmd && uses_context) addContext(cmd->accepts_filter(), cmd->accepts_modifications()); bool changes = false; bool afterCommand = false; for (auto& a : _args) { if (a._lextype == Lexer::Type::separator) continue; // Record that the command has been found, it affects behavior. if (a.hasTag("CMD")) { afterCommand = true; } // Skip admin args. else if (a.hasTag("BINARY") || a.hasTag("RC") || a.hasTag("CONFIG")) { // NOP. } // All combinations, with all 8 cases handled below.: // // -- -- -- Error: found an arg, but none expected // -- -- Mi task [Mi] [Mi] // -- Mo -- task [Mo] [Mo] // -- Mo Mi Internally inconsistent // Fi -- -- task [Fi] [Fi] // Fi -- Mi task [Fi] [Mi] // Fi Mo -- task [Fi] [Mo] // Fi Mo Mi Internally inconsistent // else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() && !cmd->accepts_miscellaneous()) { // No commands were expected --> error. throw format("The '{1}' command does not allow '{2}'.", command, a.attribute("raw")); } else if (cmd && !cmd->accepts_filter() && !cmd->accepts_modifications() && cmd->accepts_miscellaneous()) { a.tag("MISCELLANEOUS"); changes = true; } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() && !cmd->accepts_miscellaneous()) { a.tag("MODIFICATION"); changes = true; } else if (cmd && !cmd->accepts_filter() && cmd->accepts_modifications() && cmd->accepts_miscellaneous()) { // Error: internally inconsistent. throw std::string("Unknown error. Please report."); } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() && !cmd->accepts_miscellaneous()) { a.tag("FILTER"); changes = true; } else if (cmd && cmd->accepts_filter() && !cmd->accepts_modifications() && cmd->accepts_miscellaneous()) { if (!afterCommand) a.tag("FILTER"); else a.tag("MISCELLANEOUS"); changes = true; } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() && !cmd->accepts_miscellaneous()) { if (!afterCommand) a.tag("FILTER"); else a.tag("MODIFICATION"); changes = true; } else if (cmd && cmd->accepts_filter() && cmd->accepts_modifications() && cmd->accepts_miscellaneous()) { // Error: internally inconsistent. throw std::string("Unknown error. Please report."); } } if (changes && Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze categorizeArgs")); } //////////////////////////////////////////////////////////////////////////////// // The following command: // // task +home or +work list // // Is reasonable, and does not work unless the filter is parenthesized. Ignoring // context, the 'list' report has a filter, which is inserted at the beginning // like this: // // task ( status:pending ) +home or +work list // // Parenthesizing the user-provided (original) filter yields this: // // task ( status:pending ) ( +home or +work ) list // // And when the conjunction is added: // // task ( status:pending ) and ( +home or +work ) list // // the query is correct. void CLI2::parenthesizeOriginalFilter() { // Locate the first and last ORIGINAL FILTER args. unsigned int firstOriginalFilter = 0; unsigned int lastOriginalFilter = 0; for (unsigned int i = 1; i < _args.size(); ++i) { if (_args[i].hasTag("FILTER") && _args[i].hasTag("ORIGINAL")) { if (firstOriginalFilter == 0) firstOriginalFilter = i; lastOriginalFilter = i; } } // If found, parenthesize the arg list accordingly. if (firstOriginalFilter && lastOriginalFilter) { std::vector reconstructed; for (unsigned int i = 0; i < _args.size(); ++i) { if (i == firstOriginalFilter) { A2 openParen("(", Lexer::Type::op); openParen.tag("ORIGINAL"); openParen.tag("FILTER"); reconstructed.push_back(openParen); } reconstructed.push_back(_args[i]); if (i == lastOriginalFilter) { A2 closeParen(")", Lexer::Type::op); closeParen.tag("ORIGINAL"); closeParen.tag("FILTER"); reconstructed.push_back(closeParen); } } _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze parenthesizeOriginalFilter")); } } //////////////////////////////////////////////////////////////////////////////// // Scan all arguments and if any are an exact match for a command name, then // tag as CMD. If an argument is an exact match for an attribute, despite being // an inexact match for a command, then it is not a command. bool CLI2::findCommand() { for (auto& a : _args) { std::string raw = a.attribute("raw"); std::string canonical; // If the arg canonicalized to a 'cmd', but is also not an exact match // for an 'attribute', proceed. Example: // task project=foo list // ^cmd ^cmd // ^attribute if (exactMatch("cmd", raw)) canonical = raw; else if (exactMatch("attribute", raw)) continue; else if (!canonicalize(canonical, "cmd", raw)) continue; a.attribute("canonical", canonical); a.tag("CMD"); // Apply command DNA as tags. Command* command = Context::getContext().commands[canonical]; if (command->read_only()) a.tag("READONLY"); if (command->displays_id()) a.tag("SHOWSID"); if (command->needs_gc()) a.tag("RUNSGC"); if (command->uses_context()) a.tag("USESCONTEXT"); if (command->accepts_filter()) a.tag("ALLOWSFILTER"); if (command->accepts_modifications()) a.tag("ALLOWSMODIFICATIONS"); if (command->accepts_miscellaneous()) a.tag("ALLOWSMISC"); if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze findCommand")); // Stop and indicate command found. return true; } // Indicate command not found. return false; } //////////////////////////////////////////////////////////////////////////////// // Search for exact 'value' in _entities category. bool CLI2::exactMatch(const std::string& category, const std::string& value) const { // Extract a list of entities for category. auto c = _entities.equal_range(category); for (auto e = c.first; e != c.second; ++e) if (value == e->second) return true; return false; } //////////////////////////////////////////////////////////////////////////////// // +tag --> tags _hastag_ tag // -tag --> tags _notag_ tag void CLI2::desugarFilterTags() { bool changes = false; std::vector reconstructed; for (const auto& a : _args) { if (a._lextype == Lexer::Type::tag && a.hasTag("FILTER")) { changes = true; A2 left("tags", Lexer::Type::dom); left.tag("FILTER"); reconstructed.push_back(left); std::string raw = a.attribute("raw"); A2 op(raw[0] == '+' ? "_hastag_" : "_notag_", Lexer::Type::op); op.tag("FILTER"); reconstructed.push_back(op); A2 right("" + raw.substr(1) + "", Lexer::Type::string); right.tag("FILTER"); reconstructed.push_back(right); } else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterTags")); } } //////////////////////////////////////////////////////////////////////////////// void CLI2::findStrayModifications() { bool changes = false; auto command = getCommand(); if (command == "add" || command == "log") { for (auto& a : _args) { if (a.hasTag("FILTER")) { a.unTag("FILTER"); a.tag("MODIFICATION"); changes = true; } } } if (changes) if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter findStrayModifications")); } //////////////////////////////////////////////////////////////////////////////// // [.]:['"][]['"] --> name value void CLI2::desugarFilterAttributes() { bool changes = false; std::vector reconstructed; for (auto& a : _args) { if (a._lextype == Lexer::Type::pair && a.hasTag("FILTER")) { std::string raw = a.attribute("raw"); std::string name = a.attribute("name"); std::string mod = a.attribute("modifier"); std::string sep = a.attribute("separator"); std::string value = a.attribute("value"); // An unquoted string, while equivalent to an empty string, doesn't cause // an operand shortage in eval. if (value == "") value = "''"; // Some values are expressions, which need to be lexed. The best way to // determine whether an expression is either a single value, or needs to // be lexed, is to lex it and count the tokens. For example: // now+1d // This should be lexed and surrounded by parentheses: // ( // now // + // 1d // ) // Use this sequence in place of a single value. std::vector values = lexExpression(value); if (Context::getContext().config.getInteger("debug.parser") >= 2) { Context::getContext().debug("CLI2::lexExpression " + name + ':' + value); for (auto& v : values) Context::getContext().debug(" " + v.dump()); Context::getContext().debug(" "); } bool found = false; std::string canonical; if (canonicalize(canonical, "attribute", name)) { // Certain attribute types do not suport math. // string --> no // numeric --> yes // date --> yes // duration --> yes bool evalSupported = true; Column* col = Context::getContext().columns[canonical]; if (col && col->type() == "string") evalSupported = false; A2 lhs(name, Lexer::Type::dom); lhs.tag("FILTER"); lhs.attribute("canonical", canonical); lhs.attribute("modifier", mod); A2 op("", Lexer::Type::op); op.tag("FILTER"); // Attribute types that do not support evaluation should be interpreted // as strings (currently this means that string attributes are not evaluated) A2 rhs("", evalSupported ? values[0]._lextype : Lexer::Type::string); rhs.tag("FILTER"); // Special case for ':'. if (mod == "") { op.attribute("raw", "="); rhs.attribute("raw", value); } else if (mod == "before" || mod == "under" || mod == "below") { op.attribute("raw", "<"); rhs.attribute("raw", value); } else if (mod == "after" || mod == "over" || mod == "above") { op.attribute("raw", ">"); rhs.attribute("raw", value); } else if (mod == "by") { op.attribute("raw", "<="); rhs.attribute("raw", value); } else if (mod == "none") { op.attribute("raw", "=="); rhs.attribute("raw", "''"); } else if (mod == "any") { op.attribute("raw", "!=="); rhs.attribute("raw", "''"); } else if (mod == "is" || mod == "equals") { op.attribute("raw", "=="); rhs.attribute("raw", value); } else if (mod == "not") { op.attribute("raw", "!="); rhs.attribute("raw", value); } else if (mod == "isnt") { op.attribute("raw", "!=="); rhs.attribute("raw", value); } else if (mod == "has" || mod == "contains") { op.attribute("raw", "~"); rhs.attribute("raw", value); } else if (mod == "hasnt") { op.attribute("raw", "!~"); rhs.attribute("raw", value); } else if (mod == "startswith" || mod == "left") { op.attribute("raw", "~"); rhs.attribute("raw", "^" + value); } else if (mod == "endswith" || mod == "right") { op.attribute("raw", "~"); rhs.attribute("raw", value + "$"); } else if (mod == "word") { op.attribute("raw", "~"); #if defined(DARWIN) rhs.attribute("raw", value); #elif defined(SOLARIS) rhs.attribute("raw", "\\<" + value + "\\>"); #else rhs.attribute("raw", "\\b" + value + "\\b"); #endif } else if (mod == "noword") { op.attribute("raw", "!~"); #if defined(DARWIN) rhs.attribute("raw", value); #elif defined(SOLARIS) rhs.attribute("raw", "\\<" + value + "\\>"); #else rhs.attribute("raw", "\\b" + value + "\\b"); #endif } else throw format("Error: unrecognized attribute modifier '{1}'.", mod); reconstructed.push_back(lhs); reconstructed.push_back(op); // Do not modify this construct without full understanding. // Getting this wrong breaks a whole lot of filtering tests. if (evalSupported) { for (auto& v : values) reconstructed.push_back(v); } else if (Lexer::isDOM(rhs.attribute("raw"))) { rhs._lextype = Lexer::Type::dom; reconstructed.push_back(rhs); } else { reconstructed.push_back(rhs); } found = true; } // If the name does not canonicalize to either an attribute or a UDA // then it is not a recognized Lexer::Type::pair, so downgrade it to // Lexer::Type::word. else { a._lextype = Lexer::Type::word; } if (found) changes = true; else reconstructed.push_back(a); } // Not a FILTER pair. else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterAttributes")); } } //////////////////////////////////////////////////////////////////////////////// // /pattern/ --> description ~ 'pattern' void CLI2::desugarFilterPatterns() { bool changes = false; std::vector reconstructed; for (const auto& a : _args) { if (a._lextype == Lexer::Type::pattern && a.hasTag("FILTER")) { changes = true; A2 lhs("description", Lexer::Type::dom); lhs.tag("FILTER"); reconstructed.push_back(lhs); A2 op("~", Lexer::Type::op); op.tag("FILTER"); reconstructed.push_back(op); A2 rhs(a.attribute("pattern"), Lexer::Type::string); rhs.attribute("flags", a.attribute("flags")); rhs.tag("FILTER"); reconstructed.push_back(rhs); } else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPatterns")); } } //////////////////////////////////////////////////////////////////////////////// // An ID sequence can be: // // a single ID: 1 // a list of IDs: 1,3,5 // a list of IDs: 1 3 5 // a range: 5-10 // or a combination: 1,3,5-10 12 // void CLI2::findIDs() { bool changes = false; if (Context::getContext().config.getBoolean("sugar")) { bool previousFilterArgWasAnOperator = false; int filterCount = 0; for (const auto& a : _args) { if (a.hasTag("FILTER")) { ++filterCount; if (a._lextype == Lexer::Type::number) { // Skip any number that was preceded by an operator. if (!previousFilterArgWasAnOperator) { changes = true; std::string number = a.attribute("raw"); _id_ranges.emplace_back(number, number); } } else if (a._lextype == Lexer::Type::set) { // Split the ID list into elements. auto elements = split(a.attribute("raw"), ','); for (auto& element : elements) { changes = true; auto hyphen = element.find('-'); if (hyphen != std::string::npos) _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1)); else _id_ranges.emplace_back(element, element); } } std::string raw = a.attribute("raw"); previousFilterArgWasAnOperator = (a._lextype == Lexer::Type::op && raw != "(" && raw != ")") ? true : false; } } // If no IDs were found, and no filter was specified, look for number/set // listed as a MODIFICATION. std::string command = getCommand(); if (!_id_ranges.size() && filterCount == 0 && command != "add" && command != "log") { for (auto& a : _args) { if (a.hasTag("MODIFICATION")) { std::string raw = a.attribute("raw"); // For a number to be an ID, it must not contain any sign or floating // point elements. if (a._lextype == Lexer::Type::number && raw.find('.') == std::string::npos && raw.find('e') == std::string::npos && raw.find('-') == std::string::npos) { changes = true; a.unTag("MODIFICATION"); a.tag("FILTER"); _id_ranges.emplace_back(raw, raw); } else if (a._lextype == Lexer::Type::set) { a.unTag("MODIFICATION"); a.tag("FILTER"); // Split the ID list into elements. auto elements = split(raw, ','); for (const auto& element : elements) { changes = true; auto hyphen = element.find('-'); if (hyphen != std::string::npos) _id_ranges.emplace_back(element.substr(0, hyphen), element.substr(hyphen + 1)); else _id_ranges.emplace_back(element, element); } } } } } } // Sugar-free. else { std::vector reconstructed; for (const auto& a : _args) { if (a.hasTag("FILTER") && a._lextype == Lexer::Type::number) { changes = true; A2 pair("id:" + a.attribute("raw"), Lexer::Type::pair); pair.tag("FILTER"); pair.decompose(); reconstructed.push_back(pair); } else reconstructed.push_back(a); } if (changes) _args = reconstructed; } if (changes) if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter findIDs")); } //////////////////////////////////////////////////////////////////////////////// void CLI2::findUUIDs() { bool changes = false; if (Context::getContext().config.getBoolean("sugar")) { for (const auto& a : _args) { if (a._lextype == Lexer::Type::uuid && a.hasTag("FILTER")) { changes = true; _uuid_list.push_back(a.attribute("raw")); } } if (!_uuid_list.size()) { for (auto& a : _args) { if (a._lextype == Lexer::Type::uuid && a.hasTag("MODIFICATION")) { changes = true; a.unTag("MODIFICATION"); a.tag("FILTER"); _uuid_list.push_back(a.attribute("raw")); } } } } // Sugar-free. else { std::vector reconstructed; for (const auto& a : _args) { if (a.hasTag("FILTER") && a._lextype == Lexer::Type::uuid) { changes = true; A2 pair("uuid:" + a.attribute("raw"), Lexer::Type::pair); pair.tag("FILTER"); pair.decompose(); reconstructed.push_back(pair); } else reconstructed.push_back(a); } if (changes) _args = reconstructed; } if (changes) if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter findUUIDs")); } //////////////////////////////////////////////////////////////////////////////// void CLI2::insertIDExpr() { // Skip completely if no ID/UUID was found. This is because below, '(' and ')' // are inserted regardless of list size. if (!_id_ranges.size() && !_uuid_list.size()) return; // Find the *first* occurence of lexer type set/number/uuid, and replace it // with a synthesized expression. All other occurences are eaten. bool changes = false; bool foundID = false; std::vector reconstructed; for (const auto& a : _args) { if ((a._lextype == Lexer::Type::set || a._lextype == Lexer::Type::number || a._lextype == Lexer::Type::uuid) && a.hasTag("FILTER")) { if (!foundID) { foundID = true; changes = true; // Construct a single sequence that represents all _id_ranges and // _uuid_list in one clause. This is essentially converting this: // // 1,2-3 uuid,uuid uuid 4 // // into: // // ( // ( id == 1 ) // or ( ( id >= 2 ) and ( id <= 3 ) ) // or ( id == 4 ) // or ( uuid = $UUID ) // or ( uuid = $UUID ) // ) // Building block operators. A2 openParen("(", Lexer::Type::op); openParen.tag("FILTER"); A2 closeParen(")", Lexer::Type::op); closeParen.tag("FILTER"); A2 opOr("or", Lexer::Type::op); opOr.tag("FILTER"); A2 opAnd("and", Lexer::Type::op); opAnd.tag("FILTER"); A2 opSimilar("=", Lexer::Type::op); opSimilar.tag("FILTER"); A2 opEqual("==", Lexer::Type::op); opEqual.tag("FILTER"); A2 opGTE(">=", Lexer::Type::op); opGTE.tag("FILTER"); A2 opLTE("<=", Lexer::Type::op); opLTE.tag("FILTER"); // Building block attributes. A2 argID("id", Lexer::Type::dom); argID.tag("FILTER"); A2 argUUID("uuid", Lexer::Type::dom); argUUID.tag("FILTER"); reconstructed.push_back(openParen); // Add all ID ranges. for (auto r = _id_ranges.begin(); r != _id_ranges.end(); ++r) { if (r != _id_ranges.begin()) reconstructed.push_back(opOr); if (r->first == r->second) { reconstructed.push_back(openParen); reconstructed.push_back(argID); reconstructed.push_back(opEqual); A2 value(r->first, Lexer::Type::number); value.tag("FILTER"); reconstructed.push_back(value); reconstructed.push_back(closeParen); } else { bool ascending = true; int low = strtol(r->first.c_str(), nullptr, 10); int high = strtol(r->second.c_str(), nullptr, 10); if (low <= high) ascending = true; else ascending = false; reconstructed.push_back(openParen); reconstructed.push_back(argID); reconstructed.push_back(opGTE); A2 startValue((ascending ? r->first : r->second), Lexer::Type::number); startValue.tag("FILTER"); reconstructed.push_back(startValue); reconstructed.push_back(opAnd); reconstructed.push_back(argID); reconstructed.push_back(opLTE); A2 endValue((ascending ? r->second : r->first), Lexer::Type::number); endValue.tag("FILTER"); reconstructed.push_back(endValue); reconstructed.push_back(closeParen); } } // Combine the ID and UUID sections with 'or'. if (_id_ranges.size() && _uuid_list.size()) reconstructed.push_back(opOr); // Add all UUID list items. for (auto u = _uuid_list.begin(); u != _uuid_list.end(); ++u) { if (u != _uuid_list.begin()) reconstructed.push_back(opOr); reconstructed.push_back(openParen); reconstructed.push_back(argUUID); reconstructed.push_back(opSimilar); A2 value(*u, Lexer::Type::string); value.tag("FILTER"); reconstructed.push_back(value); reconstructed.push_back(closeParen); } reconstructed.push_back(closeParen); } // No 'else' because all set/number/uuid args but the first are removed. } else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter insertIDExpr")); } } //////////////////////////////////////////////////////////////////////////////// // FILTER Lexer::Type::word args will become part of an expression, and so they // need to be Lexed. void CLI2::lexFilterArgs() { bool changes = false; std::vector reconstructed; for (const auto& a : _args) { if (a._lextype == Lexer::Type::word && a.hasTag("FILTER")) { changes = true; std::string lexeme; Lexer::Type type; Lexer lex(a.attribute("raw")); while (lex.token(lexeme, type)) { A2 extra(lexeme, type); extra.tag("FILTER"); reconstructed.push_back(extra); } } else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter lexFilterArgs")); } } //////////////////////////////////////////////////////////////////////////////// // FILTER, Lexer::Type::word args are treated as search terms. // // Algorithm: // Given: // - task ... argX candidate argY // Where: // - neither argX nor argY are an operator, except (, ), and, or, xor // - candidate is one of: Lexer::Type::word // Lexer::Type::identifier // Lexer::Type::date // void CLI2::desugarFilterPlainArgs() { // First walk the arg list looking for plain words that are not part of an // existing expression. auto prevprev = &_args[0]; auto prev = &_args[0]; for (auto& a : _args) { auto raw = a.attribute("raw"); auto praw = prev->attribute("raw"); auto ppraw = prevprev->attribute("raw"); if ((prevprev->_lextype != Lexer::Type::op || // argX ppraw == "(" || ppraw == ")" || ppraw == "and" || ppraw == "or" || ppraw == "xor") && (prev->_lextype == Lexer::Type::identifier || // candidate prev->_lextype == Lexer::Type::date || // candidate prev->_lextype == Lexer::Type::word) && // candidate prev->hasTag("FILTER") && // candidate (a._lextype != Lexer::Type::op || // argY raw == "(" || raw == ")" || raw == "and" || raw == "or" || raw == "xor")) { prev->tag("PLAIN"); } prevprev = prev; prev = &a; } // Cover the case where the *last* argument is a plain arg. auto& penultimate = _args[_args.size() - 2]; auto praw = penultimate.attribute("raw"); auto& last = _args[_args.size() - 1]; if ((penultimate._lextype != Lexer::Type::op || // argX praw == "(" || praw == ")" || praw == "and" || praw == "or" || praw == "xor") && (last._lextype == Lexer::Type::identifier || // candidate last._lextype == Lexer::Type::word) && // candidate last.hasTag("FILTER")) // candidate { last.tag("PLAIN"); } // Walk the list again, upgrading PLAIN args. bool changes = false; std::vector reconstructed; for (const auto& a : _args) { if (a.hasTag("PLAIN")) { changes = true; A2 lhs("description", Lexer::Type::dom); lhs.attribute("canonical", "description"); lhs.tag("FILTER"); lhs.tag("PLAIN"); reconstructed.push_back(lhs); A2 op("~", Lexer::Type::op); op.tag("FILTER"); op.tag("PLAIN"); reconstructed.push_back(op); std::string word = a.attribute("raw"); Lexer::dequote(word); A2 rhs(word, Lexer::Type::string); rhs.tag("FILTER"); rhs.tag("PLAIN"); reconstructed.push_back(rhs); } else reconstructed.push_back(a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter desugarFilterPlainArgs")); } } //////////////////////////////////////////////////////////////////////////////// // Detects if the bracket at iterator it is a start or end of an empty paren expression // Examples: // ( status = pending ) ( ) // ^ // it -----| => true // // ( status = pending ) ( project = Home ) // ^ // it -----| => false bool CLI2::isEmptyParenExpression(std::vector::iterator it, bool forward /* = true */) const { int open = 0; int closed = 0; for (auto a = it; a != (forward ? _args.end() : _args.begin()); (forward ? ++a : --a)) { if (a->attribute("raw") == "(") open++; else if (a->attribute("raw") == ")") closed++; else // Encountering a non-paren token means there is something between parenthees return false; // Getting balanced parentheses means we have an empty paren expression if (open == closed && open != 0) return true; } // Should not end here. return false; } //////////////////////////////////////////////////////////////////////////////// // Two consecutive FILTER, non-OP arguments that are not "(" or ")" need an // "and" operator inserted between them. // // ) --> ) and // ( --> ( // ) ( --> ) and ( // --> and // void CLI2::insertJunctions() { bool changes = false; std::vector reconstructed; auto prev = _args.begin(); for (auto a = _args.begin(); a != _args.end(); ++a) { if (a->hasTag("FILTER")) { // The prev iterator should be the first FILTER arg. if (prev == _args.begin()) prev = a; // Insert AND between terms. else if (a != prev) { if ((prev->_lextype != Lexer::Type::op && a->attribute("raw") == "(" && !isEmptyParenExpression(a, true)) || (prev->attribute("raw") == ")" && a->_lextype != Lexer::Type::op && !isEmptyParenExpression(prev, false)) || (prev->attribute("raw") == ")" && a->attribute("raw") == "(" && !isEmptyParenExpression(a, true) && !isEmptyParenExpression(prev, false)) || (prev->_lextype != Lexer::Type::op && a->_lextype != Lexer::Type::op)) { A2 opOr("and", Lexer::Type::op); opOr.tag("FILTER"); reconstructed.push_back(opOr); changes = true; } } // Previous FILTER arg. prev = a; } reconstructed.push_back(*a); } if (changes) { _args = reconstructed; if (Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::prepareFilter insertJunctions")); } } //////////////////////////////////////////////////////////////////////////////// // Look for situations that require defaults: // // 1. If no command was found, and no ID/UUID, and if rc.default.command is // configured, inject the lexed tokens from rc.default.command. // // 2. If no command was found, but an ID/UUID was found, then assume a command // of 'information'. // void CLI2::defaultCommand() { // Scan the top-level branches for evidence of ID, UUID, overrides and other // arguments. bool changes = false; bool found_command = false; bool found_sequence = false; for (const auto& a : _args) { std::string raw = a.attribute("raw"); if (a.hasTag("CMD")) found_command = true; if (a._lextype == Lexer::Type::uuid || a._lextype == Lexer::Type::number) found_sequence = true; } // If no command was specified, then a command will be inserted. if (!found_command) { // Default command. if (!found_sequence) { // Apply overrides, if any. std::string defaultCommand = Context::getContext().config.get("default.command"); if (defaultCommand != "") { // Modify _args, _original_args to be: // [ ...] [...] std::vector reconstructedOriginals{_original_args[0]}; std::vector reconstructed{_args[0]}; std::string lexeme; Lexer::Type type; Lexer lex(defaultCommand); while (lex.token(lexeme, type)) { reconstructedOriginals.emplace_back(lexeme, type); A2 cmd(lexeme, type); cmd.tag("DEFAULT"); reconstructed.push_back(cmd); } for (unsigned int i = 1; i < _original_args.size(); ++i) reconstructedOriginals.push_back(_original_args[i]); for (unsigned int i = 1; i < _args.size(); ++i) reconstructed.push_back(_args[i]); _original_args = reconstructedOriginals; _args = reconstructed; changes = true; } } else { A2 info("information", Lexer::Type::word); info.tag("ASSUMED"); _args.push_back(info); changes = true; } } if (changes && Context::getContext().config.getInteger("debug.parser") >= 2) Context::getContext().debug(dump("CLI2::analyze defaultCommand")); } //////////////////////////////////////////////////////////////////////////////// // Some values are expressions, which need to be lexed. The best way to // determine whether an expression is either a single value, or needs to be // lexed, is to lex it and count the tokens. For example: // now+1d // This should be lexed and surrounded by parentheses: // ( // now // + // 1d // ) std::vector CLI2::lexExpression(const std::string& expression) { std::vector lexed; std::string lexeme; Lexer::Type type; Lexer lex(expression); while (lex.token(lexeme, type)) { A2 token(lexeme, type); token.tag("FILTER"); lexed.push_back(token); } // If there were multiple tokens, parenthesize, because this expression will // be used as a value. if (lexed.size() > 1) { A2 openParen("(", Lexer::Type::op); openParen.tag("FILTER"); A2 closeParen(")", Lexer::Type::op); closeParen.tag("FILTER"); lexed.insert(lexed.begin(), openParen); lexed.push_back(closeParen); } return lexed; } //////////////////////////////////////////////////////////////////////////////// // vim: ts=2:sw=2