Enhancements - Cmd object

- New Cmd object to handle localized commands, customReports and general
  command parsing.
- Localized new Subst methods.
- Relocate guess method from parse.cpp to text.cpp.
- Converted Att object to use new valid/parse scheme.
- Unit tests for Cmd object.
- Fixed att.t.cpp unit tests.
This commit is contained in:
Paul Beckingham 2009-06-07 14:00:14 -04:00
parent ffa0c6e758
commit 24f31eeb00
17 changed files with 416 additions and 52 deletions

View file

@ -26,7 +26,7 @@
113 Unrecognized character(s) at end of substitution 113 Unrecognized character(s) at end of substitution
114 Malformed substitution 114 Malformed substitution
# 2xx Commands # 2xx Commands - must be sequential
200 active 200 active
201 add 201 add
202 append 202 append
@ -57,7 +57,7 @@
227 undo 227 undo
228 version 228 version
# 3xx Attributes # 3xx Attributes - must be sequential
300 project 300 project
301 priority 301 priority
302 fg 302 fg
@ -71,7 +71,7 @@
310 mask 310 mask
311 imask 311 imask
# 35x Attribute modifiers # 35x Attribute modifiers - must be sequential
350 before 350 before
351 after 351 after
352 not 352 not

View file

@ -86,13 +86,39 @@ Att::~Att ()
{ {
} }
////////////////////////////////////////////////////////////////////////////////
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)))
return true;
return false;
}
return false;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// //
// start --> name --> . --> mod --> : --> " --> value --> " --> end // start --> name --> . --> mod --> : --> " --> value --> " --> end
// ^ | // ^ |
// |__________| // |__________|
// //
bool Att::parse (Nibbler& n) void Att::parse (Nibbler& n)
{ {
// Ensure a clean object first. // Ensure a clean object first.
mName = ""; mName = "";
@ -126,7 +152,6 @@ bool Att::parse (Nibbler& n)
n.getUntil (' ', mValue)) n.getUntil (' ', mValue))
{ {
decode (mValue); decode (mValue);
return true;
} }
else else
throw std::string ("Missing attribute value"); // TODO i18n throw std::string ("Missing attribute value"); // TODO i18n
@ -136,8 +161,6 @@ bool Att::parse (Nibbler& n)
} }
else else
throw std::string ("Missing : after attribute name"); // TODO i18n throw std::string ("Missing : after attribute name"); // TODO i18n
return false;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View file

@ -41,7 +41,8 @@ public:
Att& operator= (const Att&); // Assignment operator Att& operator= (const Att&); // Assignment operator
~Att (); // Destructor ~Att (); // Destructor
bool parse (Nibbler&); bool valid (const std::string&) const;
void parse (Nibbler&);
bool validMod (const std::string&) const; bool validMod (const std::string&) const;
bool match (const Att&) const; bool match (const Att&) const;

175
src/Cmd.cpp Normal file
View file

