mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00

- Implemented CmdHelp object that replaces the report.cpp longUsage function, and builds the output dynamically from other Command objects. This is also why the help text right now is very short, as only a few commands are migrated. - Obsoleted longUsage function. - Updated task.1 man page with 'execute' command details. - Modified command.lua sample to include command usage. - Removed "help" from old Context::dispatch, which means "help" is the first migrated command. - Added usage and description to all Cmd* objects. - Implemented Command::usage and Command::description as base class methods that simply return data that is specified by the derived classes.
1067 lines
32 KiB
C++
1067 lines
32 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
||
// 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 <iostream>
|
||
#include <fstream>
|
||
#include <algorithm>
|
||
#include <pwd.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <unistd.h>
|
||
#include <sys/select.h>
|
||
#include <Context.h>
|
||
#include <Directory.h>
|
||
#include <File.h>
|
||
#include <Timer.h>
|
||
#include <text.h>
|
||
#include <util.h>
|
||
#include <main.h>
|
||
#include <i18n.h>
|
||
#include <../cmake.h>
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
Context::Context ()
|
||
: config ()
|
||
, filter ()
|
||
, sequence ()
|
||
, subst ()
|
||
, task ()
|
||
, tdb ()
|
||
, tdb2 ()
|
||
, program ("")
|
||
, commandLine ("")
|
||
, file_override ("")
|
||
, var_overrides ("")
|
||
, cmd ()
|
||
, dom ()
|
||
, use_color (true)
|
||
, verbosity_legacy (false)
|
||
, inShadow (false)
|
||
, terminal_width (0)
|
||
, terminal_height (0)
|
||
{
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
Context::~Context ()
|
||
{
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::initialize2 (int argc, char** argv)
|
||
{
|
||
Timer t ("Context::initialize2");
|
||
|
||
// Capture the args.
|
||
for (int i = 0; i < argc; ++i)
|
||
{
|
||
if (i == 0)
|
||
{
|
||
program = argv[i];
|
||
std::string::size_type cal = program.find ("/cal");
|
||
if (program == "cal" ||
|
||
(cal != std::string::npos && program.length () == cal + 4))
|
||
args.push_back ("calendar");
|
||
}
|
||
else
|
||
args.push_back (argv[i]);
|
||
}
|
||
|
||
// Capture any stdin args.
|
||
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)
|
||
{
|
||
if (arg == "--")
|
||
break;
|
||
|
||
args.push_back (arg);
|
||
}
|
||
}
|
||
|
||
// TODO Scan for rc:<file> overrides --> apply.
|
||
|
||
// Combine command line into one string.
|
||
join (commandLine, " ", args);
|
||
|
||
// TODO Load relevant rc file.
|
||
|
||
// Instantiate built-in command objects.
|
||
commands["execute"] = Command::factory ("execute");
|
||
commands["help"] = Command::factory ("help");
|
||
commands["install"] = Command::factory ("install");
|
||
commands["logo"] = Command::factory ("_logo");
|
||
|
||
// TODO Instantiate extension command objects.
|
||
// TODO Instantiate default command object.
|
||
// TODO Instantiate extension UDA objects.
|
||
// TODO Instantiate extension format objects.
|
||
// TODO Hook: on-launch
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::initialize (int argc, char** argv)
|
||
{
|
||
// Capture the args.
|
||
// ...
|
||
|
||
// Capture any stdin args.
|
||
// ...
|
||
|
||
initialize ();
|
||
|
||
// Hook system init, plus post-start event occurring at the first possible
|
||
// moment after hook initialization.
|
||
hooks.initialize ();
|
||
hooks.trigger ("on-launch");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::initialize ()
|
||
{
|
||
Timer t ("Context::initialize");
|
||
|
||
// Load the configuration file from the home directory. If the file cannot
|
||
// be found, offer to create a sample one.
|
||
loadCorrectConfigFile ();
|
||
loadAliases ();
|
||
|
||
// When redirecting output to a file, do not use color.
|
||
if (!isatty (fileno (stdout)))
|
||
{
|
||
config.set ("detection", "off");
|
||
|
||
if (! config.getBoolean ("_forcecolor"))
|
||
config.set ("color", "off");
|
||
}
|
||
|
||
if (config.getBoolean ("color"))
|
||
initializeColorRules ();
|
||
|
||
Directory location (config.get ("data.location"));
|
||
|
||
// If there is a locale variant (en-US.<variant>), then strip it.
|
||
std::string locale = config.get ("locale");
|
||
std::string::size_type period = locale.find ('.');
|
||
if (period != std::string::npos)
|
||
locale = locale.substr (0, period);
|
||
|
||
// init TDB.
|
||
tdb.clear ();
|
||
std::vector <std::string> all;
|
||
split (all, location, ',');
|
||
foreach (path, all)
|
||
tdb.location (*path);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
int Context::run ()
|
||
{
|
||
int rc;
|
||
std::string output;
|
||
try
|
||
{
|
||
parse (); // Parse command line.
|
||
rc = dispatch2 (output); // Dispatch to new command handlers.
|
||
if (rc)
|
||
rc = dispatch (output); // Dispatch to old command handlers.
|
||
}
|
||
|
||
catch (const std::string& error)
|
||
{
|
||
footnote (error);
|
||
rc = 2;
|
||
}
|
||
|
||
catch (...)
|
||
{
|
||
footnote ("Unknown error.");
|
||
rc = 3;
|
||
}
|
||
|
||
// Dump all debug messages, controlled by rc.debug.
|
||
if (config.getBoolean ("debug"))
|
||
foreach (d, debugMessages)
|
||
if (color ())
|
||
std::cout << colorizeDebug (*d) << "\n";
|
||
else
|
||
std::cout << *d << "\n";
|
||
|
||
// Dump all headers, controlled by 'header' verbosity token.
|
||
if (verbose ("header"))
|
||
foreach (h, headers)
|
||
if (color ())
|
||
std::cout << colorizeHeader (*h) << "\n";
|
||
else
|
||
std::cout << *h << "\n";
|
||
|
||
// Dump the report output.
|
||
std::cout << output;
|
||
|
||
// Dump all footnotes, controlled by 'footnote' verbosity token.
|
||
if (verbose ("footnote"))
|
||
foreach (f, footnotes)
|
||
if (color ())
|
||
std::cout << colorizeFootnote (*f) << "\n";
|
||
else
|
||
std::cout << *f << "\n";
|
||
|
||
hooks.trigger ("on-exit");
|
||
return rc;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
int Context::dispatch2 (std::string &out)
|
||
{
|
||
Timer t ("Context::dispatch2");
|
||
|
||
updateXtermTitle ();
|
||
|
||
std::map <std::string, Command*>::iterator c;
|
||
for (c = commands.begin (); c != commands.end (); ++c)
|
||
{
|
||
if (c->second->implements (commandLine))
|
||
{
|
||
if (! c->second->read_only ())
|
||
tdb.gc ();
|
||
|
||
return c->second->execute (commandLine, out);
|
||
}
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
int Context::dispatch (std::string &out)
|
||
{
|
||
int rc = 0;
|
||
|
||
Timer t ("Context::dispatch");
|
||
|
||
updateXtermTitle ();
|
||
|
||
// TODO Chain-of-command pattern dispatch.
|
||
|
||
if (cmd.command == "projects") { rc = handleProjects (out); }
|
||
else if (cmd.command == "tags") { rc = handleTags (out); }
|
||
else if (cmd.command == "colors") { rc = handleColor (out); }
|
||
else if (cmd.command == "version") { rc = handleVersion (out); }
|
||
else if (cmd.command == "config") { rc = handleConfig (out); }
|
||
else if (cmd.command == "show") { rc = handleShow (out); }
|
||
else if (cmd.command == "stats") { rc = handleReportStats (out); }
|
||
else if (cmd.command == "info") { rc = handleInfo (out); }
|
||
else if (cmd.command == "history.monthly") { rc = handleReportHistoryMonthly (out); }
|
||
else if (cmd.command == "history.annual") { rc = handleReportHistoryAnnual (out); }
|
||
else if (cmd.command == "ghistory.monthly") { rc = handleReportGHistoryMonthly (out); }
|
||
else if (cmd.command == "ghistory.annual") { rc = handleReportGHistoryAnnual (out); }
|
||
else if (cmd.command == "burndown.daily") { rc = handleReportBurndownDaily (out); }
|
||
else if (cmd.command == "burndown.weekly") { rc = handleReportBurndownWeekly (out); }
|
||
else if (cmd.command == "burndown.monthly") { rc = handleReportBurndownMonthly (out); }
|
||
else if (cmd.command == "summary") { rc = handleReportSummary (out); }
|
||
else if (cmd.command == "calendar") { rc = handleReportCalendar (out); }
|
||
else if (cmd.command == "timesheet") { rc = handleReportTimesheet (out); }
|
||
else if (cmd.command == "add") { rc = handleAdd (out); }
|
||
else if (cmd.command == "log") { rc = handleLog (out); }
|
||
else if (cmd.command == "append") { rc = handleAppend (out); }
|
||
else if (cmd.command == "prepend") { rc = handlePrepend (out); }
|
||
else if (cmd.command == "annotate") { rc = handleAnnotate (out); }
|
||
else if (cmd.command == "denotate") { rc = handleDenotate (out); }
|
||
else if (cmd.command == "done") { rc = handleDone (out); }
|
||
else if (cmd.command == "delete") { rc = handleDelete (out); }
|
||
else if (cmd.command == "start") { rc = handleStart (out); }
|
||
else if (cmd.command == "stop") { rc = handleStop (out); }
|
||
else if (cmd.command == "export.csv") { rc = handleExportCSV (out); }
|
||
else if (cmd.command == "export.ical") { rc = handleExportiCal (out); }
|
||
else if (cmd.command == "export.yaml") { rc = handleExportYAML (out); }
|
||
else if (cmd.command == "import") { rc = handleImport (out); }
|
||
else if (cmd.command == "duplicate") { rc = handleDuplicate (out); }
|
||
else if (cmd.command == "edit") { rc = handleEdit (out); }
|
||
else if (cmd.command == "shell") { handleShell ( ); }
|
||
else if (cmd.command == "undo") { handleUndo ( ); }
|
||
else if (cmd.command == "merge") { tdb.gc ();
|
||
handleMerge (out); }
|
||
else if (cmd.command == "push") { handlePush (out); }
|
||
else if (cmd.command == "pull") { handlePull (out); }
|
||
else if (cmd.command == "diagnostics") { handleDiagnostics (out); }
|
||
else if (cmd.command == "count") { rc = handleCount (out); }
|
||
else if (cmd.command == "ids") { rc = handleIds (out); }
|
||
else if (cmd.command == "_projects") { rc = handleCompletionProjects (out); }
|
||
else if (cmd.command == "_tags") { rc = handleCompletionTags (out); }
|
||
else if (cmd.command == "_commands") { rc = handleCompletionCommands (out); }
|
||
else if (cmd.command == "_ids") { rc = handleCompletionIDs (out); }
|
||
else if (cmd.command == "_config") { rc = handleCompletionConfig (out); }
|
||
else if (cmd.command == "_version") { rc = handleCompletionVersion (out); }
|
||
else if (cmd.command == "_urgency") { rc = handleUrgency (out); }
|
||
else if (cmd.command == "_query") { rc = handleQuery (out); }
|
||
else if (cmd.command == "_zshcommands") { rc = handleZshCompletionCommands (out); }
|
||
else if (cmd.command == "_zshids") { rc = handleZshCompletionIDs (out); }
|
||
else if (cmd.command == "" &&
|
||
sequence.size ()) { rc = handleModify (out); }
|
||
|
||
// Commands that display IDs and therefore need TDB::gc first.
|
||
else if (cmd.validCustom (cmd.command)) { if (!inShadow) tdb.gc ();
|
||
rc = handleCustomReport (cmd.command, out); }
|
||
|
||
// If the command is not recognized, display usage.
|
||
else { rc = shortUsage (out); }
|
||
|
||
// Only update the shadow file if such an update was not suppressed (shadow),
|
||
if ((cmd.isWriteCommand () ||
|
||
(cmd.command == "" && sequence.size ())) &&
|
||
!inShadow)
|
||
shadow ();
|
||
|
||
return rc;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
bool Context::color ()
|
||
{
|
||
return config.getBoolean ("color") ||
|
||
config.getBoolean ("_forcecolor");
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// TODO Support verbosity levels.
|
||
bool Context::verbose (const std::string& token)
|
||
{
|
||
if (! verbosity.size ())
|
||
{
|
||
verbosity_legacy = config.getBoolean ("verbose");
|
||
split (verbosity, config.get ("verbose"), ',');
|
||
}
|
||
|
||
if (verbosity_legacy)
|
||
return true;
|
||
|
||
if (std::find (verbosity.begin (), verbosity.end (), token) != verbosity.end ())
|
||
return true;
|
||
|
||
return false;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::shadow ()
|
||
{
|
||
// Determine if shadow file is enabled.
|
||
File shadowFile (config.get ("shadow.file"));
|
||
if (shadowFile.data != "")
|
||
{
|
||
inShadow = true; // Prevents recursion in case shadow command writes.
|
||
|
||
// Check for silly shadow file settings.
|
||
std::string dataLocation = config.get ("data.location");
|
||
if (shadowFile.data == dataLocation + "/pending.data")
|
||
throw std::string ("Configuration variable 'shadow.file' is set to "
|
||
"overwrite your pending tasks. Please change it.");
|
||
|
||
if (shadowFile.data == dataLocation + "/completed.data")
|
||
throw std::string ("Configuration variable 'shadow.file' is set to "
|
||
"overwrite your completed tasks. Please change it.");
|
||
|
||
if (shadowFile.data == dataLocation + "/undo.data")
|
||
throw std::string ("Configuration variable 'shadow.file' is set to "
|
||
"overwrite your undo log. Please change it.");
|
||
|
||
std::string oldDetection = config.get ("detection");
|
||
std::string oldColor = config.get ("color");
|
||
|
||
clear ();
|
||
|
||
// Run report. Use shadow.command, using default.command as a fallback
|
||
// with "list" as a default.
|
||
std::string command = config.get ("shadow.command");
|
||
if (command == "")
|
||
command = config.get ("default.command");
|
||
|
||
split (args, command, ' ');
|
||
|
||
initialize ();
|
||
config.set ("detection", "off");
|
||
config.set ("color", "off");
|
||
|
||
parse ();
|
||
std::string result;
|
||
(void)dispatch (result);
|
||
std::ofstream out (shadowFile.data.c_str ());
|
||
if (out.good ())
|
||
{
|
||
out << result;
|
||
out.close ();
|
||
}
|
||
else
|
||
throw std::string ("Could not write file '") + shadowFile.data + "'";
|
||
|
||
config.set ("detection", oldDetection);
|
||
config.set ("color", oldColor);
|
||
|
||
// Optionally display a notification that the shadow file was updated.
|
||
if (config.getBoolean ("shadow.notify"))
|
||
footnote (std::string ("[Shadow file '") + shadowFile.data + "' updated.]");
|
||
|
||
inShadow = false;
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// Only allows aliases 10 deep.
|
||
std::string Context::canonicalize (const std::string& input) const
|
||
{
|
||
std::string canonical = input;
|
||
|
||
// First try to autocomplete the alias.
|
||
std::vector <std::string> options;
|
||
std::vector <std::string> matches;
|
||
foreach (name, aliases)
|
||
options.push_back (name->first);
|
||
|
||
autoComplete (input, options, matches);
|
||
if (matches.size () == 1)
|
||
{
|
||
canonical = matches[0];
|
||
|
||
// Follow the chain.
|
||
int i = 10; // Safety valve.
|
||
std::map <std::string, std::string>::const_iterator found;
|
||
while ((found = aliases.find (canonical)) != aliases.end () && i-- > 0)
|
||
canonical = found->second;
|
||
|
||
if (i < 1)
|
||
return input;
|
||
}
|
||
|
||
return canonical;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::disallowModification () const
|
||
{
|
||
if (task.size () ||
|
||
subst.mFrom != "" ||
|
||
tagAdditions.size () ||
|
||
tagRemovals.size ())
|
||
throw std::string ("The '")
|
||
+ cmd.command
|
||
+ "' command does not allow further modification of a task.";
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// Takes a vector of args (foo, rc.name:value, bar), extracts any rc.name:value
|
||
// args and sets the name/value in context.config, returning only the plain args
|
||
// (foo, bar) as output.
|
||
void Context::applyOverrides (
|
||
const std::vector <std::string>& input,
|
||
std::vector <std::string>& output)
|
||
{
|
||
bool foundTerminator = false;
|
||
foreach (in, input)
|
||
{
|
||
if (*in == "--")
|
||
{
|
||
foundTerminator = true;
|
||
output.push_back (*in);
|
||
}
|
||
else if (!foundTerminator && in->substr (0, 3) == "rc.")
|
||
{
|
||
std::string name;
|
||
std::string value;
|
||
Nibbler n (*in);
|
||
if (n.getLiteral ("rc.") && // rc.
|
||
n.getUntilOneOf (":=", name) && // xxx
|
||
n.skipN (1)) // :
|
||
{
|
||
n.getUntilEOS (value); // Don't care if it's blank.
|
||
|
||
config.set (name, value);
|
||
var_overrides += " " + *in;
|
||
footnote ("Configuration override " + in->substr (3));
|
||
}
|
||
else
|
||
footnote ("Problem with override: " + *in);
|
||
}
|
||
else
|
||
output.push_back (*in);
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::loadCorrectConfigFile ()
|
||
{
|
||
// Set up default locations.
|
||
struct passwd* pw = getpwuid (getuid ());
|
||
if (!pw)
|
||
throw std::string ("Could not read home directory from the passwd file.");
|
||
|
||
std::string home = pw->pw_dir;
|
||
File rc (home + "/.taskrc");
|
||
Directory data (home + "./task");
|
||
|
||
// Is there an file_override for rc:?
|
||
foreach (arg, args)
|
||
{
|
||
if (*arg == "--")
|
||
break;
|
||
else if (arg->substr (0, 3) == "rc:")
|
||
{
|
||
file_override = *arg;
|
||
rc = File (arg->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 = ".";
|
||
|
||
args.erase (arg);
|
||
header ("Using alternate .taskrc file " + rc.data); // TODO i18n
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Load rc file.
|
||
config.clear (); // Dump current values.
|
||
config.load (rc); // Load new file.
|
||
|
||
if (config.get ("data.location") != "")
|
||
data = Directory (config.get ("data.location"));
|
||
|
||
// Are there any var_overrides for data.location?
|
||
foreach (arg, args)
|
||
{
|
||
if (*arg == "--")
|
||
break;
|
||
else if (arg->substr (0, 16) == "rc.data.location" &&
|
||
((*arg)[16] == ':' || (*arg)[16] == '='))
|
||
{
|
||
data = Directory (arg->substr (17));
|
||
header ("Using alternate data.location " + data.data); // TODO i18n
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Do we need to create a default rc?
|
||
if (! rc.exists ())
|
||
{
|
||
if (!confirm ("A configuration file could not be found in " // TODO i18n
|
||
+ home
|
||
+ "\n\n"
|
||
+ "Would you like a sample "
|
||
+ rc.data
|
||
+ " created, so taskwarrior can proceed?"))
|
||
throw std::string ("Cannot proceed without rc file.");
|
||
|
||
config.createDefaultRC (rc, data);
|
||
}
|
||
|
||
// Create data location, if necessary.
|
||
config.createDefaultData (data);
|
||
|
||
// Apply rc overrides.
|
||
std::vector <std::string> filtered;
|
||
applyOverrides (args, filtered);
|
||
args = filtered;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::loadAliases ()
|
||
{
|
||
aliases.clear ();
|
||
|
||
std::vector <std::string> vars;
|
||
config.all (vars);
|
||
foreach (var, vars)
|
||
{
|
||
if (var->substr (0, 6) == "alias.")
|
||
{
|
||
std::string alias = var->substr (6);
|
||
std::string canonical = config.get (*var);
|
||
|
||
aliases[alias] = canonical;
|
||
}
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::parse ()
|
||
{
|
||
parse (args, cmd, task, sequence, subst, filter);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::parse (
|
||
std::vector <std::string>& parseArgs,
|
||
Cmd& parseCmd,
|
||
Task& parseTask,
|
||
Sequence& parseSequence,
|
||
Subst& parseSubst,
|
||
Filter& parseFilter)
|
||
{
|
||
Timer t ("Context::parse");
|
||
|
||
Att attribute;
|
||
tagAdditions.clear ();
|
||
tagRemovals.clear ();
|
||
std::string descCandidate = "";
|
||
bool terminated = false;
|
||
bool foundSequence = false;
|
||
bool foundSomethingAfterSequence = false;
|
||
bool foundNonSequence = false;
|
||
|
||
foreach (arg, parseArgs)
|
||
{
|
||
if (!terminated)
|
||
{
|
||
// The '--' argument shuts off all parsing - everything is an argument.
|
||
if (*arg == "--")
|
||
{
|
||
debug ("parse terminator '" + *arg + "'");
|
||
terminated = true;
|
||
}
|
||
|
||
// Sequence
|
||
// Note: "add" doesn't require an ID
|
||
else if (parseCmd.command != "add" &&
|
||
! foundSomethingAfterSequence &&
|
||
parseSequence.valid (*arg))
|
||
{
|
||
debug ("parse sequence '" + *arg + "'");
|
||
parseSequence.parse (*arg);
|
||
foundSequence = true;
|
||
}
|
||
|
||
// Tags to include begin with '+'.
|
||
else if (arg->length () > 1 &&
|
||
(*arg)[0] == '+' &&
|
||
noSpaces (*arg))
|
||
{
|
||
debug ("parse tag addition '" + *arg + "'");
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
if (arg->find (',') != std::string::npos)
|
||
throw std::string ("Tags are not permitted to contain commas.");
|
||
|
||
tagAdditions.push_back (arg->substr (1));
|
||
parseTask.addTag (arg->substr (1));
|
||
}
|
||
|
||
// Tags to remove begin with '-'.
|
||
else if (arg->length () > 1 &&
|
||
(*arg)[0] == '-' &&
|
||
noSpaces (*arg))
|
||
{
|
||
debug ("parse tag removal '" + *arg + "'");
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
if (arg->find (',') != std::string::npos)
|
||
throw std::string ("Tags are not permitted to contain commas.");
|
||
|
||
tagRemovals.push_back (arg->substr (1));
|
||
}
|
||
|
||
// Substitution of description and/or annotation text.
|
||
else if (parseSubst.valid (*arg))
|
||
{
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
debug ("parse subst '" + *arg + "'");
|
||
parseSubst.parse (*arg);
|
||
}
|
||
|
||
// Atributes - name[.mod]:[value]
|
||
else if (attribute.valid (*arg))
|
||
{
|
||
debug ("parse attribute '" + *arg + "'");
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
attribute.parse (*arg);
|
||
|
||
// There has to be a better way. And it starts with a fresh coffee.
|
||
std::string name = attribute.name ();
|
||
std::string mod = attribute.mod ();
|
||
std::string value = attribute.value ();
|
||
if (attribute.validNameValue (name, mod, value))
|
||
{
|
||
attribute.name (name);
|
||
attribute.mod (mod);
|
||
attribute.value (value);
|
||
|
||
// Preserve modifier in the key, to allow multiple modifiers on the
|
||
// same attribute. Bug #252.
|
||
if (name != "" && mod != "")
|
||
parseTask[name + "." + mod] = attribute;
|
||
else
|
||
parseTask[name] = attribute;
|
||
|
||
autoFilter (attribute, parseFilter);
|
||
}
|
||
|
||
// *arg has the appearance of an attribute (foo:bar), but isn't
|
||
// recognized, so downgrade it to part of the description.
|
||
else
|
||
{
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
if (descCandidate.length ())
|
||
descCandidate += " ";
|
||
descCandidate += *arg;
|
||
}
|
||
}
|
||
|
||
// It might be a command if one has not already been found.
|
||
else if (parseCmd.command == "" &&
|
||
parseCmd.valid (*arg))
|
||
{
|
||
debug ("parse cmd '" + *arg + "'");
|
||
parseCmd.parse (*arg);
|
||
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
}
|
||
|
||
// Anything else is just considered description.
|
||
else
|
||
{
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
foundNonSequence = true;
|
||
|
||
if (descCandidate.length ())
|
||
descCandidate += " ";
|
||
descCandidate += *arg;
|
||
}
|
||
}
|
||
|
||
// Command is terminated, therefore everything subsequently is a description.
|
||
else
|
||
{
|
||
debug ("parse post-termination description '" + *arg + "'");
|
||
if (foundSequence)
|
||
foundSomethingAfterSequence = true;
|
||
|
||
if (descCandidate.length ())
|
||
descCandidate += " ";
|
||
descCandidate += *arg;
|
||
}
|
||
}
|
||
|
||
if (descCandidate != "" && noVerticalSpace (descCandidate))
|
||
{
|
||
debug ("parse description '" + descCandidate + "'");
|
||
parseTask.set ("description", descCandidate);
|
||
|
||
foundNonSequence = true;
|
||
|
||
// Now convert the description to a filter on each word, if necessary.
|
||
if (parseCmd.isReadOnlyCommand ())
|
||
{
|
||
std::vector <std::string> words;
|
||
split (words, descCandidate, ' ');
|
||
std::vector <std::string>::iterator it;
|
||
for (it = words.begin (); it != words.end (); ++it)
|
||
{
|
||
Att a ("description", "contains", *it);
|
||
autoFilter (a, parseFilter);
|
||
}
|
||
}
|
||
}
|
||
|
||
// At this point, either a sequence or a command should have been found.
|
||
if (parseSequence.size () == 0 && parseCmd.command == "")
|
||
parseCmd.parse (descCandidate);
|
||
|
||
// Read-only command (reports, status, info ...) use filters. Write commands
|
||
// (add, done ...) do not. The filter was constructed iteratively above, but
|
||
// tags were omitted, so they are added now.
|
||
if (parseCmd.isReadOnlyCommand ())
|
||
autoFilter (parseFilter);
|
||
|
||
// If no command was specified, and there were no command line arguments
|
||
// then invoke the default command.
|
||
if (parseCmd.command == "")
|
||
{
|
||
if (parseArgs.size () == 0)
|
||
{
|
||
// Apply overrides, if any.
|
||
std::string defaultCommand = config.get ("default.command");
|
||
if (defaultCommand != "")
|
||
{
|
||
// Add on the overrides.
|
||
defaultCommand += " " + file_override + " " + var_overrides;
|
||
|
||
// Stuff the command line.
|
||
args.clear ();
|
||
split (args, defaultCommand, ' ');
|
||
header ("[task " + trim (defaultCommand) + "]");
|
||
|
||
// Reinitialize the context and recurse.
|
||
file_override = "";
|
||
var_overrides = "";
|
||
footnotes.clear ();
|
||
initialize ();
|
||
parse (args, cmd, task, sequence, subst, filter);
|
||
}
|
||
else
|
||
throw std::string ("You must specify a command, or a task ID to modify.");
|
||
}
|
||
|
||
// If the command "task 123" is entered, but with no modifier arguments,
|
||
// then the actual command is assumed to be "info".
|
||
else if (!foundNonSequence &&
|
||
(parseTask.id != 0 || parseSequence.size () != 0))
|
||
{
|
||
std::cout << "No command - assuming 'info'.\n";
|
||
parseCmd.command = "info";
|
||
}
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::decomposeSortField (
|
||
const std::string& field,
|
||
std::string& key,
|
||
bool& ascending)
|
||
{
|
||
int length = field.length ();
|
||
|
||
if (field[length - 1] == '+')
|
||
{
|
||
ascending = true;
|
||
key = field.substr (0, length - 1);
|
||
}
|
||
else if (field[length - 1] == '-')
|
||
{
|
||
ascending = false;
|
||
key = field.substr (0, length - 1);
|
||
}
|
||
else
|
||
{
|
||
ascending = true;
|
||
key = field;
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// Note: The reason some of these are commented out is because the ::clear
|
||
// method is not really "clear" but "clear_some". Some members do not need to
|
||
// be initialized. That makes this method something of a misnomer. So be it.
|
||
void Context::clear ()
|
||
{
|
||
// Config config;
|
||
filter.clear ();
|
||
sequence.clear ();
|
||
subst.clear ();
|
||
// task.clear ();
|
||
task = Task ();
|
||
tdb.clear (); // TODO Obsolete
|
||
// tdb2.clear ();
|
||
program = "";
|
||
commandLine = "";
|
||
args.clear ();
|
||
file_override = "";
|
||
var_overrides = "";
|
||
cmd.command = ""; // TODO Obsolete
|
||
tagAdditions.clear ();
|
||
tagRemovals.clear ();
|
||
|
||
clearMessages ();
|
||
inShadow = false;
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// Add all the attributes in the task to the filter. All except uuid.
|
||
void Context::autoFilter (Att& a, Filter& f)
|
||
{
|
||
// Words are found in the description using the .has modifier.
|
||
if (a.name () == "description" && a.mod () == "")
|
||
{
|
||
std::vector <std::string> words;
|
||
split (words, a.value (), ' ');
|
||
foreach (word, words)
|
||
{
|
||
f.push_back (Att ("description", "has", *word));
|
||
debug ("auto filter: " + a.name () + ".has:" + *word);
|
||
}
|
||
}
|
||
|
||
// Projects are matched left-most.
|
||
else if (a.name () == "project" && (a.mod () == "" || a.mod () == "not"))
|
||
{
|
||
if (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
|
||
{
|
||
f.push_back (Att ("project", "is", a.value ()));
|
||
debug ("auto filter: " + a.name () + ".is:" + a.value ());
|
||
}
|
||
}
|
||
|
||
// Recurrence periods are matched left-most.
|
||
else if (a.name () == "recur" && a.mod () == "")
|
||
{
|
||
if (a.value () != "")
|
||
{
|
||
f.push_back (Att ("recur", "startswith", a.value ()));
|
||
debug ("auto filter: " + a.name () + ".startswith:" + a.value ());
|
||
}
|
||
else
|
||
{
|
||
f.push_back (Att ("recur", "is", a.value ()));
|
||
debug ("auto filter: " + a.name () + ".is:" + a.value ());
|
||
}
|
||
}
|
||
|
||
// The limit attribute does not participate in filtering, and needs to be
|
||
// specifically handled in handleCustomReport.
|
||
else if (a.name () == "limit")
|
||
{
|
||
}
|
||
|
||
// Every task has a unique uuid by default, and it shouldn't be included,
|
||
// because it is guaranteed to not match.
|
||
else if (a.name () == "uuid")
|
||
{
|
||
}
|
||
|
||
// Note: Tags are handled via the +/-<tag> syntax, but also via attribute
|
||
// modifiers.
|
||
|
||
// Generic attribute matching.
|
||
else
|
||
{
|
||
f.push_back (a);
|
||
debug ("auto filter: " +
|
||
a.name () +
|
||
(a.mod () != "" ?
|
||
("." + a.mod () + ":") :
|
||
":") +
|
||
a.value ());
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// Add all the tags in the task to the filter.
|
||
void Context::autoFilter (Filter& f)
|
||
{
|
||
// This is now a correct implementation of a filter on the presence or absence
|
||
// of a tag. The prior code provided the illusion of leftmost partial tag
|
||
// matches, but was really using the 'contains' and 'nocontains' attribute
|
||
// modifiers. See bug #293.
|
||
|
||
// Include tagAdditions.
|
||
foreach (tag, tagAdditions)
|
||
{
|
||
f.push_back (Att ("tags", "word", *tag));
|
||
debug ("auto filter: +" + *tag);
|
||
}
|
||
|
||
// Include tagRemovals.
|
||
foreach (tag, tagRemovals)
|
||
{
|
||
f.push_back (Att ("tags", "noword", *tag));
|
||
debug ("auto filter: -" + *tag);
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
// This capability is to answer the question of 'what did I just do to generate
|
||
// this output?'.
|
||
void Context::updateXtermTitle ()
|
||
{
|
||
if (config.getBoolean ("xterm.title"))
|
||
{
|
||
std::string title;
|
||
join (title, " ", args);
|
||
std::cout << "]0;task " << title << "" << std::endl;
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::header (const std::string& input)
|
||
{
|
||
headers.push_back (input);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::footnote (const std::string& input)
|
||
{
|
||
footnotes.push_back (input);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::debug (const std::string& input)
|
||
{
|
||
debugMessages.push_back (input);
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|
||
void Context::clearMessages ()
|
||
{
|
||
headers.clear ();
|
||
footnotes.clear ();
|
||
debugMessages.clear ();
|
||
}
|
||
|
||
////////////////////////////////////////////////////////////////////////////////
|