diff --git a/src/A3.cpp b/src/A3.cpp new file mode 100644 index 000000000..73bed1012 --- /dev/null +++ b/src/A3.cpp @@ -0,0 +1,1506 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +//#include +//#include +#include +#include +#include +#include +#include +#include + +extern Context context; + +// Supported modifiers, synonyms on the same line. +/* +static const char* modifierNames[] = +{ + "before", "under", "below", + "after", "over", "above", + "none", + "any", + "is", "equals", + "isnt", "not", + "has", "contains", + "hasnt", + "startswith", "left", + "endswith", "right", + "word", + "noword" +}; +*/ +// Supported operators, borrowed from C++, particularly the precedence. +// Note: table is sorted by length of operator string, so searches match +// longest first. +static struct +{ + std::string op; + int precedence; + char type; + int symbol; + char associativity; +} operators[] = +{ + // Operator Precedence Type Symbol Associativity + { "and", 5, 'b', 0, 'l' }, // Conjunction + { "xor", 4, 'b', 0, 'l' }, // Disjunction + + { "or", 3, 'b', 0, 'l' }, // Disjunction + { "<=", 10, 'b', 1, 'l' }, // Less than or equal + { ">=", 10, 'b', 1, 'l' }, // Greater than or equal + { "!~", 9, 'b', 1, 'l' }, // Regex non-match + { "!=", 9, 'b', 1, 'l' }, // Inequal + + { "=", 9, 'b', 1, 'l' }, // Equal +// { "^", 16, 'b', 1, 'r' }, // Exponent + { ">", 10, 'b', 1, 'l' }, // Greater than + { "~", 9, 'b', 1, 'l' }, // Regex match + { "!", 15, 'u', 1, 'r' }, // Not +// { "-", 15, 'u', 1, 'r' }, // Unary minus + { "*", 13, 'b', 1, 'l' }, // Multiplication + { "/", 13, 'b', 1, 'l' }, // Division +// { "%", 13, 'b', 1, 'l' }, // Modulus + { "+", 12, 'b', 1, 'l' }, // Addition + { "-", 12, 'b', 1, 'l' }, // Subtraction + { "<", 10, 'b', 1, 'l' }, // Less than + { "(", 0, 'b', 1, 'l' }, // Precedence start + { ")", 0, 'b', 1, 'l' }, // Precedence end +}; + +//#define NUM_MODIFIER_NAMES (sizeof (modifierNames) / sizeof (modifierNames[0])) +#define NUM_OPERATORS (sizeof (operators) / sizeof (operators[0])) + +//static const char* non_word_chars = " +-*/%()=<>!~"; + +//////////////////////////////////////////////////////////////////////////////// +A3::A3 () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +A3::~A3 () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +// Add an Arg with a blank category for every argv. +void A3::capture (int argc, const char** argv) +{ + for (int i = 0; i < argc; ++i) + this->push_back (Arg (argv[i], "")); +} + +//////////////////////////////////////////////////////////////////////////////// +// Append an Arg with a blank category. +void A3::capture (const std::string& arg) +{ + std::vector parts; + this->push_back (Arg (arg, "")); +} + +//////////////////////////////////////////////////////////////////////////////// +// Prepend a Arg with a blank category. +void A3::capture_first (const std::string& arg) +{ + // Break the new argument into parts that comprise a series. + std::vector series; + series.push_back (Arg (arg, "")); + + // Locate an appropriate place to insert the series. This would be + // immediately after the program and command arguments. + std::vector ::iterator position; + for (position = this->begin (); position != this->end (); ++position) + if (position->_category != "program" && + position->_category != "command") + break; + + this->insert (position, series.begin (), series.end ()); +} + +//////////////////////////////////////////////////////////////////////////////// +// Scan all arguments and categorize them as: +// program +// rc +// override +// command +// terminator +// word +// +void A3::categorize () +{ + bool terminated = false; + bool found_command = false; + + // Generate a vector of command keywords against which autoComplete can run. + std::vector keywords = context.getCommands (); + + // Now categorize every argument. + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (!terminated) + { + // Nothing after -- is to be interpreted in any way. + if (arg->_raw == "--") + { + terminated = true; + arg->_category = "terminator"; + } + + // program + else if (arg == this->begin ()) + { + arg->_category = "program"; + + if ((arg->_raw.length () >= 3 && + arg->_raw.substr (arg->_raw.length () - 3) == "cal") || + (arg->_raw.length () >= 8 && + arg->_raw.substr (arg->_raw.length () - 8) == "calendar")) + { + arg->_raw = "calendar"; + arg->_category = "command"; + found_command = true; + } + } + + // command + else if (!found_command && + is_command (keywords, arg->_raw)) + { + found_command = true; + arg->_category = "command"; + } + + // rc: + // Note: This doesn't break a sequence chain. + else if (arg->_raw.substr (0, 3) == "rc:") + arg->_category = "rc"; + + // rc.: + // Note: This doesn't break a sequence chain. + else if (arg->_raw.substr (0, 3) == "rc.") + arg->_category = "override"; + + // If the type is not known, it is treated as a generic word. + else + arg->_category = "word"; + } + + // All post-termination arguments are simply words. + else + arg->_category = "word"; + } +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_command ( + const std::vector & keywords, + std::string& command) +{ + std::vector matches; + if (autoComplete (command, + keywords, + matches, + context.config.getInteger ("abbreviation.minimum")) == 1) + { + command = matches[0]; + return true; + } + + return false; +} + + + + + + + + +#ifdef NOPE +//////////////////////////////////////////////////////////////////////////////// +// Add a pair for every word from std::cin, with a category of "". +void A3::append_stdin () +{ + bool something_happened = false; + + // Use 'select' to determine whether there is any std::cin content buffered + // before trying to read it, to prevent blocking. + struct timeval tv; + fd_set fds; + tv.tv_sec = 0; + tv.tv_usec = 0; + FD_ZERO (&fds); + FD_SET (STDIN_FILENO, &fds); + select (STDIN_FILENO + 1, &fds, NULL, NULL, &tv); + if (FD_ISSET (0, &fds)) + { + std::string arg; + while (std::cin >> arg) + { + // It the terminator token is found, stop reading. + if (arg == "--") + break; + + this->push_back (Arg (arg, "", "")); + something_happened = true; + } + } + + if (something_happened) + categorize (); +} + +//////////////////////////////////////////////////////////////////////////////// +void A3::rc_override ( + std::string& home, + File& rc) +{ + // Is there an override for rc:? + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg->_third == "rc") + { + rc = File (arg->_first.substr (3)); + home = rc; + + std::string::size_type last_slash = rc.data.rfind ("/"); + if (last_slash != std::string::npos) + home = rc.data.substr (0, last_slash); + else + home = "."; + + context.header ("Using alternate .taskrc file " + rc.data); + + // Keep scanning, because if there are multiple rc:file arguments, we + // want the last one to dominate. + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +void A3::get_data_location (std::string& data) +{ + std::string location = context.config.get ("data.location"); + if (location != "") + data = location; + + // Are there any overrides for data.location? + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg->_third == "override") + { + if (arg->_first.substr (0, 16) == "rc.data.location" && + arg->_first[16] == ':') + { + data = arg->_first.substr (17); + context.header ("Using alternate data.location " + data); + } + } + + // Keep scanning, because if there are multiple rc:file arguments, we + // want the last one to dominate. + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Extracts any rc.name:value args and sets the name/value in context.config, +// leaving only the plain args. +void A3::apply_overrides () +{ + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg->_third == "override") + { + std::string name; + std::string value; + Nibbler n (arg->_first); + if (n.getLiteral ("rc.") && // rc. + n.getUntil (':', name) && // xxx + n.skip (':')) // : + { + n.getUntilEOS (value); // May be blank. + + context.config.set (name, value); + context.footnote ("Configuration override rc." + name + ":" + value); + } + else + context.footnote ("Problem with override: " + arg->_first); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// An alias must be a distinct word on the command line. +// Aliases may not recurse. +void A3::resolve_aliases () +{ + std::vector expanded; + bool something; + int safety_valve = 10; + + do + { + something = false; + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + std::map ::iterator match = + context.aliases.find (arg->_first); + + if (match != context.aliases.end ()) + { + context.debug (std::string ("A3::resolve_aliases '") + + arg->_first + + "' --> '" + + context.aliases[arg->_first] + + "'"); + + std::vector words; + splitq (words, context.aliases[arg->_first], ' '); + + std::vector ::iterator word; + for (word = words.begin (); word != words.end (); ++word) + expanded.push_back (*word); + + something = true; + } + else + expanded.push_back (arg->_first); + } + + // Only overwrite if something happened. + if (something) + { + this->clear (); + std::vector ::iterator e; + for (e = expanded.begin (); e != expanded.end (); ++e) + this->push_back (Arg (*e, "", "")); + + expanded.clear (); + categorize (); + } + } + while (something && --safety_valve > 0); +} + +//////////////////////////////////////////////////////////////////////////////// +// These are some delicate heuristics here. Tread lightly. +void A3::inject_defaults () +{ + // Scan the arguments and detect what is present. + bool found_command = false; + bool found_sequence = false; + bool found_other = false; + + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg->_third == "command") + found_command = true; + + else if (arg->_third == "id" || + arg->_third == "uuid") + found_sequence = true; + + else if (arg->_third != "program" && + arg->_third != "override" && + arg->_third != "rc") + found_other = 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.config.get ("default.command"); + if (defaultCommand != "") + { + capture_first (defaultCommand); + context.header ("[" + combine () + "]"); + } + else + throw std::string (STRING_TRIVIAL_INPUT); + } + else + { + // Modify command. + if (found_other) + { + capture_first ("modify"); + } + + // Information command. + else + { + context.header (STRING_ASSUME_INFO); + capture_first ("information"); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +std::vector A3::list () +{ + std::vector all; + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + all.push_back (arg->_first); + + return all; +} + +//////////////////////////////////////////////////////////////////////////////// +std::vector A3::operator_list () +{ + std::vector all; + for (unsigned int i = 0; i < NUM_OPERATORS; ++i) + all.push_back (operators[i].op); + + return all; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string A3::combine () +{ + std::string combined; + + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg != this->begin ()) + combined += " "; + + combined += arg->_first; + } + + return combined; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::find_command (std::string& command) +{ + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + if (arg->_third == "command") + { + command = arg->_first; + return true; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +std::string A3::find_limit () +{ + std::vector ::reverse_iterator arg; + for (arg = this->rbegin (); arg != this->rend (); ++arg) + if (arg->_first.find ("limit:") != std::string::npos) + return arg->_first.substr (6); + + return ""; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_multipart ( + const std::string& input, + std::vector & parts) +{ + parts.clear (); + Nibbler n (input); + std::string part; + while (n.getQuoted ('"', part) || + n.getQuoted ('\'', part) || +// n.getQuoted ('/', part) || <--- this line breaks subst. + n.getUntilWS (part)) + { + n.skipWS (); + parts.push_back (part); + } + + return parts.size () > 1 ? true : false; +} + +//////////////////////////////////////////////////////////////////////////////// +// :['"][]['"] +bool A3::is_attr (const std::string& input) +{ + Nibbler n (input); + std::string name; + std::string value; + + if (n.getUntilOneOf ("=:", name)) + { + if (name.length () == 0) + return false; + + if (name.find_first_of (non_word_chars) != std::string::npos) + return false; + + if (n.skip (':')) + { + // Exclude certain URLs, that look like attrs. + if (input.find ('@') <= n.cursor () || + input.find ('/') <= n.cursor ()) + return false; + + // Both quoted and unquoted Att's are accepted. + // Consider removing this for a stricter parse. + if (n.getQuoted ('"', value) || + n.getQuoted ('\'', value) || + n.getUntil (' ', value) || + n.getUntilEOS (value) || + n.depleted ()) + { + // Validate and canonicalize attribute name. + if (is_attribute (name, name)) + return true; + } + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// .:['"]['"] +bool A3::is_attmod (const std::string& input) +{ + Nibbler n (input); + std::string name; + std::string modifier; + std::string value; + + if (n.getUntilOneOf (".", name)) + { + if (name.length () == 0) + return false; + + if (name.find_first_of (non_word_chars) != std::string::npos) + return false; + + if (n.skip ('.')) + { + n.skip ('~'); + n.getUntil (':', modifier); + + if (modifier.length () == 0) + return false; + } + + if (n.skip (':')) + { + // Exclude certain URLs, that look like attrs. + if (input.find ('@') <= n.cursor () || + input.find ('/') <= n.cursor ()) + return false; + + // Both quoted and unquoted Att's are accepted. + // Consider removing this for a stricter parse. + if (n.getQuoted ('"', value) || + n.getQuoted ('\'', value) || + n.getUntil (' ', value) || + n.getUntilEOS (value) || + n.depleted ()) + { + // Validate and canonicalize attribute and modifier names. + if (is_attribute (name, name) && + is_modifier (modifier)) + return true; + } + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// ///[g] +// +// Note: one problem with this is that substitutions start with a /, and so any +// two-directory absolute path, (or three-level, if the third directory is +// named 'g') can be misinterpreted. To help (but not solve) this, if a +// substition exists on the local disk, it is not considered a subst. +// This needs to be changed to a better solution. +bool A3::is_subst (const std::string& input) +{ + std::string from; + std::string to; + Nibbler n (input); + if (n.skip ('/') && + n.getUntil ('/', from) && + from.length () && + n.skip ('/') && + n.getUntil ('/', to) && + n.skip ('/')) + { + n.skip ('g'); + if (n.depleted () && + ! Directory (input).exists ()) // Ouch - expensive call. + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// // +bool A3::is_pattern (const std::string& input) +{ + std::string::size_type first = input.find ('/', 0); + std::string::size_type second = input.find ('/', first + 1); + std::string::size_type third = input.find ('/', second + 1); + + if (first == 0 && + second == input.length () - 1 && + third == std::string::npos && + second > 1) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// [-][,[-]] +bool A3::is_id (const std::string& input) +{ + Nibbler n (input); + int id; + + if (n.getUnsignedInt (id)) + { + if (n.skip ('-')) + { + if (!n.getUnsignedInt (id)) + return false; + } + + while (n.skip (',')) + { + if (n.getUnsignedInt (id)) + { + if (n.skip ('-')) + { + if (!n.getUnsignedInt (id)) + return false; + } + } + else + return false; + } + } + else + return false; + + return n.depleted (); +} + +//////////////////////////////////////////////////////////////////////////////// +// [,...] +bool A3::is_uuid (const std::string& input) +{ + Nibbler n (input); + std::string uuid; + + if (n.getUUID (uuid)) + { + while (n.skip (',')) + { + if (!n.getUUID (uuid)) + return false; + } + } + else + return false; + + return n.depleted (); +} + +//////////////////////////////////////////////////////////////////////////////// +// [+-] +bool A3::is_tag (const std::string& input) +{ + if (input.length () > 1 && + (input[0] == '+' || + input[0] == '-') && + noSpaces (input) && + input.find ('+', 1) == std::string::npos && + input.find ('-', 1) == std::string::npos) + { + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_operator (const std::string& input) +{ + for (unsigned int i = 0; i < NUM_OPERATORS; ++i) + if (operators[i].op == input) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_operator ( + const std::string& input, + char& type, + int& precedence, + char& associativity) +{ + for (unsigned int i = 0; i < NUM_OPERATORS; ++i) + if (operators[i].op == input) + { + type = operators[i].type; + precedence = operators[i].precedence; + associativity = operators[i].associativity; + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_symbol_operator (const std::string& input) +{ + for (unsigned int i = 0; i < NUM_OPERATORS; ++i) + if (operators[i].symbol && + operators[i].op == input) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// Canonicalize attribute names. +bool A3::is_attribute (const std::string& input, std::string& canonical) +{ + // Guess at the full attribute name. + std::vector candidates; + for (unsigned i = 0; i < NUM_ATT_NAMES; ++i) + { + // Short-circuit: exact matches cause immediate return. + if (attributeNames[i] == input) + { + canonical = input; + return true; + } + + candidates.push_back (attributeNames[i]); + } + + for (unsigned i = 0; i < NUM_MODIFIABLE_ATT_NAMES; ++i) + { + // Short-circuit: exact matches cause immediate return. + if (modifiableAttributeNames[i] == input) + { + canonical = input; + return true; + } + + candidates.push_back (modifiableAttributeNames[i]); + } + + std::vector matches; + autoComplete (input, + candidates, + matches, + context.config.getInteger ("abbreviation.minimum")); + + if (matches.size () == 1) + { + canonical = matches[0]; + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_modifier (const std::string& input) +{ + // Guess at the full attribute name. + std::vector candidates; + for (unsigned i = 0; i < NUM_MODIFIER_NAMES; ++i) + { + // Short-circuit: exact matches cause immediate return. + if (modifierNames[i] == input) + return true; + + candidates.push_back (modifierNames[i]); + } + + std::vector matches; + autoComplete (input, + candidates, + matches, + context.config.getInteger ("abbreviation.minimum")); + + if (matches.size () == 1) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::is_expression (const std::string& input) +{ + std::string unquoted = unquoteText (input); + + // Look for space-separated operators. + std::vector tokens; + split (tokens, unquoted, ' '); + std::vector ::iterator token; + for (token = tokens.begin (); token != tokens.end (); ++token) + if (is_operator (*token)) + return true; + + // Look for bare or cuddled operators. + Lexer lexer (unquoted); + lexer.skipWhitespace (true); + lexer.coalesceAlpha (true); + lexer.coalesceDigits (true); + lexer.coalesceQuoted (true); + + tokens.clear (); + lexer.tokenize (tokens); + + for (token = tokens.begin (); token != tokens.end (); ++token) + if (is_operator (*token)) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// :['"]['"] +bool A3::extract_attr ( + const std::string& input, + std::string& name, + std::string& value) +{ + Nibbler n (input); + + // Ensure a clean parse. + name = ""; + value = ""; + + if (n.getUntil (':', name)) + { + if (name.length () == 0) + throw std::string ("Missing attribute name"); + + if (n.skip (':')) + { + // Both quoted and unquoted Att's are accepted. + // Consider removing this for a stricter parse. + if (n.getQuoted ('"', value) || + n.getQuoted ('\'', value) || + n.getUntilEOS (value)) + { + return true; + } + } + else + throw std::string ("Missing : after attribute name."); + } + else + throw std::string ("Missing : after attribute name."); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// .:['"]['"] +bool A3::extract_attmod ( + const std::string& input, + std::string& name, + std::string& modifier, + std::string& value, + std::string& sense) +{ + Nibbler n (input); + + // Ensure a clean parse. + name = ""; + value = ""; + modifier = ""; + sense = "positive"; + + if (n.getUntil (".", name)) + { + if (name.length () == 0) + throw std::string ("Missing attribute name"); + + if (n.skip ('.')) + { + if (n.skip ('~')) + sense = "negative"; + + if (n.getUntil (':', modifier)) + { + if (!A3::valid_modifier (modifier)) + throw std::string ("The name '") + modifier + "' is not a valid modifier."; + } + else + throw std::string ("Missing . or : after modifier."); + } + else + throw std::string ("Missing modifier."); + + if (n.skip (':')) + { + // Both quoted and unquoted Att's are accepted. + // Consider removing this for a stricter parse. + if (n.getQuoted ('"', value) || + n.getQuoted ('\'', value) || + n.getUntilEOS (value)) + { + return true; + } + } + else + throw std::string ("Missing : after attribute name."); + } + else + throw std::string ("Missing : after attribute name."); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::extract_subst ( + const std::string& input, + std::string& from, + std::string& to, + bool& global) +{ + Nibbler n (input); + if (n.skip ('/') && + n.getUntil ('/', from) && + n.skip ('/') && + n.getUntil ('/', to) && + n.skip ('/')) + { + global = n.skip ('g'); + + if (from == "") + throw std::string ("Cannot substitute an empty string."); + + if (!n.depleted ()) + throw std::string ("Unrecognized character(s) at end of substitution."); + + return true; + } + else + throw std::string ("Malformed substitution."); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::extract_pattern (const std::string& input, std::string& pattern) +{ + Nibbler n (input); + if (n.skip ('/') && + n.getUntil ('/', pattern) && + n.skip ('/')) + { + if (pattern == "") + throw std::string ("Cannot search for an empty pattern."); + + if (!n.depleted ()) + throw std::string ("Unrecognized character(s) at end of pattern."); + + return true; + } + else + throw std::string ("Malformed pattern."); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// A 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 +// +// If a sequence is followed by a non-number, then subsequent numbers are not +// interpreted as IDs. For example: +// +// 1 2 three 4 +// +// The sequence is "1 2". +// +// The _first number found in the command line is assumed to be a sequence. If +// there are two sequences, only the _first is recognized, for example: +// +// 1,2 three 4,5 +// +// The sequence is "1,2". +// +bool A3::extract_id (const std::string& input, std::vector & sequence) +{ + Nibbler n (input); + + int id; + + if (n.getUnsignedInt (id)) + { + sequence.push_back (id); + + if (n.skip ('-')) + { + int end; + if (!n.getUnsignedInt (end)) + throw std::string ("Unrecognized ID after hyphen."); + + if (id > end) + throw std::string ("Inverted range 'high-low' instead of 'low-high'"); + + for (int n = id + 1; n <= end; ++n) + sequence.push_back (n); + } + + while (n.skip (',')) + { + if (n.getUnsignedInt (id)) + { + sequence.push_back (id); + + if (n.skip ('-')) + { + int end; + if (!n.getUnsignedInt (end)) + throw std::string ("Unrecognized ID after hyphen."); + + if (id > end) + throw std::string ("Inverted range 'high-low' instead of 'low-high'"); + + for (int n = id + 1; n <= end; ++n) + sequence.push_back (n); + } + } + else + throw std::string ("Malformed ID"); + } + } + else + throw std::string ("Malformed ID"); + + return n.depleted (); +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::extract_uuid ( + const std::string& input, + std::vector & sequence) +{ + Nibbler n (input); + + std::string uuid; + if (n.getUUID (uuid)) + { + sequence.push_back (uuid); + + while (n.skip (',')) + { + if (!n.getUUID (uuid)) + throw std::string ("Unrecognized UUID after comma."); + + sequence.push_back (uuid); + } + } + else + throw std::string ("Malformed UUID"); + + if (!n.depleted ()) + throw std::string ("Unrecognized character(s) at end of pattern."); + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::extract_tag ( + const std::string& input, + char& type, + std::string& tag) +{ + if (input.length () > 1) + { + type = input[0]; + tag = input.substr (1); + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::extract_operator ( + const std::string& input, + std::string& op) +{ + op = input; + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// Almost all arguments are filters, except: +// subst +// program +// command +// rc +// override +// +// Special case: attr "limit" is ignored. +A3 A3::extract_read_only_filter () +{ + A3 filter; + + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + // Excluded. + if (arg->_third == "program" || + arg->_third == "command" || + arg->_third == "rc" || + arg->_third == "override") + { + } + + // Included. + else if (arg->_third == "tag" || + arg->_third == "pattern" || + arg->_third == "attr" || + arg->_third == "attmod" || + arg->_third == "id" || + arg->_third == "uuid" || + arg->_third == "op" || + arg->_third == "exp" || + arg->_third == "word") + { + // "limit" is special - it is recognized but not included in filters. + if (arg->_first.find ("limit:") == std::string::npos) + filter.push_back (*arg); + } + + // Error. + else + { + // substitution + throw std::string ("A ") + + arg->_third + + " '" + + arg->_first + + "' is not allowed in a read-only filter."; + } + } + + return filter; +} + +//////////////////////////////////////////////////////////////////////////////// +// A write filter includes id/uuid anywhere on the command line, but any other +// filter elements must occur before the command. +// +// Special case: attr "limit" is ignored. +A3 A3::extract_write_filter () +{ + A3 filter; + bool before_command = true; + + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + // Only use args prior to command. + if (arg->_third == "command") + before_command = false; + + // Excluded. + else if (arg->_third == "program" || + arg->_third == "rc" || + arg->_third == "override") + { + } + + // Included regardless of position. + else if (arg->_third == "id" || + arg->_third == "uuid") + { + filter.push_back (*arg); + } + + // Included if prior to command. + else if (arg->_third == "tag" || + arg->_third == "pattern" || + arg->_third == "attr" || + arg->_third == "attmod" || + arg->_third == "op" || + arg->_third == "exp" || + arg->_third == "word") + { + if (before_command) + { + // "limit" is special - it is recognized but not included in filters. + if (arg->_first.find ("limit:") == std::string::npos) + filter.push_back (*arg); + } + } + + // Error. + else + { + if (before_command) + { + // substitution + throw std::string ("A substitution '") + + arg->_first + + "' is not allowed in a write command filter."; + } + } + } + + return filter; +} + +//////////////////////////////////////////////////////////////////////////////// +A3 A3::extract_modifications (bool include_seq/* = false*/) +{ + A3 modifications; + + bool seen_command = false; + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + // Only use args after command. + if (arg->_third == "command") + { + seen_command = true; + } + + // Sequence excluded regardless of location. + else if (arg->_third == "id" || + arg->_third == "uuid") + { + if (include_seq) + modifications.push_back (Arg (arg->_first, arg->_second, "word")); + } + + else if (seen_command) + { + // Excluded. + if (arg->_third == "program" || + arg->_third == "rc" || + arg->_third == "override") + { + } + + // Included. + else if (arg->_third == "tag" || + arg->_third == "attr" || + arg->_third == "subst" || + arg->_third == "op" || + arg->_third == "exp" || + arg->_third == "word") + { + // "limit" is special - it is recognized but not included in filters. + if (arg->_first.find ("limit:") == std::string::npos) + modifications.push_back (*arg); + } + + // Error. + else + { + // Instead of errors, simply downgrade these to 'word'. + if (arg->_third == "pattern" || + arg->_third == "attmod" || + arg->_third == "id" || + arg->_third == "uuid") + { + arg->_third = "word"; + modifications.push_back (*arg); + } + else + throw std::string ("Error: unrecognized argument in modifications."); + } + } + } + + return modifications; +} + +//////////////////////////////////////////////////////////////////////////////// +A3 A3::extract_simple_words () +{ + A3 filter; + + std::vector ::iterator arg; + for (arg = this->begin (); arg != this->end (); ++arg) + { + // Excluded. + if (arg->_third == "program" || + arg->_third == "command" || + arg->_third == "rc" || + arg->_third == "override" || + arg->_third == "attr" || + arg->_third == "attmod") + { + ; + } + + // Included. + else if (arg->_third == "tag" || + arg->_third == "pattern" || + arg->_third == "subst" || + arg->_third == "id" || + arg->_third == "uuid" || + arg->_third == "op" || + arg->_third == "exp" || + arg->_third == "word") + { + // "limit" is special - it is recognized but not included in filters. + if (arg->_first.find ("limit:") == std::string::npos) + filter.push_back (*arg); + } + + // Error. + else + throw std::string ("Argument '") + arg->_first + "' is not allowed " + "with this command."; + } + + return filter; +} + +//////////////////////////////////////////////////////////////////////////////// +bool A3::valid_modifier (const std::string& modifier) +{ + for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i) + if (modifierNames[i] == modifier) + return true; + + return false; +} +#endif // NOPE + + + + + +//////////////////////////////////////////////////////////////////////////////// +void A3::dump (const std::string& label) +{ + // Set up a color mapping. + std::map color_map; + color_map["program"] = Color ("bold blue on blue"); + color_map["command"] = Color ("bold cyan on cyan"); + color_map["rc"] = Color ("bold red on red"); + color_map["override"] = Color ("bold red on red"); + color_map["terminator"] = Color ("bold yellow on yellow"); + color_map["word"] = Color ("black on white"); + color_map["none"] = Color ("black on white"); + +/* + color_map["tag"] = Color ("green on gray2"); + color_map["pattern"] = Color ("cyan on gray2"); + color_map["attr"] = Color ("bold red on gray2"); + color_map["attmod"] = Color ("bold red on gray2"); + color_map["id"] = Color ("yellow on gray2"); + color_map["uuid"] = Color ("yellow on gray2"); + color_map["subst"] = Color ("bold cyan on gray2"); + color_map["exp"] = Color ("bold green on gray2"); +*/ +// color_map["none"] = Color ("white on gray2"); + // Fundamentals. +/* + color_map["lvalue"] = Color ("bold green on rgb010"); + color_map["op"] = Color ("white on rgb010"); + color_map["int"] = Color ("bold yellow on rgb010"); + color_map["number"] = Color ("bold yellow on rgb010"); + color_map["string"] = Color ("bold yellow on rgb010"); + color_map["rx"] = Color ("bold red on rgb010"); +*/ + + Color color_debug (context.config.get ("color.debug")); + std::stringstream out; + out << color_debug.colorize (label) + << "\n"; + + ViewText view; + view.width (context.getWidth ()); + view.leftMargin (2); + for (unsigned int i = 0; i < this->size (); ++i) + view.add (Column::factory ("string", "")); + + view.addRow (); + view.addRow (); + view.addRow (); + + for (unsigned int i = 0; i < this->size (); ++i) + { + std::string raw = (*this)[i]._raw; + std::string category = (*this)[i]._category; + + Color c; + if (color_map[category].nontrivial ()) + c = color_map[category]; + else + c = color_map["none"]; + + view.set (0, i, raw, c); + view.set (2, i, category, c); + } + + out << view.render (); + context.debug (out.str ()); +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/A3.h b/src/A3.h new file mode 100644 index 000000000..6a1e6f013 --- /dev/null +++ b/src/A3.h @@ -0,0 +1,140 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_A3 +#define INCLUDED_A3 +#define L10N // Localization complete. + +#include +#include +#include + +#define ARGUMENTS_SEQUENCE_MAX_RANGE 1000 + +class Arg +{ +public: + Arg ( + const std::string& raw, + const std::string& category) + : _raw (raw) + , _category (category) + { + } + + Arg (const Arg& other) + { + _raw = other._raw; + _category = other._category; + } + + Arg& operator= (const Arg& other) + { + if (this != &other) + { + _raw = other._raw; + _category = other._category; + } + + return *this; + } + + bool operator== (const Arg& other) const + { + return _raw == other._raw && + _category == other._category; + } + +public: + std::string _raw; // Raw input token, never modified + std::string _category; // Categorized argument +}; + +class A3 : public std::vector +{ +public: + A3 (); + ~A3 (); + + void capture (int, const char**); + void capture (const std::string&); + void capture_first (const std::string&); + + void categorize (); + + static bool is_command (const std::vector &, std::string&); + +/* + void append_stdin (); + void rc_override (std::string&, File&); + void get_data_location (std::string&); + void apply_overrides (); + void resolve_aliases (); + void inject_defaults (); + + std::vector list (); + static std::vector operator_list (); + std::string combine (); + + bool find_command (std::string&); + std::string find_limit (); + + static bool is_multipart (const std::string&, std::vector &); + static bool is_attr (const std::string&); + static bool is_attmod (const std::string&); + static bool is_subst (const std::string&); + static bool is_pattern (const std::string&); + static bool is_id (const std::string&); + static bool is_uuid (const std::string&); + static bool is_tag (const std::string&); + static bool is_operator (const std::string&); + static bool is_operator (const std::string&, char&, int&, char&); + static bool is_symbol_operator (const std::string&); + static bool is_attribute (const std::string&, std::string&); + static bool is_modifier (const std::string&); + static bool is_expression (const std::string&); + + static bool extract_attr (const std::string&, std::string&, std::string&); + static bool extract_attmod (const std::string&, std::string&, std::string&, std::string&, std::string&); + static bool extract_subst (const std::string&, std::string&, std::string&, bool&); + static bool extract_pattern (const std::string&, std::string&); + static bool extract_id (const std::string&, std::vector &); + static bool extract_uuid (const std::string&, std::vector &); + static bool extract_tag (const std::string&, char&, std::string&); + static bool extract_operator (const std::string&, std::string&); + + A3 extract_read_only_filter (); + A3 extract_write_filter (); + A3 extract_modifications (bool include_seq = false); + A3 extract_simple_words (); + + static bool valid_modifier (const std::string&); +*/ + void dump (const std::string&); +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2a9705473..99e90b82c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,7 +5,8 @@ include_directories (${CMAKE_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src/columns ${TASK_INCLUDE_DIRS}) -set (task_SRCS API.cpp API.h +set (task_SRCS A3.cpp A3.h + API.cpp API.h Arguments.cpp Arguments.h Att.cpp Att.h Color.cpp Color.h @@ -15,6 +16,7 @@ set (task_SRCS API.cpp API.h Date.cpp Date.h Directory.cpp Directory.h Duration.cpp Duration.h + E9.cpp E9.h Expression.cpp Expression.h File.cpp File.h Hooks.cpp Hooks.h diff --git a/src/Context.cpp b/src/Context.cpp index de9d60a90..337cf6b99 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -73,8 +73,8 @@ int Context::initialize (int argc, const char** argv) try { // char** argv --> std::vector Context::args. - // TODO Handle "cal" case here. args.capture (argc, argv); + a3.capture (argc, argv); // echo one two -- three | task zero --> task zero one two // 'three' is left in the input buffer. @@ -122,6 +122,9 @@ int Context::initialize (int argc, const char** argv) // Categorize all arguments one more time. THIS IS NECESSARY. args.categorize (); + a3.categorize (); + a3.dump ("Initial"); // TODO Remove. + // Handle default command and assumed 'info' command. args.inject_defaults (); @@ -283,7 +286,7 @@ int Context::dispatch (std::string &out) tdb2.gc (); } - args.dump ("Argument Categorization"); +// args.dump ("Argument Categorization"); return c->execute (out); } @@ -422,6 +425,17 @@ const std::vector Context::getColumns () const return output; } +//////////////////////////////////////////////////////////////////////////////// +const std::vector Context::getCommands () const +{ + std::vector output; + std::map ::const_iterator i; + for (i = commands.begin (); i != commands.end (); ++i) + output.push_back (i->first); + + return output; +} + //////////////////////////////////////////////////////////////////////////////// void Context::assumeLocations () { diff --git a/src/Context.h b/src/Context.h index 50e9c85c9..af533aaf5 100644 --- a/src/Context.h +++ b/src/Context.h @@ -40,6 +40,7 @@ #include #include #include +#include class Context { @@ -59,6 +60,7 @@ public: int getHeight (); // determine terminal height const std::vector getColumns () const; + const std::vector getCommands () const; bool color (); // TTY or ? bool verbose (const std::string&); // Verbosity control @@ -81,6 +83,7 @@ private: public: std::string program; Arguments args; + A3 a3; std::string home_dir; File rc_file; Path data_dir; diff --git a/src/E9.cpp b/src/E9.cpp new file mode 100644 index 000000000..8b1cfd087 --- /dev/null +++ b/src/E9.cpp @@ -0,0 +1,1087 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// + +#include // TODO Remove. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern Context context; + +//////////////////////////////////////////////////////////////////////////////// +// Perform all the necessary steps prior to an eval call. +E9::E9 (Arguments& arguments) +: _args (arguments) +, _prepared (false) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +E9::~E9 () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +bool E9::evalFilter (const Task& task) +{ + if (_args.size () == 0) + return true; + + if (!_prepared) + { + _args.dump ("E9::evalFilter"); + + expand_sequence (); + implicit_and (); + expand_tag (); + expand_pattern (); + expand_attr (); + expand_attmod (); + expand_word (); + expand_tokens (); + postfix (); + + _prepared = true; + } + + // Evaluate the expression. + std::vector value_stack; + eval (task, value_stack); + + // Coerce stack element to boolean. + Variant result (value_stack.back ()); + value_stack.pop_back (); + return result.boolean (); +} + +//////////////////////////////////////////////////////////////////////////////// +std::string E9::evalE9 (const Task& task) +{ + if (_args.size () == 0) + return ""; + + if (!_prepared) + { + _args.dump ("E9::evalE9"); + +// expand_sequence (); +// implicit_and (); +// expand_tag (); +// expand_pattern (); +// expand_attr (); +// expand_attmod (); +// expand_word (); + expand_tokens (); + postfix (); + + _prepared = true; + } + + // Evaluate the expression. + std::vector value_stack; + eval (task, value_stack); + + // Coerce stack element to string. + Variant result (value_stack.back ()); + value_stack.pop_back (); + result.cast (Variant::v_string); + return context.dom.get (result._string, task); +} + +//////////////////////////////////////////////////////////////////////////////// +void E9::eval (const Task& task, std::vector & value_stack) +{ + // Case sensitivity is configurable. + bool case_sensitive = context.config.getBoolean ("search.case.sensitive"); + + std::vector ::const_iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_second == "op") + { +// std::cout << "# operator " << arg->_first << "\n"; + + // Handle the unary operator first. + if (arg->_first == "!") + { + // Are there sufficient arguments? + if (value_stack.size () < 1) + throw std::string ("Error: Insufficient operands for '!' operator."); + + Variant right (value_stack.back ()); + if (right._raw_type == "lvalue") + { + right = Variant (context.dom.get (right._raw, task)); + right._raw = value_stack.back ()._raw; + right._raw_type = value_stack.back ()._raw_type; + } + value_stack.pop_back (); + +// std::cout << "# " << " ! " << right.dump () << "\n"; + bool result = !right; + right = Variant (result); + right._raw_type = "bool"; + +// std::cout << "# --> " << right.dump () << "\n"; + value_stack.push_back (right); + + // This only occurs here, because the unary operators are handled, and + // now the binary operators will be processed. + continue; + } + + // Are there sufficient arguments? + if (value_stack.size () < 2) + throw std::string ("Error: Insufficient operands for '") + arg->_first + "' operator."; + + // rvalue (string, rx, int, number, dom ...). + Variant right (value_stack.back ()); + if (right._raw_type == "lvalue") + { + right = Variant (context.dom.get (right._raw, task)); + right._raw = value_stack.back ()._raw; + right._raw_type = value_stack.back ()._raw_type; + } + value_stack.pop_back (); +// std::cout << "# right variant " << right.dump () << "\n"; + + // lvalue (dom). + Variant left (value_stack.back ()); + if (left._raw_type == "lvalue") + { + left = Variant (context.dom.get (left._raw, task)); + left._raw = value_stack.back ()._raw; + left._raw_type = value_stack.back ()._raw_type; + } + value_stack.pop_back (); +// std::cout << "# left variant " << left.dump () << "\n"; + + // Now the binary operators. + if (arg->_first == "and") + { +// std::cout << "# " << left.dump () << " and " << right.dump () << "\n"; + bool result = (left && right); + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "xor") + { +// std::cout << "# " << left.dump () << " xor " << right.dump () << "\n"; + bool left_bool = left.boolean (); + bool right_bool = right.boolean (); + bool result = (left_bool && !right_bool) || (!left_bool && right_bool); + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "or") + { +// std::cout << "# " << left.dump () << " or " << right.dump () << "\n"; + bool result = (left || right); + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "<=") + { +// std::cout << "# " << left.dump () << " <= " << right.dump () << "\n"; + bool result = false; + if (left._raw == "priority") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + + if (left._string == right._string ) result = true; + else if ( right._string == "H") result = true; + else if (left._string == "L" && right._string == "M") result = true; + else if (left._string == "" ) result = true; + } + else + result = (left <= right); + + left = Variant (result); + left._raw_type = "bool"; + + value_stack.push_back (left); + } + + else if (arg->_first == ">=") + { +// std::cout << "# " << left.dump () << " >= " << right.dump () << "\n"; + bool result = false; + if (left._raw == "priority") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + + if (left._string == right._string ) result = true; + else if (left._string == "H" ) result = true; + else if (left._string == "M" && right._string == "L") result = true; + else if ( right._string == "" ) result = true; + } + else + result = (left >= right); + + left = Variant (result); + left._raw_type = "bool"; + + value_stack.push_back (left); + } + + else if (arg->_first == "!~") + { +// std::cout << "# " << left.dump () << " !~ " << right.dump () << "\n"; + bool case_sensitive = context.config.getBoolean ("search.case.sensitive"); + bool result = !eval_match (left, right, case_sensitive); + + // Matches against description are really against either description, + // annotations or project. + // Short-circuit if match already failed. + if (result && left._raw == "description") + { + // TODO check further. + } + + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "!=") + { +// std::cout << "# " << left.dump () << " != " << right.dump () << "\n"; + bool result = (left != right); + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "=") + { +// std::cout << "# " << left.dump () << " = " << right.dump () << "\n"; + bool result = false; + if (left._raw == "project" || left._raw == "recur") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + if (right._string.length () <= left._string.length ()) + result = compare (right._string, + left._string.substr (0, right._string.length ()), + (bool) case_sensitive); + } + else + result = (left == right); + + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == ">") + { +// std::cout << "# " << left.dump () << " > " << right.dump () << "\n"; + bool result = false; + if (left._raw == "priority") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + + if (left._string == "H" && right._string != "H") result = true; + else if (left._string == "M" && right._string == "L") result = true; + else if (left._string != "" && right._string == "") result = true; + } + else + result = (left > right); + + left = Variant (result); + left._raw_type = "bool"; + + value_stack.push_back (left); + } + + else if (arg->_first == "~") + { +// std::cout << "# " << left.dump () << " ~ " << right.dump () << "\n"; + bool case_sensitive = context.config.getBoolean ("search.case.sensitive"); + bool result = eval_match (left, right, case_sensitive); + + // Matches against description are really against either description, + // annotations or project. + // Short-circuit if match is already found. + if (!result && left._raw == "description") + { + // TODO check further. + } + + left = Variant (result); + left._raw_type = "bool"; + +// std::cout << "# --> " << left.dump () << "\n"; + value_stack.push_back (left); + } + + else if (arg->_first == "*") + { + left = left * right; + value_stack.push_back (left); + } + + else if (arg->_first == "/") + { + left = left / right; + value_stack.push_back (left); + } + + else if (arg->_first == "+") + { + left = left + right; + value_stack.push_back (left); + } + + else if (arg->_first == "-") + { + left = left - right; + value_stack.push_back (left); + } + + else if (arg->_first == "<") + { +// std::cout << "# " << left.dump () << " < " << right.dump () << "\n"; + bool result = false; + if (left._raw == "priority") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + + if (left._string != "H" && right._string == "H") result = true; + else if (left._string == "L" && right._string == "M") result = true; + else if (left._string == "" && right._string != "") result = true; + } + else + result = (left < right); + + left = Variant (result); + left._raw_type = "bool"; + + value_stack.push_back (left); + } + + else + throw std::string ("Unsupported operator '") + arg->_first + "'."; + } + + // It's not an operator, it's an lvalue or some form of rvalue. + else + { + Variant operand; + create_variant (operand, arg->_first, arg->_second); + value_stack.push_back (operand); + } + } + + // Check for stack remnants. + if (value_stack.size () != 1) + throw std::string ("Error: E9::eval found extra items on the stack."); +} + +//////////////////////////////////////////////////////////////////////////////// +bool E9::eval_match (Variant& left, Variant& right, bool case_sensitive) +{ + if (right._raw_type == "rx") + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + + // Create a cached entry, if it does not already exist. + if (_regexes.find (right._string) == _regexes.end ()) + _regexes[right._string] = RX (right._string, case_sensitive); + + if (_regexes[right._string].match (left._string)) + return true; + } + else + { + left.cast (Variant::v_string); + right.cast (Variant::v_string); + if (find (left._string, right._string, (bool) case_sensitive) != std::string::npos) + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +void E9::create_variant ( + Variant& variant, + const std::string& value, + const std::string& type) +{ +// std::cout << "# create_variant " << value << "/" << type << "\n"; + + // DOM references are not resolved until the operator is processed. This + // preserves the original name, which helps determine how to apply the + // operator. + if (type == "lvalue") + variant = Variant (value); + + else if (type == "int") + variant = Variant ((int) strtol (value.c_str (), NULL, 10)); + + else if (type == "number") + variant = Variant (strtod (value.c_str (), NULL)); + + else if (type == "rvalue" || + type == "string" || + type == "rx") + // TODO Is unquoteText necessary? + variant = Variant (unquoteText (value)); + + else + throw std::string ("Unrecognized operand '") + type + "'."; + + variant._raw = value; + variant._raw_type = type; +} + +//////////////////////////////////////////////////////////////////////////////// +// Convert: 1,3-5,00000000-0000-0000-0000-000000000000 +// +// To: (id=1 or (id>=3 and id<=5) or +// uuid="00000000-0000-0000-0000-000000000000") +void E9::expand_sequence () +{ + Arguments temp; + + // Extract all the components of a sequence. + std::vector ids; + std::vector uuids; + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "id") + Arguments::extract_id (arg->_first, ids); + + else if (arg->_third == "uuid") + Arguments::extract_uuid (arg->_first, uuids); + } + + // If there is no sequence, we're done. + if (ids.size () == 0 && uuids.size () == 0) + return; + + // Construct the algebraic form. + std::stringstream sequence; + sequence << "("; + for (unsigned int i = 0; i < ids.size (); ++i) + { + if (i) + sequence << " or "; + + sequence << "id=" << ids[i]; + } + + if (uuids.size ()) + { + if (sequence.str ().length () > 1) + sequence << " or "; + + for (unsigned int i = 0; i < uuids.size (); ++i) + { + if (i) + sequence << " or "; + + sequence << "uuid=\"" << uuids[i] << "\""; + } + } + + sequence << ")"; + + // Copy everything up to the first id/uuid. + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "id" || arg->_third == "uuid") + break; + + temp.push_back (*arg); + } + + // Now insert the new sequence expression. + temp.push_back (Triple (sequence.str (), "exp", "seq")); + + // Now copy everything after the last id/uuid. + bool found_id = false; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "id" || arg->_third == "uuid") + found_id = true; + + else if (found_id) + temp.push_back (*arg); + } + + _args.swap (temp); + _args.dump ("E9::expand_sequence"); +} + +//////////////////////////////////////////////////////////////////////////////// +void E9::expand_tokens () +{ + Arguments temp; + bool delta = false; + + // Get a list of all operators. + std::vector operators = Arguments::operator_list (); + + // Look at all args. + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_second == "exp") + { + tokenize (arg->_first, arg->_third, operators, temp); + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_tokens"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Nibble the whole bloody thing. Nuke it from orbit - it's the only way to be +// sure. +void E9::tokenize ( + const std::string& input, + const std::string& category, + std::vector & operators, + Arguments& tokens) +{ + // Date format, for both parsing and rendering. + std::string date_format = context.config.get ("dateformat"); + + // Nibble each arg token by token. + Nibbler n (input); + + // Fake polymorphism. + std::string s; + int i; + double d; + time_t t; + + while (! n.depleted ()) + { + if (n.getQuoted ('"', s, true) || + n.getQuoted ('\'', s, true)) + tokens.push_back (Triple (s, "string", category)); + + else if (n.getQuoted ('/', s, true)) + tokens.push_back (Triple (s, "pattern", category)); + + else if (n.getOneOf (operators, s)) + tokens.push_back (Triple (s, "op", category)); + + else if (n.getDOM (s)) + tokens.push_back (Triple (s, "lvalue", category)); + + else if (n.getNumber (d)) + tokens.push_back (Triple (format (d), "number", category)); + + else if (n.getDateISO (t)) + tokens.push_back (Triple (Date (t).toISO (), "date", category)); + + else if (n.getDate (date_format, t)) + tokens.push_back (Triple (Date (t).toString (date_format), "date", category)); + + else if (n.getInt (i)) + tokens.push_back (Triple (format (i), "int", category)); + + else if (n.getWord (s)) + tokens.push_back (Triple (s, "rvalue", category)); + + else + { + if (! n.getUntilWS (s)) + n.getUntilEOS (s); + + tokens.push_back (Triple (s, "string", category)); + } + + n.skipWS (); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Inserts the 'and' operator by default between terms that are not separated by +// at least one operator. +// +// Converts: +// to: and +// +void E9::implicit_and () +{ + Arguments temp; + bool delta = false; + + std::string previous = "op"; + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + // Old-style filters need 'and' conjunctions. + if (previous != "op" && + arg->_third != "op") + { + temp.push_back (Triple ("and", "op", "-")); + delta = true; + } + + // Now insert the adjacent non-operator. + temp.push_back (*arg); + previous = arg->_third; + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::implicit_and"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Convert: +with -without +// To: tags ~ with +// tags !~ without +void E9::expand_tag () +{ + Arguments temp; + bool delta = false; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "tag") + { + char type; + std::string value; + Arguments::extract_tag (arg->_first, type, value); + + temp.push_back (Triple ("tags", "lvalue", arg->_third)); + temp.push_back (Triple (type == '+' ? "~" : "!~", "op", arg->_third)); + temp.push_back (Triple (value, "string", arg->_third)); + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_tag"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Convert: /foo/ +// To: description ~ foo +void E9::expand_pattern () +{ + Arguments temp; + bool delta = false; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "pattern") + { + std::string value; + Arguments::extract_pattern (arg->_first, value); + + temp.push_back (Triple ("description", "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); + temp.push_back (Triple (value, "rx", arg->_third)); + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_pattern"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +----------------+ +----------+ +------+ +---------+ +// | : | | | | = | | | +// +----------------+ +----------+ +------+ +---------+ +// | | --> | | | op | | exp | +// +----------------+ +----------+ +------+ +---------+ +// | attr | | attr | | attr | | attr | +// +----------------+ +----------+ +------+ +---------+ +// +void E9::expand_attr () +{ + Arguments temp; + bool delta = false; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "attr") + { + std::string name; + std::string value; + Arguments::extract_attr (arg->_first, name, value); + + // Canonicalize 'name'. + Arguments::is_attribute (name, name); + +/* + // Always quote the value, so that empty values, or values containing spaces + // are preserved. + value = "\"" + value + "\""; +*/ + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("=", "op", arg->_third)); + temp.push_back (Triple (value, "exp", arg->_third)); + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_attr"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// +----------------------+ +----------+ +-------+ +---------------+ +// | .: | | | | | | | +// +----------------------+ +----------+ +-------+ +---------------+ +// | | --> | | | op | | exp/string/rx | +// +----------------------+ +----------+ +-------+ +---------------+ +// | attr | | attr | | attr | | attr | +// +----------------------+ +----------+ +-------+ +---------------+ +// +void E9::expand_attmod () +{ + Arguments temp; + bool delta = false; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "attmod") + { + std::string name; + std::string mod; + std::string value; + std::string sense; + Arguments::extract_attmod (arg->_first, name, mod, value, sense); + Arguments::is_attribute (name, name); + Arguments::is_modifier (mod); + +/* + // Always quote the value, so that empty values, or values containing spaces + // are preserved. + std::string raw_value = value; + value = "\"" + value + "\""; +*/ + + if (mod == "before" || mod == "under" || mod == "below") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("<", "op", arg->_third)); + temp.push_back (Triple (value, "exp", arg->_third)); + } + else if (mod == "after" || mod == "over" || mod == "above") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple (">", "op", arg->_third)); + temp.push_back (Triple (value, "exp", arg->_third)); + } + else if (mod == "none") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("==", "op", arg->_third)); + temp.push_back (Triple ("", "string", arg->_third)); + } + else if (mod == "any") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("!=", "op", arg->_third)); + temp.push_back (Triple ("", "string", arg->_third)); + } + else if (mod == "is" || mod == "equals") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("=", "op", arg->_third)); + temp.push_back (Triple (value, "exp", arg->_third)); + } + else if (mod == "isnt" || mod == "not") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("!=", "op", arg->_third)); + temp.push_back (Triple (value, "exp", arg->_third)); + } + else if (mod == "has" || mod == "contains") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); + temp.push_back (Triple (value, "rx", arg->_third)); + } + else if (mod == "hasnt") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("!~", "op", arg->_third)); + temp.push_back (Triple (value, "rx", arg->_third)); + } + else if (mod == "startswith" || mod == "left") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); +// temp.push_back (Triple ("^" + raw_value, "rx", arg->_third)); + temp.push_back (Triple ("^" + value, "rx", arg->_third)); + } + else if (mod == "endswith" || mod == "right") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); +// temp.push_back (Triple (raw_value + "$", "rx", arg->_third)); + temp.push_back (Triple (value + "$", "rx", arg->_third)); + } + else if (mod == "word") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); +// temp.push_back (Triple ("\\b" + raw_value + "\\b", "rx", arg->_third)); + temp.push_back (Triple ("\\b" + value + "\\b", "rx", arg->_third)); + } + else if (mod == "noword") + { + temp.push_back (Triple (name, "lvalue", arg->_third)); + temp.push_back (Triple ("!~", "op", arg->_third)); +// temp.push_back (Triple ("\\b" + raw_value + "\\b", "rx", arg->_third)); + temp.push_back (Triple ("\\b" + value + "\\b", "rx", arg->_third)); + } + else + throw std::string ("Error: unrecognized attribute modifier '") + mod + "'."; + + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_attmod"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Convert: +// To: description ~ +void E9::expand_word () +{ + Arguments temp; + bool delta = false; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_third == "word") + { + temp.push_back (Triple ("description", "lvalue", arg->_third)); + temp.push_back (Triple ("~", "op", arg->_third)); + temp.push_back (Triple ("\"" + arg->_first + "\"", "string", arg->_third)); + + delta = true; + } + else + temp.push_back (*arg); + } + + if (delta) + { + _args.swap (temp); + _args.dump ("E9::expand_word"); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Dijkstra Shunting Algorithm. +// http://en.wikipedia.org/wiki/Shunting-yard_algorithm +// +// While there are tokens to be read: +// Read a token. +// If the token is an operator, o1, then: +// while there is an operator token, o2, at the top of the stack, and +// either o1 is left-associative and its precedence is less than or +// equal to that of o2, +// or o1 is right-associative and its precedence is less than that +// of o2, +// pop o2 off the stack, onto the output queue; +// push o1 onto the stack. +// If the token is a left parenthesis, then push it onto the stack. +// If the token is a right parenthesis: +// Until the token at the top of the stack is a left parenthesis, pop +// operators off the stack onto the output queue. +// Pop the left parenthesis from the stack, but not onto the output queue. +// If the token at the top of the stack is a function token, pop it onto +// the output queue. +// If the stack runs out without finding a left parenthesis, then there +// are mismatched parentheses. +// If the token is a number, then add it to the output queue. +// +// When there are no more tokens to read: +// While there are still operator tokens in the stack: +// If the operator token on the top of the stack is a parenthesis, then +// there are mismatched parentheses. +// Pop the operator onto the output queue. +// Exit. +// +void E9::postfix () +{ + Arguments temp; + + std::pair item; + Arguments op_stack; + char type; + int precedence; + char associativity; + + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + { + if (arg->_first == "(") + { + op_stack.push_back (*arg); + } + else if (arg->_first == ")") + { + while (op_stack.size () > 0 && + op_stack.back ()._first != "(") + { + temp.push_back (op_stack.back ()); + op_stack.pop_back (); + } + + if (op_stack.size ()) + op_stack.pop_back (); + else + throw std::string ("Mismatched parentheses in expression"); + } + else if (Arguments::is_operator (arg->_first, type, precedence, associativity)) + { + char type2; + int precedence2; + char associativity2; + while (op_stack.size () > 0 && + Arguments::is_operator (op_stack.back ()._first, type2, precedence2, associativity2) && + ((associativity == 'l' && precedence <= precedence2) || + (associativity == 'r' && precedence < precedence2))) + { + temp.push_back (op_stack.back ()); + op_stack.pop_back (); + } + + op_stack.push_back (*arg); + } + else + { + temp.push_back (*arg); + } + } + + while (op_stack.size () != 0) + { + if (op_stack.back ()._first == "(" || + op_stack.back ()._first == ")") + throw std::string ("Mismatched parentheses in expression"); + + temp.push_back (op_stack.back ()); + op_stack.pop_back (); + } + + _args.swap (temp); + _args.dump ("E9::toPostfix"); +} + +//////////////////////////////////////////////////////////////////////////////// +// Test whether the _original arguments are old style or new style. +// +// Old style: no single argument corresponds to an operator, ie no 'and', 'or', +// etc. +// +// New style: at least one argument that is an operator. +// +bool E9::is_new_style () +{ + std::vector ::iterator arg; + for (arg = _args.begin (); arg != _args.end (); ++arg) + if (Arguments::is_symbol_operator (arg->_first)) + return true; + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/E9.h b/src/E9.h new file mode 100644 index 000000000..3e7b94a9e --- /dev/null +++ b/src/E9.h @@ -0,0 +1,73 @@ +//////////////////////////////////////////////////////////////////////////////// +// taskwarrior - a command line task list manager. +// +// Copyright 2006 - 2011, Paul Beckingham, Federico Hernandez. +// All rights reserved. +// +// This program is free software; you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation; either version 2 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// this program; if not, write to the +// +// Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, +// Boston, MA +// 02110-1301 +// USA +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_E9 +#define INCLUDED_E9 +#define L10N // Localization complete. + +#include +#include +#include +#include +#include +#include + +class E9 +{ +public: + E9 (Arguments&); + ~E9 (); + bool eval (const Task&); + bool evalFilter (const Task&); + std::string evalE9 (const Task&); + void eval (const Task&, std::vector &); + +private: + void expand_sequence (); + void implicit_and (); + void expand_tag (); + void expand_pattern (); + void expand_attr (); + void expand_attmod (); + void expand_word (); + void expand_tokens (); + void postfix (); + + void tokenize (const std::string&, const std::string&, std::vector &, Arguments&); + void create_variant (Variant&, const std::string&, const std::string&); + bool is_new_style (); + +private: + bool eval_match (Variant&, Variant&, bool); + +private: + Arguments _args; + std::map _regexes; + bool _prepared; +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/Expression.cpp b/src/Expression.cpp index fffb98b52..8f2ff15c5 100644 --- a/src/Expression.cpp +++ b/src/Expression.cpp @@ -60,7 +60,7 @@ bool Expression::evalFilter (const Task& task) if (!_prepared) { - _args.dump ("Expression::evalFilter"); +// _args.dump ("Expression::evalFilter"); expand_sequence (); implicit_and (); @@ -93,7 +93,7 @@ std::string Expression::evalExpression (const Task& task) if (!_prepared) { - _args.dump ("Expression::evalExpression"); +// _args.dump ("Expression::evalExpression"); // expand_sequence (); // implicit_and (); @@ -567,7 +567,7 @@ void Expression::expand_sequence () } _args.swap (temp); - _args.dump ("Expression::expand_sequence"); +// _args.dump ("Expression::expand_sequence"); } //////////////////////////////////////////////////////////////////////////////// @@ -595,7 +595,7 @@ void Expression::expand_tokens () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_tokens"); +// _args.dump ("Expression::expand_tokens"); } } @@ -694,7 +694,7 @@ void Expression::implicit_and () if (delta) { _args.swap (temp); - _args.dump ("Expression::implicit_and"); +// _args.dump ("Expression::implicit_and"); } } @@ -728,7 +728,7 @@ void Expression::expand_tag () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_tag"); +// _args.dump ("Expression::expand_tag"); } } @@ -760,7 +760,7 @@ void Expression::expand_pattern () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_pattern"); +// _args.dump ("Expression::expand_pattern"); } } @@ -807,7 +807,7 @@ void Expression::expand_attr () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_attr"); +// _args.dump ("Expression::expand_attr"); } } @@ -933,7 +933,7 @@ void Expression::expand_attmod () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_attmod"); +// _args.dump ("Expression::expand_attmod"); } } @@ -963,7 +963,7 @@ void Expression::expand_word () if (delta) { _args.swap (temp); - _args.dump ("Expression::expand_word"); +// _args.dump ("Expression::expand_word"); } } @@ -1063,7 +1063,7 @@ void Expression::postfix () } _args.swap (temp); - _args.dump ("Expression::toPostfix"); +// _args.dump ("Expression::toPostfix"); } ////////////////////////////////////////////////////////////////////////////////