@ -0,0 +1,175 @@
////////////////////////////////////////////////////////////////////////////////
// task - a command line task list manager.
//
// Copyright 2006 - 2009, 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 <iostream>
#include "Cmd.h"
#include "Context.h"
#include "util.h"
#include "text.h"
#include "i18n.h"
extern Context context;
////////////////////////////////////////////////////////////////////////////////
Cmd::Cmd ()
: command ("")
{
}
////////////////////////////////////////////////////////////////////////////////
Cmd::Cmd (const std::string& input)
{
parse (input);
}
////////////////////////////////////////////////////////////////////////////////
Cmd::~Cmd ()
{
}
////////////////////////////////////////////////////////////////////////////////
// Determines whether the string represents a unique command name or custom
// report name.
bool Cmd::valid (const std::string& input)
{
loadCommands ();
loadCustomReports ();
std::string candidate = lowerCase (input);
std::vector <std::string> matches;
autoComplete (candidate, commands, matches);
if (0 == matches.size ())
return false;
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Determines whether the string represents a valid custom report name.
bool Cmd::validCustom (const std::string& input)
{
loadCustomReports ();
std::vector <std::string> matches;
autoComplete (lowerCase (input), customReports, matches);
return matches.size () == 1 ? true : false;
}
////////////////////////////////////////////////////////////////////////////////
void Cmd::parse (const std::string& input)
{
loadCommands ();
loadCustomReports ();
std::string candidate = lowerCase (input);
std::vector <std::string> matches;
autoComplete (candidate, commands, matches);
if (1 == matches.size ())
command = matches[0];
else if (0 == matches.size ())
command = "";
else
{
std::string error = "Ambiguous command '" + candidate + "' - could be either of "; // TODO i18n
std::string combined;
join (combined, ", ", matches);
error += combined;
throw error + combined;
}
}
////////////////////////////////////////////////////////////////////////////////
void Cmd::loadCommands ()
{
if (commands.size () == 0)
{
commands.push_back (context.stringtable.get (CMD_ACTIVE, "active"));
commands.push_back (context.stringtable.get (CMD_ADD, "add"));
commands.push_back (context.stringtable.get (CMD_APPEND, "append"));
commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate"));
commands.push_back (context.stringtable.get (CMD_CALENDAR, "calendar"));
commands.push_back (context.stringtable.get (CMD_COLORS, "colors"));
commands.push_back (context.stringtable.get (CMD_COMPLETED, "completed"));
commands.push_back (context.stringtable.get (CMD_DELETE, "delete"));
commands.push_back (context.stringtable.get (CMD_DONE, "done"));
commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate"));
commands.push_back (context.stringtable.get (CMD_EDIT, "edit"));
commands.push_back (context.stringtable.get (CMD_EXPORT, "export"));
commands.push_back (context.stringtable.get (CMD_HELP, "help"));
commands.push_back (context.stringtable.get (CMD_HISTORY, "history"));
commands.push_back (context.stringtable.get (CMD_GHISTORY, "ghistory"));
commands.push_back (context.stringtable.get (CMD_IMPORT, "import"));
commands.push_back (context.stringtable.get (CMD_INFO, "info"));
commands.push_back (context.stringtable.get (CMD_NEXT, "next"));
commands.push_back (context.stringtable.get (CMD_OVERDUE, "overdue"));
commands.push_back (context.stringtable.get (CMD_PROJECTS, "projects"));
commands.push_back (context.stringtable.get (CMD_START, "start"));
commands.push_back (context.stringtable.get (CMD_STATS, "statistics"));
commands.push_back (context.stringtable.get (CMD_STOP, "stop"));
commands.push_back (context.stringtable.get (CMD_SUMMARY, "summary"));
commands.push_back (context.stringtable.get (CMD_TAGS, "tags"));
commands.push_back (context.stringtable.get (CMD_TIMESHEET, "timesheet"));
commands.push_back (context.stringtable.get (CMD_UNDELETE, "undelete"));
commands.push_back (context.stringtable.get (CMD_UNDO, "undo"));
commands.push_back (context.stringtable.get (CMD_VERSION, "version"));
}
}
////////////////////////////////////////////////////////////////////////////////
void Cmd::loadCustomReports ()
{
if (customReports.size () == 0)
{
std::vector <std::string> all;
context.config.all (all);
foreach (i, all)
{
if (i->substr (0, 7) == "report.")
{
std::string report = i->substr (7, std::string::npos);
std::string::size_type columns = report.find (".columns");
if (columns != std::string::npos)
{
report = report.substr (0, columns);
// A custom report is also a command.
customReports.push_back (report);
commands.push_back (report);
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////

58
src/Cmd.h Normal file
View file

@ -0,0 +1,58 @@
////////////////////////////////////////////////////////////////////////////////
// task - a command line task list manager.
//
// Copyright 2006 - 2009, 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
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_CMD
#define INCLUDED_CMD
#include <vector>
#include <string>
#include "Cmd.h"
class Cmd
{
public:
Cmd (); // Default constructor
Cmd (const std::string&); // Default constructor
~Cmd (); // Destructor
bool valid (const std::string&);
bool validCustom (const std::string&);
void parse (const std::string&);
public:
std::string command;
private:
void loadCommands ();
void loadCustomReports ();
private:
std::vector <std::string> commands;
std::vector <std::string> customReports;
};
#endif
////////////////////////////////////////////////////////////////////////////////

View file

@ -275,7 +275,8 @@ void Context::parse ()
else if (arg[0] == '-') else if (arg[0] == '-')
task.addRemoveTag (arg->substr (1, std::string::npos)); task.addRemoveTag (arg->substr (1, std::string::npos));
} }
*/
/*
// Attributes contain a constant string followed by a colon, followed by a // Attributes contain a constant string followed by a colon, followed by a
// value. // value.
else if ((colon = arg->find (":")) != std::string::npos) else if ((colon = arg->find (":")) != std::string::npos)
@ -312,7 +313,6 @@ void Context::parse ()
std::cout << "# found subst" << std::endl; std::cout << "# found subst" << std::endl;
subst.parse (*arg); subst.parse (*arg);
} }
/* /*
// Command. // Command.
else if (command == "") else if (command == "")
@ -331,7 +331,6 @@ void Context::parse ()
} }
} }
*/ */
// Anything else is just considered description. // Anything else is just considered description.
else else
{ {

View file

@ -2,10 +2,10 @@ bin_PROGRAMS = task
task_SOURCES = Config.cpp Date.cpp Record.cpp T.cpp T2.cpp TDB.cpp TDB2.cpp \ task_SOURCES = Config.cpp Date.cpp Record.cpp T.cpp T2.cpp TDB.cpp TDB2.cpp \
Att.cpp Filter.cpp Sequence.cpp Table.cpp Grid.cpp Timer.cpp \ Att.cpp Filter.cpp Sequence.cpp Table.cpp Grid.cpp Timer.cpp \
Duration.cpp StringTable.cpp Location.cpp Subst.cpp Keymap.cpp \ Duration.cpp StringTable.cpp Location.cpp Subst.cpp Keymap.cpp \
Nibbler.cpp Context.cpp color.cpp parse.cpp task.cpp edit.cpp \ Nibbler.cpp Context.cpp Cmd.cpp color.cpp parse.cpp task.cpp \
command.cpp report.cpp util.cpp text.cpp rules.cpp import.cpp \ edit.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp \
interactive.cpp \ import.cpp interactive.cpp \
Config.h Date.h Record.h T.h TDB.h Att.h Filter.h Sequence.h \ Config.h Date.h Record.h T.h TDB.h Att.h Filter.h Sequence.h \
Table.h Grid.h Timer.h Duration.h StringTable.h Location.h \ Table.h Grid.h Timer.h Duration.h StringTable.h Location.h \
Subst.h Keymap.h Nibbler.h Context.h color.h task.h Subst.h Keymap.h Nibbler.h Context.h Cmd.h color.h task.h

View file

@ -95,8 +95,9 @@ void Record::parse (const std::string& input)
Nibbler nl (line); Nibbler nl (line);
Att a; Att a;
while (!nl.depleted () && a.parse (nl)) while (!nl.depleted ())
{ {
a.parse (nl);
(*this)[a.name ()] = a; (*this)[a.name ()] = a;
nl.skip (' '); nl.skip (' ');
} }

View file

@ -25,6 +25,17 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
//
// Strings that should be localized:
// - All text output that the user sees or types
//
// Strings that should NOT be localized:
// - ./taskrc configuration variable names
// - certain literals associated with parsing
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_I18N #ifndef INCLUDED_I18N
#define INCLUDED_I18N #define INCLUDED_I18N

View file

@ -158,38 +158,6 @@ static const char* commands[] =
static std::vector <std::string> customReports; static std::vector <std::string> customReports;
////////////////////////////////////////////////////////////////////////////////
void guess (
const std::string& type,
std::vector<std::string>& options,
std::string& candidate)
{
std::vector <std::string> matches;
autoComplete (candidate, options, matches);
if (1 == matches.size ())
candidate = matches[0];
else if (0 == matches.size ())
candidate = "";
else
{
std::string error = "Ambiguous ";
error += type;
error += " '";
error += candidate;
error += "' - could be either of ";
for (size_t i = 0; i < matches.size (); ++i)
{
if (i)
error += ", ";
error += matches[i];
}
throw error;
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void guess ( void guess (
const std::string& type, const std::string& type,
@ -375,7 +343,7 @@ bool validDescription (const std::string& input)
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
static bool validCommand (std::string& input) bool validCommand (std::string& input)
{ {
std::string copy = input; std::string copy = input;
guess ("command", commands, copy); guess ("command", commands, copy);

View file

@ -43,6 +43,7 @@ bool validPriority (const std::string&);
bool validDate (std::string&); bool validDate (std::string&);
bool validDuration (std::string&); bool validDuration (std::string&);
bool validDescription (const std::string&); bool validDescription (const std::string&);
bool validCommand (std::string&);
void loadCustomReports (); void loadCustomReports ();
bool isCustomReport (const std::string&); bool isCustomReport (const std::string&);
void allCustomReports (std::vector <std::string>&); void allCustomReports (std::vector <std::string>&);

View file

@ -14,3 +14,4 @@ stringtable.t
nibbler.t nibbler.t
subst.t subst.t
filt.t filt.t
cmd.t

View file

@ -1,11 +1,12 @@
PROJECT = t.t t2.t tdb.t date.t duration.t t.benchmark.t text.t autocomplete.t \ PROJECT = t.t t2.t tdb.t date.t duration.t t.benchmark.t text.t autocomplete.t \
parse.t seq.t att.t stringtable.t record.t nibbler.t subst.t filt.t parse.t seq.t att.t stringtable.t record.t nibbler.t subst.t filt.t \
cmd.t
CFLAGS = -I. -I.. -Wall -pedantic -ggdb3 -fno-rtti CFLAGS = -I. -I.. -Wall -pedantic -ggdb3 -fno-rtti
LFLAGS = -L/usr/local/lib LFLAGS = -L/usr/local/lib
OBJECTS = ../TDB.o ../TDB2.o ../T.o ../T2.o ../parse.o ../text.o ../Date.o \ OBJECTS = ../TDB.o ../TDB2.o ../T.o ../T2.o ../parse.o ../text.o ../Date.o \
../Duration.o ../util.o ../Config.o ../Sequence.o ../Att.o \ ../Duration.o ../util.o ../Config.o ../Sequence.o ../Att.o \
../Record.o ../StringTable.o ../Subst.o ../Nibbler.o ../Location.o \ ../Record.o ../StringTable.o ../Subst.o ../Nibbler.o ../Location.o \
../Filter.o ../Context.o ../Keymap.o ../Filter.o ../Context.o ../Keymap.o ../Cmd.o
all: $(PROJECT) all: $(PROJECT)
@ -69,3 +70,6 @@ nibbler.t: nibbler.t.o $(OBJECTS) test.o
filt.t: filt.t.o $(OBJECTS) test.o filt.t: filt.t.o $(OBJECTS) test.o
g++ filt.t.o $(OBJECTS) test.o $(LFLAGS) -o filt.t g++ filt.t.o $(OBJECTS) test.o $(LFLAGS) -o filt.t
cmd.t: cmd.t.o $(OBJECTS) test.o
g++ cmd.t.o $(OBJECTS) test.o $(LFLAGS) -o cmd.t

View file

@ -33,7 +33,25 @@ Context context;
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int main (int argc, char** argv) int main (int argc, char** argv)
{ {
UnitTest t (59); UnitTest t (74);
Att a;
t.notok (a.valid ("name"), "Att::valid name -> fail");
t.notok (a.valid (":"), "Att::valid : -> fail");
t.notok (a.valid (":value"), "Att::valid :value -> fail");
t.ok (a.valid ("name:value"), "Att::valid name:value");
t.ok (a.valid ("name:value "), "Att::valid name:value\\s");
t.ok (a.valid ("name:'value'"), "Att::valid name:'value'");
t.ok (a.valid ("name:'one two'"), "Att::valid name:'one two'");
t.ok (a.valid ("name:\"value\""), "Att::valid name:\"value\"");
t.ok (a.valid ("name:\"one two\""), "Att::valid name:\"one two\"");
t.ok (a.valid ("name:"), "Att::valid name:");
t.ok (a.valid ("name:&quot;"), "Att::valid &quot;");
t.ok (a.valid ("name.one:value"), "Att::valid name.one.value");
t.ok (a.valid ("name.one.two:value"), "Att::valid name.one.two:value");
t.ok (a.valid ("name.:value"), "Att::valid name.:value");
t.ok (a.valid ("name..:value"), "Att::valid name..:value");
Att a1 ("name", "value"); Att a1 ("name", "value");
t.is (a1.name (), "name", "Att::Att (name, value), Att.name"); t.is (a1.name (), "name", "Att::Att (name, value), Att.name");

71
src/tests/cmd.t.cpp Normal file
View file

@ -0,0 +1,71 @@
////////////////////////////////////////////////////////////////////////////////
// task - a command line task list manager.
//
// Copyright 2006 - 2009, 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 <Context.h>
#include <Cmd.h>
#include <test.h>
Context context;
////////////////////////////////////////////////////////////////////////////////
int main (int argc, char** argv)
{
UnitTest t (18);
context.config.set ("report.foo.columns", "id");
Cmd cmd;
t.ok (cmd.valid ("annotate"), "Cmd::valid annotate");
t.ok (cmd.valid ("annotat"), "Cmd::valid annotat");
t.ok (cmd.valid ("annota"), "Cmd::valid annota");
t.ok (cmd.valid ("annot"), "Cmd::valid annot");
t.ok (cmd.valid ("anno"), "Cmd::valid anno");
t.ok (cmd.valid ("ann"), "Cmd::valid ann");
t.ok (cmd.valid ("an"), "Cmd::valid an");
t.ok (cmd.valid ("ANNOTATE"), "Cmd::valid ANNOTATE");
t.ok (cmd.valid ("ANNOTAT"), "Cmd::valid ANNOTAT");
t.ok (cmd.valid ("ANNOTA"), "Cmd::valid ANNOTA");
t.ok (cmd.valid ("ANNOT"), "Cmd::valid ANNOT");
t.ok (cmd.valid ("ANNO"), "Cmd::valid ANNO");
t.ok (cmd.valid ("ANN"), "Cmd::valid ANN");
t.ok (cmd.valid ("AN"), "Cmd::valid AN");
t.ok (cmd.validCustom ("foo"), "Cmd::validCustom foo");
t.notok (cmd.validCustom ("bar"), "Cmd::validCustom bar -> fail");
bool good = true;
try { cmd.parse ("a"); } catch (...) { good = false; }
t.notok (good, "Cmd::parse a -> fail");
good = true;
try { cmd.parse ("add"); } catch (...) { good = false; }
t.ok (good, "Cmd::parse add");
return 0;
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -305,3 +305,35 @@ const char* optionalBlankLine ()
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void guess (
const std::string& type,
std::vector<std::string>& options,
std::string& candidate)
{
std::vector <std::string> matches;
autoComplete (candidate, options, matches);
if (1 == matches.size ())
candidate = matches[0];
else if (0 == matches.size ())
candidate = "";
else
{
std::string error = "Ambiguous "; // TODO i18n
error += type;
error += " '";
error += candidate;
error += "' - could be either of "; // TODO i18n
for (size_t i = 0; i < matches.size (); ++i)
{
if (i)
error += ", ";
error += matches[i];
}
throw error;
}
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -45,6 +45,7 @@ std::string commify (const std::string&);
std::string lowerCase (const std::string&); std::string lowerCase (const std::string&);
std::string upperCase (const std::string&); std::string upperCase (const std::string&);
const char* optionalBlankLine (); const char* optionalBlankLine ();
void guess (const std::string&, std::vector<std::string>&, std::string&);
#endif #endif
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////