From 4b71fa73f8faf0f112c2505e56fe3fc571a31b61 Mon Sep 17 00:00:00 2001 From: Dan White Date: Sat, 19 Mar 2011 00:55:57 -0400 Subject: [PATCH] Feature #710 - Added feature #710, which adds an attribute modifier prefix to return the complement of a filtered set (thanks to Dan White). - Added missing description to the 'help' command. Signed-off-by: Paul Beckingham --- AUTHORS | 1 + ChangeLog | 2 + NEWS | 2 + doc/man/task.1.in | 9 ++- src/Att.cpp | 46 +++++++++++- src/Att.h | 3 + src/Context.cpp | 14 +++- src/Filter.cpp | 6 +- src/report.cpp | 3 + test/filter-prefix.t | 166 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 241 insertions(+), 11 deletions(-) create mode 100755 test/filter-prefix.t diff --git a/AUTHORS b/AUTHORS index 258ad73cf..51f946816 100644 --- a/AUTHORS +++ b/AUTHORS @@ -41,6 +41,7 @@ The following submitted code, packages or analysis, and deserve special thanks: Eric Fluger Andreas Poisel Timm Reitinger + Dan White Thanks to the following, who submitted detailed bug reports and excellent suggestions: diff --git a/ChangeLog b/ChangeLog index 684dc6feb..86da11aeb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -10,6 +10,8 @@ + Added feature #700, which adds tab-completion of built-in tags. + Corrected sorting to use std::stable_sort instead of std::sort, which is not guaranteed stable (thanks to Stefan Hacker). + + Added feature #710, which adds an attribute modifier prefix to return the + complement of a filtered set (thanks to Dan White). ------ old releases ------------------------------ diff --git a/NEWS b/NEWS index dbb060f32..60dae974b 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,8 @@ New Features in taskwarrior 2.0.0 of the actual tasks. For advanced pipeline use. - Now supplements the command line with data read from standard input, which allows commands like: echo 'add Pay the bills' | task + - Attribute modifiers may be prefixed with '~' to return the opposite of a + filter's results. Please refer to the ChangeLog file for full details. There are too many to list here. diff --git a/doc/man/task.1.in b/doc/man/task.1.in index e79b214ef..7383c0f59 100644 --- a/doc/man/task.1.in +++ b/doc/man/task.1.in @@ -424,7 +424,8 @@ id1 and id2 should be completed before this task. Consequently, this task will then show up on the 'blocked' report. .SH ATTRIBUTE MODIFIERS -Attribute modifiers improve filters. Supported modifiers are: +Attribute modifiers improve filters. Prefixing any modifier with '~' returns +the complementary set of items. Supported modifiers are: .RS .B before (synonyms under, below) @@ -517,7 +518,11 @@ modifier matches against the left, or beginning of an attribute, such that: task list project.startswith:H task list project:H -are equivalent and will match any project starting with 'H'. +are equivalent and will match any project starting with 'H'. Matching all +projects not starting with 'H' is done with: + + task list project.~startswith:H + task list project.not:H The .I endswith diff --git a/src/Att.cpp b/src/Att.cpp index 5fb4dc232..c5a33a16f 100644 --- a/src/Att.cpp +++ b/src/Att.cpp @@ -113,6 +113,7 @@ Att::Att () : mName ("") , mValue ("") , mMod ("") +, mSense ("positive") { } @@ -122,6 +123,17 @@ Att::Att (const std::string& name, const std::string& mod, const std::string& va mName = name; mValue = value; mMod = mod; + mSense = "positive"; +} + +//////////////////////////////////////////////////////////////////////////////// +Att::Att (const std::string& name, const std::string& mod, const std::string& value, + const std::string& sense) +{ + mName = name; + mValue = value; + mMod = mod; + mSense = sense; } //////////////////////////////////////////////////////////////////////////////// @@ -134,6 +146,7 @@ Att::Att (const std::string& name, const std::string& mod, int value) mValue = s.str (); mMod = mod; + mSense = "positive"; } //////////////////////////////////////////////////////////////////////////////// @@ -142,6 +155,7 @@ Att::Att (const std::string& name, const std::string& value) mName = name; mValue = value; mMod = ""; + mSense = "positive"; } //////////////////////////////////////////////////////////////////////////////// @@ -154,6 +168,7 @@ Att::Att (const std::string& name, int value) mValue = s.str (); mMod = ""; + mSense = "positive"; } //////////////////////////////////////////////////////////////////////////////// @@ -162,6 +177,7 @@ Att::Att (const Att& other) mName = other.mName; mValue = other.mValue; mMod = other.mMod; + mSense = other.mSense; } //////////////////////////////////////////////////////////////////////////////// @@ -172,6 +188,7 @@ Att& Att::operator= (const Att& other) mName = other.mName; mValue = other.mValue; mMod = other.mMod; + mSense = other.mSense; } return *this; @@ -180,9 +197,10 @@ Att& Att::operator= (const Att& other) //////////////////////////////////////////////////////////////////////////////// bool Att::operator== (const Att& other) const { - return mName == other.mName && - mMod == other.mMod && - mValue == other.mValue; + return mName == other.mName && + mMod == other.mMod && + mValue == other.mValue && + mSense == other.mSense; } //////////////////////////////////////////////////////////////////////////////// @@ -190,6 +208,20 @@ Att::~Att () { } +//////////////////////////////////////////////////////////////////////////////// +bool Att::logicSense (bool match) const +{ + if (mSense == "positive") + return match; + else if (mSense == "negative") + return ! match; + else + { + context.debug ("mSense: " + mSense); + throw ("unknown mSense " + mSense); + } +} + //////////////////////////////////////////////////////////////////////////////// // For parsing. bool Att::valid (const std::string& input) const @@ -515,6 +547,7 @@ void Att::parse (Nibbler& n) mName = ""; mValue = ""; mMod = ""; + mSense = "positive"; if (n.getUntilOneOf (".:", mName)) { @@ -524,6 +557,13 @@ void Att::parse (Nibbler& n) if (n.skip ('.')) { std::string mod; + + if (n.skip ('~')) + { + context.debug ("using negative logic"); + mSense = "negative"; + } + if (n.getUntil (":", mod)) { if (validMod (mod)) diff --git a/src/Att.h b/src/Att.h index 9e5061fe6..74bed7f81 100644 --- a/src/Att.h +++ b/src/Att.h @@ -36,6 +36,7 @@ class Att public: Att (); Att (const std::string&, const std::string&, const std::string&); + Att (const std::string&, const std::string&, const std::string&, const std::string&); Att (const std::string&, const std::string&, int); Att (const std::string&, const std::string&); Att (const std::string&, int); @@ -44,6 +45,7 @@ public: bool operator== (const Att&) const; ~Att (); + bool logicSense (bool match) const; bool valid (const std::string&) const; static bool validInternalName (const std::string&); static bool validModifiableName (const std::string&); @@ -82,6 +84,7 @@ private: std::string mName; std::string mValue; std::string mMod; + std::string mSense; }; #endif diff --git a/src/Context.cpp b/src/Context.cpp index cbff74dfa..121bdf1c4 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -861,12 +861,20 @@ void Context::autoFilter (Att& a, Filter& f) } // Projects are matched left-most. - else if (a.name () == "project" && a.mod () == "") + else if (a.name () == "project" && (a.mod () == "" || a.mod () == "not")) { if (a.value () != "") { - f.push_back (Att ("project", "startswith", a.value ())); - debug ("auto filter: " + a.name () + ".startswith:" + a.value ()); + if (a.mod () == "not") + { + f.push_back (Att ("project", "startswith", a.value (), "negative")); + debug ("auto filter: " + a.name () + ".~startswith:" + a.value ()); + } + else + { + f.push_back (Att ("project", "startswith", a.value ())); + debug ("auto filter: " + a.name () + ".startswith:" + a.value ()); + } } else { diff --git a/src/Filter.cpp b/src/Filter.cpp index 3e5096f0e..b4f91a51e 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -81,12 +81,12 @@ bool Filter::pass (const Record& record) const // are willing to invest a week understanding and testing it. if (att->modType (att->mod ()) == "positive") { - if (! (pass || annotation_pass_count)) + if (! att->logicSense (pass || annotation_pass_count)) return false; } else { - if (! (pass && annotation_fail_count == 0)) + if (! att->logicSense (pass && annotation_fail_count == 0)) return false; } } @@ -102,7 +102,7 @@ bool Filter::pass (const Record& record) const // An individual attribute match failure is enough to fail the filter. if ((r = record.find (att->name ())) != record.end ()) { - if (! att->match (r->second)) + if (! att->logicSense (att->match (r->second))) return false; } else if (! att->match (Att ())) diff --git a/src/report.cpp b/src/report.cpp index 5aef3b18f..a3e2784b1 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -346,6 +346,9 @@ int longUsage (std::string& outs) << " word" << "\n" << " noword" << "\n" << "\n" + << "Modifiers can be inverted with the ~ character:" << "\n" + << " project.~is is equivalent to project.isnt" << "\n" + << "\n" << " For example:" << "\n" << " task list due.before:eom priority.not:L" << "\n" << "\n" diff --git a/test/filter-prefix.t b/test/filter-prefix.t new file mode 100755 index 000000000..705995369 --- /dev/null +++ b/test/filter-prefix.t @@ -0,0 +1,166 @@ +#! /usr/bin/perl +################################################################################ +## 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 +## +################################################################################ + +use strict; +use warnings; +use Test::More tests => 85; + +# Create the rc file. +if (open my $fh, '>', 'filter.rc') +{ + print $fh "data.location=.\n"; + close $fh; + ok (-r 'filter.rc', 'Created filter.rc'); +} + +# Test the filters. +qx{../src/task rc:filter.rc add project:foo.uno priority:H +tag one foo}; +qx{../src/task rc:filter.rc add project:foo.dos priority:H two}; +qx{../src/task rc:filter.rc add project:foo.tres three}; +qx{../src/task rc:filter.rc add project:bar.uno priority:H four}; +qx{../src/task rc:filter.rc add project:bar.dos +tag five}; +qx{../src/task rc:filter.rc add project:bar.tres six foo}; +qx{../src/task rc:filter.rc add project:bazuno seven bar foo}; +qx{../src/task rc:filter.rc add project:bazdos eight bar foo}; + +my $output = qx{../src/task rc:filter.rc list}; +like ($output, qr/one/, 'a1'); +like ($output, qr/two/, 'a2'); +like ($output, qr/three/, 'a3'); +like ($output, qr/four/, 'a4'); +like ($output, qr/five/, 'a5'); +like ($output, qr/six/, 'a6'); +like ($output, qr/seven/, 'a7'); +like ($output, qr/eight/, 'a8'); + +$output = qx{../src/task rc:filter.rc list project:foo}; +like ($output, qr/one/, 'b1'); +like ($output, qr/two/, 'b2'); +like ($output, qr/three/, 'b3'); +unlike ($output, qr/four/, 'b4'); +unlike ($output, qr/five/, 'b5'); +unlike ($output, qr/six/, 'b6'); +unlike ($output, qr/seven/, 'b7'); +unlike ($output, qr/eight/, 'b8'); + +$output = qx{../src/task rc:filter.rc list project.not:foo}; +unlike ($output, qr/one/, 'c1'); +unlike ($output, qr/two/, 'c2'); +unlike ($output, qr/three/, 'c3'); +like ($output, qr/four/, 'c4'); +like ($output, qr/five/, 'c5'); +like ($output, qr/six/, 'c6'); +like ($output, qr/seven/, 'c7'); +like ($output, qr/eight/, 'c8'); + +$output = qx{../src/task rc:filter.rc list project.startswith:bar}; +unlike ($output, qr/one/, 'd1'); +unlike ($output, qr/two/, 'd2'); +unlike ($output, qr/three/, 'd3'); +like ($output, qr/four/, 'd4'); +like ($output, qr/five/, 'd5'); +like ($output, qr/six/, 'd6'); +unlike ($output, qr/seven/, 'd7'); +unlike ($output, qr/eight/, 'd8'); + +$output = qx{../src/task rc:filter.rc list project.~startswith:bar}; +like ($output, qr/one/, 'e1'); +like ($output, qr/two/, 'e2'); +like ($output, qr/three/, 'e3'); +unlike ($output, qr/four/, 'e4'); +unlike ($output, qr/five/, 'e5'); +unlike ($output, qr/six/, 'e6'); +like ($output, qr/seven/, 'e7'); +like ($output, qr/eight/, 'e8'); + +$output = qx{../src/task rc:filter.rc list project:ba}; +unlike ($output, qr/one/, 'f1'); +unlike ($output, qr/two/, 'f2'); +unlike ($output, qr/three/, 'f3'); +like ($output, qr/four/, 'f4'); +like ($output, qr/five/, 'f5'); +like ($output, qr/six/, 'f6'); +like ($output, qr/seven/, 'f7'); +like ($output, qr/eight/, 'f8'); + +$output = qx{../src/task rc:filter.rc list project.not:ba}; +like ($output, qr/one/, 'g1'); +like ($output, qr/two/, 'g2'); +like ($output, qr/three/, 'g3'); +unlike ($output, qr/four/, 'g4'); +unlike ($output, qr/five/, 'g5'); +unlike ($output, qr/six/, 'g6'); +unlike ($output, qr/seven/, 'g7'); +unlike ($output, qr/eight/, 'g8'); + +$output = qx{../src/task rc:filter.rc list project.~startswith:ba}; +like ($output, qr/one/, 'h1'); +like ($output, qr/two/, 'h2'); +like ($output, qr/three/, 'h3'); +unlike ($output, qr/four/, 'h4'); +unlike ($output, qr/five/, 'h5'); +unlike ($output, qr/six/, 'h6'); +unlike ($output, qr/seven/, 'h7'); +unlike ($output, qr/eight/, 'h8'); + +$output = qx{../src/task rc:filter.rc list description.has:foo}; +like ($output, qr/one/, 'i1'); +unlike ($output, qr/two/, 'i2'); +unlike ($output, qr/three/, 'i3'); +unlike ($output, qr/four/, 'i4'); +unlike ($output, qr/five/, 'i5'); +like ($output, qr/six/, 'i6'); +like ($output, qr/seven/, 'i7'); +like ($output, qr/eight/, 'i8'); + +$output = qx{../src/task rc:filter.rc list description.~has:foo}; +unlike ($output, qr/one/, 'j1'); +like ($output, qr/two/, 'j2'); +like ($output, qr/three/, 'j3'); +like ($output, qr/four/, 'j4'); +like ($output, qr/five/, 'j5'); +unlike ($output, qr/six/, 'j6'); +unlike ($output, qr/seven/, 'j7'); +unlike ($output, qr/eight/, 'j8'); + + +# Cleanup. +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +unlink 'completed.data'; +ok (!-r 'completed.data', 'Removed completed.data'); + +unlink 'undo.data'; +ok (!-r 'undo.data', 'Removed undo.data'); + +unlink 'filter.rc'; +ok (!-r 'filter.rc', 'Removed filter.rc'); + +exit 0; +