//////////////////////////////////////////////////////////////////////////////// // taskwarrior - a command line task list manager. // // Copyright 2006 - 2010, Paul Beckingham. // 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 "text.h" #include "Color.h" #include "util.h" #include "Date.h" #include "Duration.h" #include "Context.h" #include "Att.h" extern Context context; static const char* internalNames[] = { "entry", "start", "end", "parent", "uuid", "mask", "imask", "limit", "status", "description", "tags", // Note that annotations are not listed. }; static const char* modifiableNames[] = { "project", "priority", "fg", "bg", "due", "recur", "until", "wait", "depends", }; // 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" }; #define NUM_INTERNAL_NAMES (sizeof (internalNames) / sizeof (internalNames[0])) #define NUM_MODIFIABLE_NAMES (sizeof (modifiableNames) / sizeof (modifiableNames[0])) #define NUM_MODIFIER_NAMES (sizeof (modifierNames) / sizeof (modifierNames[0])) //////////////////////////////////////////////////////////////////////////////// Att::Att () : mName ("") , mValue ("") , mMod ("") { } //////////////////////////////////////////////////////////////////////////////// Att::Att (const std::string& name, const std::string& mod, const std::string& value) { mName = name; mValue = value; mMod = mod; } //////////////////////////////////////////////////////////////////////////////// Att::Att (const std::string& name, const std::string& mod, int value) { mName = name; std::stringstream s; s << value; mValue = s.str (); mMod = mod; } //////////////////////////////////////////////////////////////////////////////// Att::Att (const std::string& name, const std::string& value) { mName = name; mValue = value; mMod = ""; } //////////////////////////////////////////////////////////////////////////////// Att::Att (const std::string& name, int value) { mName = name; std::stringstream s; s << value; mValue = s.str (); mMod = ""; } //////////////////////////////////////////////////////////////////////////////// Att::Att (const Att& other) { mName = other.mName; mValue = other.mValue; mMod = other.mMod; } //////////////////////////////////////////////////////////////////////////////// Att& Att::operator= (const Att& other) { if (this != &other) { mName = other.mName; mValue = other.mValue; mMod = other.mMod; } return *this; } //////////////////////////////////////////////////////////////////////////////// bool Att::operator== (const Att& other) const { return mName == other.mName && mMod == other.mMod && mValue == other.mValue; } //////////////////////////////////////////////////////////////////////////////// Att::~Att () { } //////////////////////////////////////////////////////////////////////////////// // For parsing. bool Att::valid (const std::string& input) const { Nibbler n (input); std::string ignored; if (n.getUntilOneOf (".:", ignored)) { if (ignored.length () == 0) return false; while (n.skip ('.')) if (!n.getUntilOneOf (".:", ignored)) return false; if (n.skip (':') && (n.getQuoted ('"', ignored) || n.getUntil (' ', ignored) || n.getUntilEOS (ignored) || n.depleted ())) return true; return false; } return false; } //////////////////////////////////////////////////////////////////////////////// bool Att::validInternalName (const std::string& name) { for (unsigned int i = 0; i < NUM_INTERNAL_NAMES; ++i) if (name == internalNames[i]) return true; return false; } //////////////////////////////////////////////////////////////////////////////// bool Att::validModifiableName (const std::string& name) { for (unsigned int i = 0; i < NUM_MODIFIABLE_NAMES; ++i) if (name == modifiableNames[i]) return true; return false; } //////////////////////////////////////////////////////////////////////////////// bool Att::validNameValue ( const std::string& name, const std::string& mod, const std::string& value) { std::string writableName = name; std::string writableMod = mod; std::string writableValue = value; return Att::validNameValue (writableName, writableMod, writableValue); } //////////////////////////////////////////////////////////////////////////////// bool Att::validNameValue ( std::string& name, std::string& mod, std::string& value) { // First, guess at the full attribute name. std::vector candidates; for (unsigned i = 0; i < NUM_INTERNAL_NAMES; ++i) candidates.push_back (internalNames[i]); for (unsigned i = 0; i < NUM_MODIFIABLE_NAMES; ++i) candidates.push_back (modifiableNames[i]); std::vector matches; autoComplete (name, candidates, matches); if (matches.size () == 0) return false; else if (matches.size () != 1) { std::string error = "Ambiguous attribute '" + name + "' - could be either of "; // TODO i18n std::sort (matches.begin (), matches.end ()); std::string combined; join (combined, ", ", matches); throw error + combined + "."; } name = matches[0]; // Second, guess at the modifier name. if (mod != "") { candidates.clear (); for (unsigned i = 0; i < NUM_MODIFIER_NAMES; ++i) candidates.push_back (modifierNames[i]); matches.clear (); autoComplete (mod, candidates, matches); if (matches.size () == 0) throw std::string ("Unrecognized modifier '") + mod + "'."; else if (matches.size () != 1) { std::string error = "Ambiguous modifier '" + mod + "' - could be either of "; // TODO i18n std::sort (matches.begin (), matches.end ()); std::string combined; join (combined, ", ", matches); error += combined; throw error + combined + "."; } mod = matches[0]; } // Some attributes are intended to be private, unless the command is read- // only, in which cased these are perfectly valid elements of a filter. if (context.cmd.isWriteCommand () && !validModifiableName (name)) throw std::string ("\"") + name + "\" is not an attribute you may modify directly."; // Thirdly, make sure the value has the expected form or values. if (name == "project") { if (!noSpaces (value)) throw std::string ("The '") + name + "' attribute may not contain spaces."; } else if (name == "priority") { if (value != "") { value = upperCase (value); if (value != "H" && value != "M" && value != "L") throw std::string ("\"") + value + "\" is not a valid priority. Use H, M, L or leave blank."; } } else if (name == "description") { if (context.cmd.isWriteCommand ()) { if (value == "") throw std::string ("The '") + name + "' attribute must not be blank."; if (!noVerticalSpace (value)) throw std::string ("The '") + name + "' attribute must not contain vertical white space."; } } else if (name == "fg" || name == "bg") { // TODO Determine whether color abbreviations are supported, and if so, // modify 'value' here accordingly. } // Dates can now be either a date, or a duration that is added as an offset // to the current date. else if (name == "due" || name == "until" || name == "wait") { // Validate and convert to epoch. if (value != "") { // Try parsing as a duration. If unsuccessful, try again, as a date. try { Date now; Duration dur (value); if (dur.negative ()) value = (now - (time_t)dur).toEpochString (); else value = (now + (time_t)dur).toEpochString (); } // If the date parsing failed, try parsing as a duration. If successful, // add the duration to the current date. If unsuccessful, propagate the // original date parse error. // Try parsing as a date. If unsuccessfull, throw. catch (...) { try { value = Date (value, context.config.get ("dateformat")).toEpochString (); } catch (std::string& e) { throw e; } } } } else if (name == "recur") { // Just validate, don't convert to days. Duration d; if (value != "") d.parse (value); } else if (name == "limit") { if (value == "" || (value != "page" && !digitsOnly (value))) throw std::string ("The '") + name + "' attribute must be an integer, or the value 'page'."; } else if (name == "status") { value = lowerCase (value); std::vector matches; std::vector candidates; candidates.push_back ("pending"); candidates.push_back ("completed"); candidates.push_back ("deleted"); candidates.push_back ("recurring"); candidates.push_back ("waiting"); autoComplete (value, candidates, matches); if (matches.size () == 1) value = matches[0]; else throw std::string ("\"") + value + "\" is not a valid status. Use 'pending', 'completed', 'deleted', 'recurring' or 'waiting'."; } else if (! validInternalName (name) && ! validModifiableName (name)) throw std::string ("'") + name + "' is not a recognized attribute."; return true; } //////////////////////////////////////////////////////////////////////////////// // TODO Deprecated - remove. bool Att::validMod (const std::string& mod) { for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i) if (modifierNames[i] == mod) return true; return false; } //////////////////////////////////////////////////////////////////////////////// // The type of an attribute is useful for modifier evaluation. std::string Att::type (const std::string& name) const { if (name == "due" || name == "wait" || name == "until" || name == "start" || name == "entry" || name == "end") return "date"; else if (name == "recur") return "duration"; else if (name == "limit") return "number"; else if (name == "priority") return "priority"; else return "text"; } //////////////////////////////////////////////////////////////////////////////// // The type of an attribute is useful for modifier evaluation. std::string Att::modType (const std::string& name) const { if (name == "hasnt" || name == "isnt" || name == "not" || // TODO Verify this. name == "noword") return "negative"; return "positive"; } //////////////////////////////////////////////////////////////////////////////// // // start --> name --> . --> mod --> : --> " --> value --> " --> end // | ^ // |_____________________| // void Att::parse (const std::string& input) { Nibbler n (input); parse (n); } void Att::parse (Nibbler& n) { // Ensure a clean object first. mName = ""; mValue = ""; mMod = ""; if (n.getUntilOneOf (".:", mName)) { if (mName.length () == 0) throw std::string ("Missing attribute name"); // TODO i18n if (n.skip ('.')) { std::string mod; if (n.getUntil (":", mod)) { if (validMod (mod)) mMod = mod; else throw std::string ("The name '") + mod + "' is not a valid modifier."; // TODO i18n } else throw std::string ("Missing . or : after modifier."); // TODO i18n } if (n.skip (':')) { // Both quoted and unquoted Att's are accepted. // Consider removing this for a stricter parse. if (n.getQuoted ('"', mValue) || n.getUntil (' ', mValue)) { decode (mValue); } } else throw std::string ("Missing : after attribute name."); // TODO i18n } else throw std::string ("Missing : after attribute name."); // TODO i18n /* TODO This might be too slow to include. Test this assumption. validNameValue (mName, mMod, mValue); */ } //////////////////////////////////////////////////////////////////////////////// // "this" is the attribute that has modifiers. "other" is the attribute from a // Record that does not have modifiers, but may have a value. bool Att::match (const Att& other) const { // All matches are assumed to pass, any short-circuit on non-match. bool case_sensitive = context.config.getBoolean ("search.case.sensitive"); // If there are no mods, just perform a straight compare on value. if (mMod == "") { // Exact matches on dates should only compare m/d/y, not h:m:s. This allows // comparisons like "task list due:today" (bug #405). std::string which = type (mName); if (which == "date") { if (other.mValue == "") return false; Date left (mValue); Date right (other.mValue); if (! left.sameDay (right)) return false; } else { if (!compare (mValue, other.mValue, (bool) case_sensitive)) return false; } } // has = contains as a substring. else if (mMod == "has" || mMod == "contains") // TODO i18n { if (find (other.mValue, mValue, (bool) case_sensitive) == std::string::npos) return false; } // is = equal. Nop. else if (mMod == "is" || mMod == "equals") // TODO i18n { if (!compare (mValue, other.mValue, (bool) case_sensitive)) return false; } // isnt = not equal. else if (mMod == "isnt" || mMod == "not") // TODO i18n { if (compare (mValue, other.mValue, (bool) case_sensitive)) return false; } // any = any value, but not empty value. else if (mMod == "any") // TODO i18n { if (other.mValue == "") return false; } // none = must have empty value. else if (mMod == "none") // TODO i18n { if (other.mValue != "") return false; } // startswith = first characters must match. else if (mMod == "startswith" || mMod == "left") // TODO i18n { if (other.mValue.length () < mValue.length ()) return false; if (!compare (mValue, other.mValue.substr (0, mValue.length ()), (bool) case_sensitive)) return false; } // endswith = last characters must match. else if (mMod == "endswith" || mMod == "right") // TODO i18n { if (other.mValue.length () < mValue.length ()) return false; if (!compare (mValue, other.mValue.substr ( other.mValue.length () - mValue.length (), std::string::npos), (bool) case_sensitive)) return false; } // hasnt = does not contain as a substring. else if (mMod == "hasnt") // TODO i18n { if (find (other.mValue, mValue, (bool) case_sensitive) != std::string::npos) return false; } // before = under = below = < else if (mMod == "before" || mMod == "under" || mMod == "below") { std::string which = type (mName); if (which == "duration") { Duration literal (mValue); Duration variable ((time_t)atoi (other.mValue.c_str ())); if (!(variable < literal)) return false; } else if (which == "date") { Date literal (mValue.c_str (), context.config.get ("dateformat")); Date variable ((time_t)atoi (other.mValue.c_str ())); if (other.mValue == "" || ! (variable < literal)) return false; } else if (which == "number") { if (atoi (mValue.c_str ()) >= atoi (other.mValue.c_str ())) return false; } else if (which == "text") { if (mValue <= other.mValue) return false; } else if (which == "priority") { if (mValue == "" || other.mValue == "H" || mValue == other.mValue || (mValue == "L" && other.mValue == "M")) return false; } } // after = over = above = > else if (mMod == "after" || mMod == "over" || mMod == "above") { std::string which = type (mName); if (which == "duration") { Duration literal (mValue); Duration variable ((time_t)atoi (other.mValue.c_str ())); if (! (variable > literal)) return false; } else if (which == "date") { Date literal (mValue.c_str (), context.config.get ("dateformat")); Date variable ((time_t)atoi (other.mValue.c_str ())); if (! (variable > literal)) return false; } else if (which == "number") { if (atoi (mValue.c_str ()) <= atoi (other.mValue.c_str ())) return false; } else if (which == "text") { if (mValue >= other.mValue) return false; } else if (which == "priority") { if (mValue == "H" || other.mValue == "" || mValue == other.mValue || (mValue == "M" && other.mValue == "L")) return false; } } // word = contains as a substring, with word boundaries. else if (mMod == "word") // TODO i18n { // Fail if the substring is not found. std::string::size_type sub = find (other.mValue, mValue, (bool) case_sensitive); if (sub == std::string::npos) return false; // Also fail if there is no word boundary at beginning and end. if (!isWordStart (other.mValue, sub)) return false; if (!isWordEnd (other.mValue, sub + mValue.length () - 1)) return false; } // noword = does not contain as a substring, with word boundaries. else if (mMod == "noword") // TODO i18n { // Fail if the substring is not found. std::string::size_type sub = find (other.mValue, mValue); if (sub != std::string::npos && isWordStart (other.mValue, sub) && isWordEnd (other.mValue, sub + mValue.length () - 1)) { return false; } } return true; } //////////////////////////////////////////////////////////////////////////////// // name : " value " std::string Att::composeF4 () const { std::string output = ""; if (mName != "" && mValue != "") { std::string value = mValue; encode (value); enquote (value); output += mName + ":" + value; } return output; } //////////////////////////////////////////////////////////////////////////////// void Att::mod (const std::string& input) { if (input != "" && !validMod (input)) throw std::string ("The name '") + input + "' is not a valid modifier."; // TODO i18n mMod = input; } //////////////////////////////////////////////////////////////////////////////// std::string Att::mod () const { return mMod; } //////////////////////////////////////////////////////////////////////////////// std::string Att::name () const { return mName; } //////////////////////////////////////////////////////////////////////////////// void Att::name (const std::string& name) { mName = name; } //////////////////////////////////////////////////////////////////////////////// std::string Att::value () const { return mValue; } //////////////////////////////////////////////////////////////////////////////// void Att::value (const std::string& value) { mValue = value; } //////////////////////////////////////////////////////////////////////////////// int Att::value_int () const { return atoi (mValue.c_str ()); } //////////////////////////////////////////////////////////////////////////////// void Att::allNames (std::vector & all) { all.clear (); unsigned int i; for (i = 0; i < NUM_INTERNAL_NAMES; ++i) all.push_back (internalNames[i]); for (i = 0; i < NUM_MODIFIABLE_NAMES; ++i) all.push_back (modifiableNames[i]); } //////////////////////////////////////////////////////////////////////////////// void Att::value_int (int value) { std::stringstream s; s << value; mValue = s.str (); } //////////////////////////////////////////////////////////////////////////////// // Add quotes. void Att::enquote (std::string& value) const { value = '"' + value + '"'; } //////////////////////////////////////////////////////////////////////////////// // Remove quotes. Instead of being picky, just remove them all. There should // be none within the value, and this will correct for one possible corruption // that hand-editing the pending.data file could cause. void Att::dequote (std::string& value) const { std::string::size_type quote; while ((quote = value.find ('"')) != std::string::npos) value.replace (quote, 1, ""); } //////////////////////////////////////////////////////////////////////////////// // Encode values prior to serialization. // \t -> &tab; // " -> &dquot; // [ -> &open; // ] -> &close; void Att::encode (std::string& value) const { std::string::size_type i; while ((i = value.find ('\t')) != std::string::npos) value.replace (i, 1, "&tab;"); // no i18n while ((i = value.find ('"')) != std::string::npos) value.replace (i, 1, "&dquot;"); // no i18n while ((i = value.find ('[')) != std::string::npos) value.replace (i, 1, "&open;"); // no i18n while ((i = value.find (']')) != std::string::npos) value.replace (i, 1, "&close;"); // no i18n } //////////////////////////////////////////////////////////////////////////////// // Decode values after parse. // \t <- &tab; // " <- " or &dquot; // ' <- &squot; // , <- , // [ <- &open; // ] <- &close; // : <- : void Att::decode (std::string& value) const { std::string::size_type i; // Supported encodings. while ((i = value.find ("&tab;")) != std::string::npos) value.replace (i, 5, "\t"); while ((i = value.find ("&dquot;")) != std::string::npos) value.replace (i, 7, "\""); while ((i = value.find (""")) != std::string::npos) value.replace (i, 6, "\""); while ((i = value.find ("&open;")) != std::string::npos) value.replace (i, 6, "["); while ((i = value.find ("&close;")) != std::string::npos) value.replace (i, 7, "]"); // Support for deprecated encodings. These cannot be removed or old files // will not be parsable. Not just old files - completed.data can contain // tasks formatted/encoded using these. while ((i = value.find ("&squot;")) != std::string::npos) value.replace (i, 7, "'"); while ((i = value.find (",")) != std::string::npos) value.replace (i, 7, ","); while ((i = value.find (":")) != std::string::npos) value.replace (i, 7, ":"); } ////////////////////////////////////////////////////////////////////////////////