From 0c5a71b02ffc3cffa7d3c69b4cd0dac3df176ec9 Mon Sep 17 00:00:00 2001 From: Paul Beckingham Date: Wed, 27 Jan 2010 12:26:06 -0500 Subject: [PATCH] Enhancement - caseless substitution - Substitutions "task /from/to/" now obey the rc.search.case.sensitive setting. --- src/Config.cpp | 3 ++ src/Subst.cpp | 38 ++++++++++---- src/command.cpp | 22 ++++---- src/tests/caseless.t | 119 ++++++++++++++++++++++++++++++++++++++++++ src/tests/subst.t.cpp | 41 ++++++++++++++- 5 files changed, 203 insertions(+), 20 deletions(-) create mode 100755 src/tests/caseless.t diff --git a/src/Config.cpp b/src/Config.cpp index 0f0490764..b9dd5b473 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -63,6 +63,7 @@ std::string Config::defaults = "next=2 # How many tasks per project in next report\n" "bulk=2 # > 2 tasks considered 'a lot', for confirmation\n" "nag=You have higher priority tasks. # Nag message to keep you honest\n" // TODO + "search.case.sensitive=yes # Setting to no allows case insensitive searches\n" "\n" "# Dates\n" "dateformat=m/d/Y # Preferred input and display date format\n" @@ -71,6 +72,8 @@ std::string Config::defaults = "weekstart=Sunday # Sunday or Monday only\n" "displayweeknumber=yes # Show week numbers on calendar\n" "due=7 # Task is considered due in 7 days\n" + "\n" + "# Calendar controls\n" "calendar.legend=yes # Display the legend on calendar\n" "calendar.details=sparse # Calendar shows information for tasks w/due dates: full, sparse or none\n" "calendar.details.report=list # Report to use when showing task information in cal\n" diff --git a/src/Subst.cpp b/src/Subst.cpp index 1585bce61..0dc9433a4 100644 --- a/src/Subst.cpp +++ b/src/Subst.cpp @@ -28,6 +28,7 @@ #include "Subst.h" #include "Nibbler.h" #include "Context.h" +#include "text.h" #include "i18n.h" extern Context context; @@ -121,31 +122,48 @@ void Subst::apply ( std::vector & annotations) const { std::string::size_type pattern; + bool sensitive = context.config.getBoolean ("search.case.sensitive"); if (mFrom != "") { if (mGlobal) { // Perform all subs on description. - while ((pattern = description.find (mFrom)) != std::string::npos) + int counter = 0; + pattern = 0; + + while ((pattern = find (description, mFrom, pattern, sensitive)) != std::string::npos) + { description.replace (pattern, mFrom.length (), mTo); + pattern += mTo.length (); + + if (++counter > 1000) + throw ("Terminated substitution because more than a thousand changes were made - infinite loop protection."); + } // Perform all subs on annotations. + counter = 0; + pattern = 0; std::vector ::iterator i; for (i = annotations.begin (); i != annotations.end (); ++i) { - std::string description = i->value (); - while ((pattern = description.find (mFrom)) != std::string::npos) + std::string annotation = i->value (); + while ((pattern = find (annotation, mFrom, pattern, sensitive)) != std::string::npos) { - description.replace (pattern, mFrom.length (), mTo); - i->value (description); + annotation.replace (pattern, mFrom.length (), mTo); + pattern += mTo.length (); + + i->value (annotation); + + if (++counter > 1000) + throw ("Terminated substitution because more than a thousand changes were made - infinite loop protection."); } } } else { // Perform first description substitution. - if ((pattern = description.find (mFrom)) != std::string::npos) + if ((pattern = find (description, mFrom, sensitive)) != std::string::npos) description.replace (pattern, mFrom.length (), mTo); // Failing that, perform the first annotation substitution. @@ -154,11 +172,11 @@ void Subst::apply ( std::vector ::iterator i; for (i = annotations.begin (); i != annotations.end (); ++i) { - std::string description = i->value (); - if ((pattern = description.find (mFrom)) != std::string::npos) + std::string annotation = i->value (); + if ((pattern = find (annotation, mFrom, sensitive)) != std::string::npos) { - description.replace (pattern, mFrom.length (), mTo); - i->value (description); + annotation.replace (pattern, mFrom.length (), mTo); + i->value (annotation); break; } } diff --git a/src/command.cpp b/src/command.cpp index 42ed7eae3..8e58f3074 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -649,15 +649,19 @@ int handleConfig (std::string &outs) // These are the regular configuration variables. // Note that there is a leading and trailing space, to make searching easier. std::string recognized = - " annotations blanklines bulk calendar.details calendar.details.report calendar.holidays " - "calendar.legend color color.active color.due color.overdue color.pri.H color.pri.L " - "color.pri.M color.pri.none color.recurring color.tagged color.footnote color.header " - "color.debug color.alternate color.calendar.today color.calendar.due color.calendar.overdue " - "color.calendar.weekend color.calendar.holiday color.calendar.weeknumber confirmation " - "curses data.location dateformat dateformat.holiday dateformat.report debug default.command " - "default.priority default.project defaultwidth due locale displayweeknumber echo.command " - "fontunderline locking monthsperline nag next project shadow.command shadow.file shadow.notify " - "weekstart editor import.synonym.id import.synonym.uuid complete.all.projects complete.all.tags " + " annotations blanklines bulk calendar.details calendar.details.report " + "calendar.holidays calendar.legend color color.active color.due " + "color.overdue color.pri.H color.pri.L color.pri.M color.pri.none " + "color.recurring color.tagged color.footnote color.header color.debug " + "color.alternate color.calendar.today color.calendar.due " + "color.calendar.overdue color.calendar.weekend color.calendar.holiday " + "color.calendar.weeknumber confirmation curses data.location dateformat " + "dateformat.holiday dateformat.report debug default.command " + "default.priority default.project defaultwidth due locale " + "displayweeknumber echo.command fontunderline locking monthsperline nag " + "next project shadow.command shadow.file shadow.notify weekstart editor " + "import.synonym.id import.synonym.uuid complete.all.projects " + "complete.all.tags search.case.sensitive " #ifdef FEATURE_SHELL "shell.prompt " #endif diff --git a/src/tests/caseless.t b/src/tests/caseless.t new file mode 100755 index 000000000..c7a0eb884 --- /dev/null +++ b/src/tests/caseless.t @@ -0,0 +1,119 @@ +#! /usr/bin/perl +################################################################################ +## task - 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 +## +################################################################################ + +use strict; +use warnings; +use Test::More tests => 21; + +# Create the rc file. +if (open my $fh, '>', 'caseless.rc') +{ + print $fh "data.location=.\n"; + close $fh; + ok (-r 'append.rc', 'Created append.rc'); +} + +# Attempt case-sensitive and case-insensitive substitutions and filters. +qx{../task add one two three}; +qx{../task 1 annotate four five six}; + +# Description substitution. +qx{../task rc.search.case.sensitive=yes 1 /One/ONE/}; +my $output = qx{../task info 1}; +unlike ($output, qr/One two three/, 'one two three\nfour five six -> /One/ONE/ = fail'); + +qx{../task rc.search.case.sensitive=no 1 /One/ONE/}; +$output = qx{../task info 1}; +like ($output, qr/ONE two three/, 'one two three\nfour five six -> /One/ONE/ = caseless succeed'); + +qx{../task rc.search.case.sensitive=yes 1 /one/One/}; +$output = qx{../task info 1}; +unlike ($output, qr/One two three/, 'ONE two three\nfour five six -> /one/ONE/ = fail'); + +qx{../task rc.search.case.sensitive=no 1 /one/one/}; +$output = qx{../task info 1}; +like ($output, qr/one two three/, 'ONE two three\nfour five six -> /one/one/ = caseless succeed'); + +# Annotation substitution. +qx{../task rc.search.case.sensitive=yes 1 /Five/FIVE/}; +$output = qx{../task info 1}; +unlike ($output, qr/four five six/, 'one two three\nfour five six -> /Five/FIVE/ = fail'); + +qx{../task rc.search.case.sensitive=no 1 /Five/FIVE/}; +$output = qx{../task info 1}; +like ($output, qr/four FIVE six/, 'one two three\nfour five six -> /Five/FIVE/ = caseless succeed'); + +qx{../task rc.search.case.sensitive=yes 1 /five/Five/}; +$output = qx{../task info 1}; +unlike ($output, qr/four five six/, 'one two three\nfour FIVE six -> /five/Five/ = fail'); + +qx{../task rc.search.case.sensitive=no 1 /five/five/}; +$output = qx{../task info 1}; +like ($output, qr/four FIVE six/, 'one two three\nfour FIVE six -> /five/five/ = caseless succeed'); + +# Description filter. +$output = qx{../task rc.search.case.sensitive=yes ls One}; +unlike ($output, qr/one two three/, 'one two three\bfour five six -> ls One = fail'); + +$output = qx{../task rc.search.case.sensitive=no ls One}; +like ($output, qr/one two three/, 'one two three\bfour five six -> ls One caseless = succeed'); + +$output = qx{../task rc.search.case.sensitive=yes ls Five}; +unlike ($output, qr/four five six/, 'one two three\bfour five six -> ls Five = fail'); + +$output = qx{../task rc.search.case.sensitive=no ls Five}; +like ($output, qr/four five six/, 'one two three\bfour five six -> ls Five caseless = succeed'); + +# Annotation filter. +$output = qx{../task rc.search.case.sensitive=yes ls description.contains:Three}; +unlike ($output, qr/one two three/, 'one two three\bfour five six -> ls description.contains:Three = fail'); + +$output = qx{../task rc.search.case.sensitive=no ls description.contains:Three}; +like ($output, qr/one two three/, 'one two three\bfour five six -> ls description.contains:Three caseless = succeed'); + +$output = qx{../task rc.search.case.sensitive=yes ls description.contains:Six}; +unlike ($output, qr/four five six/, 'one two three\bfour five six -> ls description.contains:Six = fail'); + +$output = qx{../task rc.search.case.sensitive=no ls description.contains:Six}; +like ($output, qr/four five six/, 'one two three\bfour five six -> ls description.contains:Six caseless = succeed'); + +# 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 'append.rc'; +ok (!-r 'append.rc', 'Removed append.rc'); + +exit 0; + diff --git a/src/tests/subst.t.cpp b/src/tests/subst.t.cpp index 8b98a0c8a..4ba14f88d 100644 --- a/src/tests/subst.t.cpp +++ b/src/tests/subst.t.cpp @@ -34,11 +34,13 @@ Context context; //////////////////////////////////////////////////////////////////////////////// int main (int argc, char** argv) { - UnitTest t (15); + UnitTest t (19); Task task; task.set ("description", "one two three four"); + context.config.set ("search.case.sensitive", "yes"); + Subst s; t.ok (s.valid ("/a/b/x"), "valid /a/b/x"); t.ok (s.valid ("/a/b/"), "valid /a/b/"); @@ -90,6 +92,43 @@ int main (int argc, char** argv) t.fail ("failed to parse '/e /E /g'"); } + // Now repeat the last two tests with a case-insensitive setting. + context.config.set ("search.case.sensitive", "no"); + good = true; + try { s.parse ("/tWo/TWO/"); } catch (...) { good = false; } + t.ok (good, "parsed /tWo/TWO/ (rc.search.case.sensitive=no)"); + if (good) + { + std::string description = task.get ("description"); + std::vector annotations; + task.getAnnotations (annotations); + + s.apply (description, annotations); + t.is (description, "one TWO three four", "single word subst (rc.search.case.sensitive=no)"); + } + else + { + t.fail ("failed to parse '/tWo/TWO/' (rc.search.case.sensitive=no)"); + } + + good = true; + try { s.parse ("/E /E /g"); } catch (...) { good = false; } + t.ok (good, "parsed /E /E /g (rc.search.case.sensitive=no)"); + if (good) + { + std::string description = task.get ("description"); + std::vector annotations; + task.getAnnotations (annotations); + + s.apply (description, annotations); + t.is (description, "onE two threE four", "multiple word subst (rc.search.case.sensitive=no)"); + } + else + { + t.fail ("failed to parse '/E /E /g' (rc.search.case.sensitive=no)"); + } + + context.config.set ("search.case.sensitive", "yes"); good = true; try { s.parse ("/from/to/g"); } catch (...) { good = false; } t.ok (good, "parsed /from/to/g");