- Using '--' on the command line separates the left hand side, where task is free to interpret arguments in the usual way, and the right hand side, which is then assumed to be part of the task description, and is not interpreted by task.
646 lines
15 KiB
C++
646 lines
15 KiB
C++
////////////////////////////////////////////////////////////////////////////////
|
|
// 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 <stdlib.h>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
|
|
#include "Date.h"
|
|
#include "Duration.h"
|
|
#include "T.h"
|
|
#include "text.h"
|
|
#include "util.h"
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// NOTE: These are static arrays only because there is no initializer list for
|
|
// std::vector.
|
|
static const char* colors[] =
|
|
{
|
|
"bold",
|
|
"underline",
|
|
"bold_underline",
|
|
|
|
"black",
|
|
"red",
|
|
"green",
|
|
"yellow",
|
|
"blue",
|
|
"magenta",
|
|
"cyan",
|
|
"white",
|
|
|
|
"bold_black",
|
|
"bold_red",
|
|
"bold_green",
|
|
"bold_yellow",
|
|
"bold_blue",
|
|
"bold_magenta",
|
|
"bold_cyan",
|
|
"bold_white",
|
|
|
|
"underline_black",
|
|
"underline_red",
|
|
"underline_green",
|
|
"underline_yellow",
|
|
"underline_blue",
|
|
"underline_magenta",
|
|
"underline_cyan",
|
|
"underline_white",
|
|
|
|
"bold_underline_black",
|
|
"bold_underline_red",
|
|
"bold_underline_green",
|
|
"bold_underline_yellow",
|
|
"bold_underline_blue",
|
|
"bold_underline_magenta",
|
|
"bold_underline_cyan",
|
|
"bold_underline_white",
|
|
|
|
"on_black",
|
|
"on_red",
|
|
"on_green",
|
|
"on_yellow",
|
|
"on_blue",
|
|
"on_magenta",
|
|
"on_cyan",
|
|
"on_white",
|
|
|
|
"on_bright_black",
|
|
"on_bright_red",
|
|
"on_bright_green",
|
|
"on_bright_yellow",
|
|
"on_bright_blue",
|
|
"on_bright_magenta",
|
|
"on_bright_cyan",
|
|
"on_bright_white",
|
|
"",
|
|
};
|
|
|
|
static const char* attributes[] =
|
|
{
|
|
"project",
|
|
"priority",
|
|
"fg",
|
|
"bg",
|
|
"due",
|
|
"entry",
|
|
"start",
|
|
"end",
|
|
"recur",
|
|
"until",
|
|
"mask",
|
|
"imask",
|
|
"",
|
|
};
|
|
|
|
// Alphabetical please.
|
|
static const char* commands[] =
|
|
{
|
|
"active",
|
|
"add",
|
|
"append",
|
|
"annotate",
|
|
"calendar",
|
|
"colors",
|
|
"completed",
|
|
"delete",
|
|
"done",
|
|
"duplicate",
|
|
"edit",
|
|
"export",
|
|
"help",
|
|
"history",
|
|
"ghistory",
|
|
"import",
|
|
"info",
|
|
"next",
|
|
"overdue",
|
|
"projects",
|
|
"start",
|
|
"stats",
|
|
"stop",
|
|
"summary",
|
|
"tags",
|
|
"timesheet",
|
|
"undelete",
|
|
"undo",
|
|
"version",
|
|
"",
|
|
};
|
|
|
|
static std::vector <std::string> customReports;
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void guess (const std::string& type, const char** list, std::string& candidate)
|
|
{
|
|
std::vector <std::string> options;
|
|
for (int i = 0; list[i][0]; ++i)
|
|
options.push_back (list[i]);
|
|
|
|
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 (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;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool isCommand (const std::string& candidate)
|
|
{
|
|
std::vector <std::string> options;
|
|
for (int i = 0; commands[i][0]; ++i)
|
|
options.push_back (commands[i]);
|
|
|
|
std::vector <std::string> matches;
|
|
autoComplete (candidate, options, matches);
|
|
if (0 == matches.size ())
|
|
{
|
|
autoComplete (candidate, customReports, matches);
|
|
if (0 == matches.size ())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool validDate (std::string& date, Config& conf)
|
|
{
|
|
Date test (date, conf.get ("dateformat", "m/d/Y"));
|
|
|
|
char epoch[16];
|
|
sprintf (epoch, "%d", (int) test.toEpoch ());
|
|
date = epoch;
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool validPriority (const std::string& input)
|
|
{
|
|
if (input != "H" &&
|
|
input != "M" &&
|
|
input != "L" &&
|
|
input != "")
|
|
throw std::string ("\"") +
|
|
input +
|
|
"\" is not a valid priority. Use H, M, L or leave blank.";
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validAttribute (
|
|
std::string& name,
|
|
std::string& value,
|
|
Config& conf)
|
|
{
|
|
guess ("attribute", attributes, name);
|
|
if (name != "")
|
|
{
|
|
if ((name == "fg" || name == "bg") && value != "")
|
|
guess ("color", colors, value);
|
|
|
|
else if (name == "due" && value != "")
|
|
validDate (value, conf);
|
|
|
|
else if (name == "until" && value != "")
|
|
validDate (value, conf);
|
|
|
|
else if (name == "priority")
|
|
{
|
|
value = upperCase (value);
|
|
return validPriority (value);
|
|
}
|
|
|
|
// Some attributes are intended to be private.
|
|
else if (name == "entry" ||
|
|
name == "start" ||
|
|
name == "end" ||
|
|
name == "mask" ||
|
|
name == "imask")
|
|
throw std::string ("\"") +
|
|
name +
|
|
"\" is not an attribute you may modify directly.";
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validId (const std::string& input)
|
|
{
|
|
if (input.length () == 0)
|
|
return false;
|
|
|
|
for (size_t i = 0; i < input.length (); ++i)
|
|
if (!::isdigit (input[i]))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// 1,2-4,6
|
|
static bool validSequence (
|
|
const std::string& input,
|
|
std::vector <int>& ids)
|
|
{
|
|
std::vector <std::string> ranges;
|
|
split (ranges, input, ',');
|
|
|
|
std::vector <std::string>::iterator it;
|
|
for (it = ranges.begin (); it != ranges.end (); ++it)
|
|
{
|
|
std::vector <std::string> range;
|
|
split (range, *it, '-');
|
|
|
|
switch (range.size ())
|
|
{
|
|
case 1:
|
|
{
|
|
if (! validId (range[0]))
|
|
return false;
|
|
|
|
int id = ::atoi (range[0].c_str ());
|
|
ids.push_back (id);
|
|
}
|
|
break;
|
|
|
|
case 2:
|
|
{
|
|
if (! validId (range[0]) ||
|
|
! validId (range[1]))
|
|
return false;
|
|
|
|
int low = ::atoi (range[0].c_str ());
|
|
int high = ::atoi (range[1].c_str ());
|
|
if (low >= high)
|
|
return false;
|
|
|
|
for (int i = low; i <= high; ++i)
|
|
ids.push_back (i);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ids.size () ? true : false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validTag (const std::string& input)
|
|
{
|
|
if ((input[0] == '-' || input[0] == '+') &&
|
|
input.length () > 1)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validDescription (const std::string& input)
|
|
{
|
|
if (input.length () == 0) return false;
|
|
if (input.find ("\r") != std::string::npos) return false;
|
|
if (input.find ("\f") != std::string::npos) return false;
|
|
if (input.find ("\n") != std::string::npos) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validCommand (std::string& input)
|
|
{
|
|
std::string copy = input;
|
|
guess ("command", commands, copy);
|
|
if (copy == "")
|
|
{
|
|
copy = input;
|
|
guess ("command", customReports, copy);
|
|
if (copy == "")
|
|
return false;
|
|
}
|
|
|
|
input = copy;
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
static bool validSubstitution (
|
|
std::string& input,
|
|
std::string& from,
|
|
std::string& to,
|
|
bool& global)
|
|
{
|
|
size_t first = input.find ('/');
|
|
if (first != std::string::npos)
|
|
{
|
|
size_t second = input.find ('/', first + 1);
|
|
if (second != std::string::npos)
|
|
{
|
|
size_t third = input.find ('/', second + 1);
|
|
if (third != std::string::npos)
|
|
{
|
|
if (first == 0 &&
|
|
first < second &&
|
|
second < third &&
|
|
(third == input.length () - 1 ||
|
|
third == input.length () - 2))
|
|
{
|
|
from = input.substr (first + 1, second - first - 1);
|
|
to = input.substr (second + 1, third - second - 1);
|
|
|
|
global = false;
|
|
if (third == input.length () - 2 &&
|
|
input.find ('g', third + 1) != std::string::npos)
|
|
global = true;
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool validDuration (std::string& input)
|
|
{
|
|
try { Duration (input); }
|
|
catch (...) { return false; }
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
// Token EBNF
|
|
// ------- ----------------------------------
|
|
// command first non-id recognized argument
|
|
//
|
|
// substitution ::= "/" from "/" to "/g"
|
|
// | "/" from "/" to "/" ;
|
|
//
|
|
// tags ::= "+" word
|
|
// | "-" word ;
|
|
//
|
|
// attributes ::= word ":" value
|
|
// | word ":"
|
|
//
|
|
// sequence ::= \d+ "," sequence
|
|
// | \d+ "-" \d+ ;
|
|
//
|
|
// description (whatever isn't one of the above)
|
|
void parse (
|
|
std::vector <std::string>& args,
|
|
std::string& command,
|
|
T& task,
|
|
Config& conf)
|
|
{
|
|
command = "";
|
|
|
|
bool terminated = false;
|
|
bool foundSequence = false;
|
|
bool foundSomethingAfterSequence = false;
|
|
|
|
std::string descCandidate = "";
|
|
for (size_t i = 0; i < args.size (); ++i)
|
|
{
|
|
std::string arg (args[i]);
|
|
|
|
// Ignore any argument that is "rc:...", because that is the command line
|
|
// specified rc file.
|
|
if (arg.substr (0, 3) != "rc:")
|
|
{
|
|
if (!terminated)
|
|
{
|
|
size_t colon; // Pointer to colon in argument.
|
|
std::string from;
|
|
std::string to;
|
|
bool global;
|
|
std::vector <int> sequence;
|
|
|
|
// The '--' argument shuts off all parsing - everything is an argument.
|
|
if (arg == "--")
|
|
terminated = true;
|
|
|
|
// An id is the first argument found that contains all digits.
|
|
else if (lowerCase (command) != "add" && // "add" doesn't require an ID
|
|
validSequence (arg, sequence) &&
|
|
! foundSomethingAfterSequence)
|
|
{
|
|
foundSequence = true;
|
|
foreach (id, sequence)
|
|
task.addId (*id);
|
|
}
|
|
|
|
// Tags begin with + or - and contain arbitrary text.
|
|
else if (validTag (arg))
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
if (arg[0] == '+')
|
|
task.addTag (arg.substr (1, std::string::npos));
|
|
else if (arg[0] == '-')
|
|
task.addRemoveTag (arg.substr (1, std::string::npos));
|
|
}
|
|
|
|
// Attributes contain a constant string followed by a colon, followed by a
|
|
// value.
|
|
else if ((colon = arg.find (":")) != std::string::npos)
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
std::string name = lowerCase (arg.substr (0, colon));
|
|
std::string value = arg.substr (colon + 1, std::string::npos);
|
|
|
|
if (validAttribute (name, value, conf))
|
|
{
|
|
if (name != "recur" || validDuration (value))
|
|
task.setAttribute (name, value);
|
|
}
|
|
|
|
// If it is not a valid attribute, then allow the argument as part of
|
|
// the description.
|
|
else
|
|
{
|
|
if (descCandidate.length ())
|
|
descCandidate += " ";
|
|
descCandidate += arg;
|
|
}
|
|
}
|
|
|
|
// Substitution of description text.
|
|
else if (validSubstitution (arg, from, to, global))
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
task.setSubstitution (from, to, global);
|
|
}
|
|
|
|
// Command.
|
|
else if (command == "")
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
std::string l = lowerCase (arg);
|
|
if (isCommand (l) && validCommand (l))
|
|
command = l;
|
|
else
|
|
{
|
|
if (descCandidate.length ())
|
|
descCandidate += " ";
|
|
descCandidate += arg;
|
|
}
|
|
}
|
|
|
|
// Anything else is just considered description.
|
|
else
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
if (descCandidate.length ())
|
|
descCandidate += " ";
|
|
descCandidate += arg;
|
|
}
|
|
}
|
|
// terminated, therefore everything subsequently is a description.
|
|
else
|
|
{
|
|
if (foundSequence)
|
|
foundSomethingAfterSequence = true;
|
|
|
|
if (descCandidate.length ())
|
|
descCandidate += " ";
|
|
descCandidate += arg;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (validDescription (descCandidate))
|
|
task.setDescription (descCandidate);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void loadCustomReports (Config& conf)
|
|
{
|
|
std::vector <std::string> all;
|
|
conf.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);
|
|
customReports.push_back (report);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
bool isCustomReport (const std::string& report)
|
|
{
|
|
foreach (i, customReports)
|
|
if (*i == report)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
void allCustomReports (std::vector <std::string>& all)
|
|
{
|
|
all = customReports;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|