diff --git a/ChangeLog b/ChangeLog
index ec85a169b..0b41fd8bc 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -22,6 +22,12 @@
task are also modified.
+ Custom reports now support user-specified column labels (thanks to T.
Charles Yun).
+ + Task can now import tasks from a variety of data formats, including task
+ export files from versions 1.4.3 and earlier, versions 1.5.0 and later,
+ todo.sh 2.x, CSV, plain text and task command line. See online docs for
+ full details.
+ + Export was including 'id' in the column header even though it was not
+ included in the data.
------ old releases ------------------------------
diff --git a/html/import.html b/html/import.html
new file mode 100644
index 000000000..35e9ec83f
--- /dev/null
+++ b/html/import.html
@@ -0,0 +1,115 @@
+
+
+
+ Export was including 'id' in the column header even though it was not
+ included in the data.
diff --git a/src/Makefile.am b/src/Makefile.am
index 8203dbb14..c164f7999 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,2 +1,2 @@
bin_PROGRAMS = task
-task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp Timer.cpp color.cpp parse.cpp task.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp Config.h Date.h T.h TDB.h Table.h Grid.h Timer.h color.h task.h
+task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp Timer.cpp color.cpp parse.cpp task.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp import.cpp Config.h Date.h T.h TDB.h Table.h Grid.h Timer.h color.h task.h
diff --git a/src/Makefile.in b/src/Makefile.in
index ae8c519e5..0cdac5fab 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -47,7 +47,7 @@ am_task_OBJECTS = Config.$(OBJEXT) Date.$(OBJEXT) T.$(OBJEXT) \
TDB.$(OBJEXT) Table.$(OBJEXT) Grid.$(OBJEXT) Timer.$(OBJEXT) \
color.$(OBJEXT) parse.$(OBJEXT) task.$(OBJEXT) \
command.$(OBJEXT) report.$(OBJEXT) util.$(OBJEXT) \
- text.$(OBJEXT) rules.$(OBJEXT)
+ text.$(OBJEXT) rules.$(OBJEXT) import.$(OBJEXT)
task_OBJECTS = $(am_task_OBJECTS)
task_LDADD = $(LDADD)
DEFAULT_INCLUDES = -I. -I$(top_builddir)@am__isrc@
@@ -155,7 +155,7 @@ sysconfdir = @sysconfdir@
target_alias = @target_alias@
top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
-task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp Timer.cpp color.cpp parse.cpp task.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp Config.h Date.h T.h TDB.h Table.h Grid.h Timer.h color.h task.h
+task_SOURCES = Config.cpp Date.cpp T.cpp TDB.cpp Table.cpp Grid.cpp Timer.cpp color.cpp parse.cpp task.cpp command.cpp report.cpp util.cpp text.cpp rules.cpp import.cpp Config.h Date.h T.h TDB.h Table.h Grid.h Timer.h color.h task.h
all: all-am
.SUFFIXES:
@@ -231,6 +231,7 @@ distclean-compile:
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/Timer.Po@am__quote@
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/color.Po@am__quote@
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/command.Po@am__quote@
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/import.Po@am__quote@
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/parse.Po@am__quote@
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/report.Po@am__quote@
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/rules.Po@am__quote@
diff --git a/src/import.cpp b/src/import.cpp
new file mode 100644
index 000000000..ce36f8480
--- /dev/null
+++ b/src/import.cpp
@@ -0,0 +1,1189 @@
+////////////////////////////////////////////////////////////////////////////////
+// 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
+#include
+#include
+#include
+#include "Date.h"
+#include "task.h"
+
+////////////////////////////////////////////////////////////////////////////////
+enum fileType
+{
+ not_a_clue,
+ task_1_4_3,
+ task_1_5_0,
+ task_1_6_0,
+ task_cmd_line,
+ todo_sh_2_0,
+ csv,
+ text
+};
+
+static fileType determineFileType (const std::vector & lines)
+{
+ // '7f7a4191-c2f2-487f-8855-7a1eb378c267',' ...
+ // ....:....|....:....|....:....|....:....|
+ // 1 10 20 30 40
+ if (lines.size () > 1 &&
+ lines[1][0] == '\'' &&
+ lines[1][9] == '-' &&
+ lines[1][14] == '-' &&
+ lines[1][19] == '-' &&
+ lines[1][24] == '-' &&
+ lines[1][37] == '\'' &&
+ lines[1][38] == ',' &&
+ lines[1][39] == '\'')
+ {
+ if (lines[0] == "'uuid','status','tags','entry','start','due','recur',"
+ "'end','project','priority','fg','bg','description'")
+ return task_1_6_0;
+
+ if (lines[0] == "'id','uuid','status','tags','entry','start','due','recur',"
+ "'end','project','priority','fg','bg','description'")
+ return task_1_5_0;
+
+ if (lines[0] == "'id','status','tags','entry','start','due','end','project',"
+ "'priority','fg','bg','description'")
+ return task_1_4_3;
+ }
+
+ // A task command line might include a priority or project.
+ for (unsigned int i = 0; i < lines.size (); ++i)
+ {
+ std::vector words;
+ split (words, lines[i], ' ');
+
+ for (unsigned int w = 0; w < words.size (); ++w)
+ if (words[w].substr (0, 9) == "priority:" ||
+ words[w].substr (0, 8) == "priorit:" ||
+ words[w].substr (0, 7) == "priori:" ||
+ words[w].substr (0, 6) == "prior:" ||
+ words[w].substr (0, 5) == "prio:" ||
+ words[w].substr (0, 4) == "pri:" ||
+ words[w].substr (0, 8) == "project:" ||
+ words[w].substr (0, 7) == "projec:" ||
+ words[w].substr (0, 6) == "proje:" ||
+ words[w].substr (0, 5) == "proj:" ||
+ words[w].substr (0, 4) == "pro:")
+ return task_cmd_line;
+ }
+
+ // x 2009-03-25 Walk the dog +project @context
+ // This is a test +project @context
+ for (unsigned int i = 0; i < lines.size (); ++i)
+ {
+ // All done tasks begin with "x YYYY-MM-DD".
+ if (lines[i].length () > 12)
+ {
+ if ( lines[i][0] == 'x' &&
+ lines[i][1] == ' ' &&
+ ::isdigit (lines[i][2]) &&
+ ::isdigit (lines[i][3]) &&
+ ::isdigit (lines[i][4]) &&
+ ::isdigit (lines[i][5]) &&
+ lines[i][6] == '-' &&
+ ::isdigit (lines[i][7]) &&
+ ::isdigit (lines[i][8]) &&
+ lines[i][9] == '-' &&
+ ::isdigit (lines[i][10]) &&
+ ::isdigit (lines[i][11]))
+ return todo_sh_2_0;
+ }
+
+ std::vector words;
+ split (words, lines[i], ' ');
+ for (unsigned int w = 0; w < words.size (); ++w)
+ {
+ // +project
+ if (words[w].length () > 1 &&
+ words[w][0] == '+' &&
+ ::isalnum (words[w][1]))
+ return todo_sh_2_0;
+
+ // @context
+ if (words[w].length () > 1 &&
+ words[w][0] == '@' &&
+ ::isalnum (words[w][1]))
+ return todo_sh_2_0;
+ }
+ }
+
+ // CSV - commas on every non-comment, non-trivial line.
+ bool commas_on_every_line = true;
+ for (unsigned int i = 0; i < lines.size (); ++i)
+ {
+ if (lines[i].length () > 10 &&
+ lines[i].find (",") == std::string::npos)
+ {
+ commas_on_every_line = false;
+ break;
+ }
+ }
+ if (commas_on_every_line)
+ return csv;
+
+ // Looks like 'text' is the default case, if there is any data at all.
+ if (lines.size () > 1)
+ return text;
+
+ return not_a_clue;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static void decorateTask (T& task, Config& conf)
+{
+ char entryTime[16];
+ sprintf (entryTime, "%u", (unsigned int) time (NULL));
+ task.setAttribute ("entry", entryTime);
+
+ // Override with default.project, if not specified.
+ std::string defaultProject = conf.get ("default.project", "");
+ if (task.getAttribute ("project") == "" && defaultProject != "")
+ task.setAttribute ("project", defaultProject);
+
+ // Override with default.priority, if not specified.
+ std::string defaultPriority = conf.get ("default.priority", "");
+ if (task.getAttribute ("priority") == "" &&
+ defaultPriority != "" &&
+ validPriority (defaultPriority))
+ task.setAttribute ("priority", defaultPriority);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importTask_1_4_3 (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ try
+ {
+ // Skip the first line, if it is a columns header line.
+ if (it->substr (0, 5) == "'id',")
+ continue;
+
+ std::vector fields;
+ split (fields, *it, ',');
+
+ // If there is an unexpected number of fields, something is wrong. Perhaps
+ // an embedded comma, in which case there are (at least) two fields that
+ // need to be concatenated.
+ if (fields.size () > 12)
+ {
+ int safety = 10; // Shouldn't be more than 10 commas in a description/project.
+
+ do
+ {
+ std::vector modified;
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ if (fields[f][0] != '\'' &&
+ fields[f][fields[f].length () - 1] == '\'')
+ {
+ modified[modified.size () - 1] += "," + fields[f];
+ }
+
+ else
+ modified.push_back (fields[f]);
+ }
+ fields = modified;
+
+ if (safety-- <= 0)
+ throw "unrecoverable";
+ }
+ while (fields.size () > 12);
+ }
+
+ if (fields.size () < 12)
+ throw "unrecoverable";
+
+ // Build up this task ready for insertion.
+ T task;
+
+ // Handle the 12 fields.
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ switch (f)
+ {
+ case 0: // 'uuid'
+ task.setUUID (fields[f].substr (1, 36));
+ break;
+
+ case 1: // 'status'
+ if (fields[f] == "'pending'") task.setStatus (T::pending);
+ else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
+ else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
+ else if (fields[f] == "'completed'") task.setStatus (T::completed);
+ break;
+
+ case 2: // 'tags'
+ if (fields[f].length () > 2)
+ {
+ std::string tokens = fields[f].substr (1, fields[f].length () - 2);
+ std::vector tags;
+ split (tags, tokens, ' ');
+ for (unsigned int i = 0; i < tags.size (); ++i)
+ task.addTag (tags[i]);
+ }
+ break;
+
+ case 3: // entry
+ task.setAttribute ("entry", fields[f]);
+ break;
+
+ case 4: // start
+ if (fields[f].length ())
+ task.setAttribute ("start", fields[f]);
+ break;
+
+ case 5: // due
+ if (fields[f].length ())
+ task.setAttribute ("due", fields[f]);
+ break;
+
+ case 6: // end
+ if (fields[f].length ())
+ task.setAttribute ("end", fields[f]);
+ break;
+
+ case 7: // 'project'
+ if (fields[f].length () > 2)
+ task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 8: // 'priority'
+ if (fields[f].length () > 2)
+ task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 9: // 'fg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 10: // 'bg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 11: // 'description'
+ if (fields[f].length () > 2)
+ task.setDescription (fields[f].substr (1, fields[f].length () - 2));
+ break;
+ }
+ }
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (*it);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size () - 1)
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importTask_1_5_0 (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ try
+ {
+ // Skip the first line, if it is a columns header line.
+ if (it->substr (0, 5) == "'id',")
+ continue;
+
+ std::vector fields;
+ split (fields, *it, ',');
+
+ // If there is an unexpected number of fields, something is wrong. Perhaps
+ // an embedded comma, in which case there are (at least) two fields that
+ // need to be concatenated.
+ if (fields.size () > 13)
+ {
+ int safety = 10; // Shouldn't be more than 10 commas in a description/project.
+
+ do
+ {
+ std::vector modified;
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ if (fields[f][0] != '\'' &&
+ fields[f][fields[f].length () - 1] == '\'')
+ {
+ modified[modified.size () - 1] += "," + fields[f];
+ }
+
+ else
+ modified.push_back (fields[f]);
+ }
+ fields = modified;
+
+ if (safety-- <= 0)
+ throw "unrecoverable";
+ }
+ while (fields.size () > 13);
+ }
+
+ if (fields.size () < 13)
+ throw "unrecoverable";
+
+ // Build up this task ready for insertion.
+ T task;
+
+ // Handle the 13 fields.
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ switch (f)
+ {
+ case 0: // 'uuid'
+ task.setUUID (fields[f].substr (1, 36));
+ break;
+
+ case 1: // 'status'
+ if (fields[f] == "'pending'") task.setStatus (T::pending);
+ else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
+ else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
+ else if (fields[f] == "'completed'") task.setStatus (T::completed);
+ break;
+
+ case 2: // 'tags'
+ if (fields[f].length () > 2)
+ {
+ std::string tokens = fields[f].substr (1, fields[f].length () - 2);
+ std::vector tags;
+ split (tags, tokens, ' ');
+ for (unsigned int i = 0; i < tags.size (); ++i)
+ task.addTag (tags[i]);
+ }
+ break;
+
+ case 3: // entry
+ task.setAttribute ("entry", fields[f]);
+ break;
+
+ case 4: // start
+ if (fields[f].length ())
+ task.setAttribute ("start", fields[f]);
+ break;
+
+ case 5: // due
+ if (fields[f].length ())
+ task.setAttribute ("due", fields[f]);
+ break;
+
+ case 6: // recur
+ if (fields[f].length ())
+ task.setAttribute ("recur", fields[f]);
+ break;
+
+ case 7: // end
+ if (fields[f].length ())
+ task.setAttribute ("end", fields[f]);
+ break;
+
+ case 8: // 'project'
+ if (fields[f].length () > 2)
+ task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 9: // 'priority'
+ if (fields[f].length () > 2)
+ task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 10: // 'fg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 11: // 'bg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 12: // 'description'
+ if (fields[f].length () > 2)
+ task.setDescription (fields[f].substr (1, fields[f].length () - 2));
+ break;
+ }
+ }
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (*it);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size () - 1)
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importTask_1_6_0 (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ try
+ {
+ // Skip the first line, if it is a columns header line.
+ if (it->substr (0, 7) == "'uuid',")
+ continue;
+
+ std::vector fields;
+ split (fields, *it, ',');
+
+ // If there is an unexpected number of fields, something is wrong. Perhaps
+ // an embedded comma, in which case there are (at least) two fields that
+ // need to be concatenated.
+ if (fields.size () > 13)
+ {
+ int safety = 10; // Shouldn't be more than 10 commas in a description/project.
+
+ do
+ {
+ std::vector modified;
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ if (fields[f][0] != '\'' &&
+ fields[f][fields[f].length () - 1] == '\'')
+ {
+ modified[modified.size () - 1] += "," + fields[f];
+ }
+
+ else
+ modified.push_back (fields[f]);
+ }
+ fields = modified;
+
+ if (safety-- <= 0)
+ throw "unrecoverable";
+ }
+ while (fields.size () > 13);
+ }
+
+ if (fields.size () < 13)
+ throw "unrecoverable";
+
+ // Build up this task ready for insertion.
+ T task;
+
+ // Handle the 13 fields.
+ for (unsigned int f = 0; f < fields.size (); ++f)
+ {
+ switch (f)
+ {
+ case 0: // 'uuid'
+ task.setUUID (fields[f].substr (1, 36));
+ break;
+
+ case 1: // 'status'
+ if (fields[f] == "'pending'") task.setStatus (T::pending);
+ else if (fields[f] == "'recurring'") task.setStatus (T::recurring);
+ else if (fields[f] == "'deleted'") task.setStatus (T::deleted);
+ else if (fields[f] == "'completed'") task.setStatus (T::completed);
+ break;
+
+ case 2: // 'tags'
+ if (fields[f].length () > 2)
+ {
+ std::string tokens = fields[f].substr (1, fields[f].length () - 2);
+ std::vector tags;
+ split (tags, tokens, ' ');
+ for (unsigned int i = 0; i < tags.size (); ++i)
+ task.addTag (tags[i]);
+ }
+ break;
+
+ case 3: // entry
+ task.setAttribute ("entry", fields[f]);
+ break;
+
+ case 4: // start
+ if (fields[f].length ())
+ task.setAttribute ("start", fields[f]);
+ break;
+
+ case 5: // due
+ if (fields[f].length ())
+ task.setAttribute ("due", fields[f]);
+ break;
+
+ case 6: // recur
+ if (fields[f].length ())
+ task.setAttribute ("recur", fields[f]);
+ break;
+
+ case 7: // end
+ if (fields[f].length ())
+ task.setAttribute ("end", fields[f]);
+ break;
+
+ case 8: // 'project'
+ if (fields[f].length () > 2)
+ task.setAttribute ("project", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 9: // 'priority'
+ if (fields[f].length () > 2)
+ task.setAttribute ("priority", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 10: // 'fg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("fg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 11: // 'bg'
+ if (fields[f].length () > 2)
+ task.setAttribute ("bg", fields[f].substr (1, fields[f].length () - 2));
+ break;
+
+ case 12: // 'description'
+ if (fields[f].length () > 2)
+ task.setDescription (fields[f].substr (1, fields[f].length () - 2));
+ break;
+ }
+ }
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (*it);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size () - 1)
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importTaskCmdLine (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ std::string line = *it;
+
+ try
+ {
+ std::vector args;
+ split (args, std::string ("add ") + line, ' ');
+
+ T task;
+ std::string command;
+ parse (args, command, task, conf);
+ handleAdd (tdb, task, conf);
+ }
+
+ catch (...)
+ {
+ failed.push_back (line);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size ())
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importTodoSh_2_0 (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ try
+ {
+ std::vector args;
+ args.push_back ("add");
+
+ bool isPending = true;
+ Date endDate;
+
+ std::vector words;
+ split (words, *it, ' ');
+
+ for (unsigned int w = 0; w < words.size (); ++w)
+ {
+ if (words[w].length () > 1 &&
+ words[w][0] == '+')
+ {
+ args.push_back (std::string ("project:") +
+ words[w].substr (1, std::string::npos));
+ }
+
+ // Convert "+aaa" to "project:aaa".
+ // Convert "@aaa" to "+aaa".
+ else if (words[w].length () > 1 &&
+ words[w][0] == '@')
+ {
+ args.push_back (std::string ("+") +
+ words[w].substr (1, std::string::npos));
+ }
+
+ // Convert "(A)" to "priority:H".
+ // Convert "(B)" to "priority:M".
+ // Convert "(?)" to "priority:L".
+ else if (words[w].length () == 3 &&
+ words[w][0] == '(' &&
+ words[w][2] == ')')
+ {
+ if (words[w][1] == 'A') args.push_back ("priority:H");
+ else if (words[w][1] == 'B') args.push_back ("priority:M");
+ else args.push_back ("priority:L");
+ }
+
+ // Set status, if completed.
+ else if (w == 0 &&
+ words[w] == "x")
+ {
+ isPending = false;
+ }
+
+ // Set status, and add an end date, if completed.
+ else if (! isPending &&
+ w == 1 &&
+ words[w].length () == 10 &&
+ words[w][4] == '-' &&
+ words[w][7] == '-')
+ {
+ endDate = Date (words[w], "Y-M-D");
+ }
+
+ // Just an ordinary word.
+ else
+ {
+ args.push_back (words[w]);
+ }
+ }
+
+ T task;
+ std::string command;
+ parse (args, command, task, conf);
+ decorateTask (task, conf);
+
+ if (isPending)
+ {
+ task.setStatus (T::pending);
+ }
+ else
+ {
+ task.setStatus (T::completed);
+
+ char end[16];
+ sprintf (end, "%u", (unsigned int) endDate.toEpoch ());
+ task.setAttribute ("end", end);
+ }
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (*it);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size ())
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importText (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+ int count = 0;
+
+ std::vector ::const_iterator it;
+ for (it = lines.begin (); it != lines.end (); ++it)
+ {
+ std::string line = *it;
+
+ // Strip comments
+ std::string::size_type pound = line.find ("#");
+ if (pound != std::string::npos)
+ line = line.substr (0, pound);
+
+ // Skip blank lines
+ if (line.length () > 0)
+ {
+ try
+ {
+ ++count;
+ std::vector args;
+ split (args, std::string ("add ") + line, ' ');
+
+ T task;
+ std::string command;
+ parse (args, command, task, conf);
+ decorateTask (task, conf);
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (line);
+ }
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << count
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+static std::string importCSV (
+ TDB& tdb,
+ Config& conf,
+ const std::vector & lines)
+{
+ std::vector failed;
+
+ // Set up mappings. Assume no fields match.
+ std::map mapping;
+ mapping ["id"] = -1;
+ mapping ["uuid"] = -1;
+ mapping ["status"] = -1;
+ mapping ["tags"] = -1;
+ mapping ["entry"] = -1;
+ mapping ["start"] = -1;
+ mapping ["due"] = -1;
+ mapping ["recur"] = -1;
+ mapping ["end"] = -1;
+ mapping ["project"] = -1;
+ mapping ["priority"] = -1;
+ mapping ["fg"] = -1;
+ mapping ["bg"] = -1;
+ mapping ["description"] = -1;
+
+ std::vector headings;
+ split (headings, lines[0], ',');
+
+ for (unsigned int h = 0; h < headings.size (); ++h)
+ {
+ std::string name = lowerCase (trim (unquoteText (trim (headings[h]))));
+
+ // If there is a mapping for the field, use the value.
+ if (name == "id" ||
+ name == "#" ||
+ name == "sequence" ||
+ name.find ("num") != std::string::npos)
+ {
+ mapping["id"] = (int)h;
+ }
+
+ else if (name == "uuid" ||
+ name == "guid" ||
+ name.find ("unique") != std::string::npos)
+ {
+ mapping["uuid"] = (int)h;
+ }
+
+ else if (name == "status" ||
+ name == "condition" ||
+ name == "state")
+ {
+ mapping["status"] = (int)h;
+ }
+
+ else if (name == "tags" ||
+ name.find ("categor") != std::string::npos ||
+ name.find ("tag") != std::string::npos)
+ {
+ mapping["tags"] = (int)h;
+ }
+
+ else if (name == "entry" ||
+ name.find ("added") != std::string::npos ||
+ name.find ("created") != std::string::npos ||
+ name.find ("entered") != std::string::npos)
+ {
+ mapping["entry"] = (int)h;
+ }
+
+ else if (name == "start" ||
+ name.find ("began") != std::string::npos ||
+ name.find ("begun") != std::string::npos ||
+ name.find ("started") != std::string::npos ||
+ name == "")
+ {
+ mapping["start"] = (int)h;
+ }
+
+ else if (name == "due" ||
+ name.find ("expected") != std::string::npos)
+ {
+ mapping["due"] = (int)h;
+ }
+
+ else if (name == "recur" ||
+ name == "frequency")
+ {
+ mapping["recur"] = (int)h;
+ }
+
+ else if (name == "end" ||
+ name == "done" ||
+ name.find ("complete") != std::string::npos)
+ {
+ mapping["end"] = (int)h;
+ }
+
+ else if (name == "project" ||
+ name.find ("proj") != std::string::npos)
+ {
+ mapping["project"] = (int)h;
+ }
+
+ else if (name == "priority" ||
+ name == "pri" ||
+ name.find ("importan") != std::string::npos)
+ {
+ mapping["priority"] = (int)h;
+ }
+
+ else if (name.find ("fg") != std::string::npos ||
+ name.find ("foreground") != std::string::npos ||
+ name.find ("color") != std::string::npos)
+ {
+ mapping["fg"] = (int)h;
+ }
+
+ else if (name == "bg" ||
+ name.find ("background") != std::string::npos)
+ {
+ mapping["bg"] = (int)h;
+ }
+
+ else if (name.find ("desc") != std::string::npos ||
+ name.find ("detail") != std::string::npos ||
+ name.find ("what") != std::string::npos)
+ {
+ mapping["description"] = (int)h;
+ }
+ }
+
+ // TODO Dump mappings and ask for confirmation?
+
+ std::vector ::const_iterator it = lines.begin ();
+ for (++it; it != lines.end (); ++it)
+ {
+ try
+ {
+ std::vector fields;
+ split (fields, *it, ',');
+
+ T task;
+
+ int f;
+ if ((f = mapping["uuid"]) != -1)
+ task.setUUID (lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["status"]) != -1)
+ {
+ std::string value = lowerCase (unquoteText (trim (fields[f])));
+
+ if (value == "recurring") task.setStatus (T::recurring);
+ else if (value == "deleted") task.setStatus (T::deleted);
+ else if (value == "completed") task.setStatus (T::completed);
+ else task.setStatus (T::pending);
+ }
+
+ if ((f = mapping["tags"]) != -1)
+ {
+ std::string value = unquoteText (trim (fields[f]));
+ std::vector tags;
+ split (tags, value, ' ');
+ for (unsigned int i = 0; i < tags.size (); ++i)
+ task.addTag (tags[i]);
+ }
+
+ if ((f = mapping["entry"]) != -1)
+ task.setAttribute ("entry", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["start"]) != -1)
+ task.setAttribute ("start", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["due"]) != -1)
+ task.setAttribute ("due", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["recur"]) != -1)
+ task.setAttribute ("recur", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["end"]) != -1)
+ task.setAttribute ("end", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["project"]) != -1)
+ task.setAttribute ("project", unquoteText (trim (fields[f])));
+
+ if ((f = mapping["priority"]) != -1)
+ {
+ std::string value = upperCase (unquoteText (trim (fields[f])));
+ if (value == "H" || value == "M" || value == "L")
+ task.setAttribute ("priority", value);
+ }
+
+ if ((f = mapping["fg"]) != -1)
+ task.setAttribute ("fg", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["bg"]) != -1)
+ task.setAttribute ("bg", lowerCase (unquoteText (trim (fields[f]))));
+
+ if ((f = mapping["description"]) != -1)
+ task.setDescription (unquoteText (trim (fields[f])));
+
+ if (! tdb.addT (task))
+ failed.push_back (*it);
+ }
+
+ catch (...)
+ {
+ failed.push_back (*it);
+ }
+ }
+
+ std::stringstream out;
+ out << "Imported "
+ << (lines.size () - failed.size () - 1)
+ << " tasks successfully, with "
+ << failed.size ()
+ << " errors."
+ << std::endl;
+
+ if (failed.size ())
+ {
+ std::string bad;
+ join (bad, "\n", failed);
+ return out.str () + "\nCould not import:\n\n" + bad;
+ }
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+std::string handleImport (TDB& tdb, T& task, Config& conf)
+{
+ std::stringstream out;
+
+ // Use the description as a file name.
+ std::string file = trim (task.getDescription ());
+ if (file.length () > 0)
+ {
+ // Load the file.
+ std::vector all;
+ slurp (file, all, true);
+
+ std::vector lines;
+ std::vector ::iterator it;
+ for (it = all.begin (); it != all.end (); ++it)
+ {
+ std::string line = *it;
+
+ // Strip comments
+ std::string::size_type pound = line.find ("#");
+ if (pound != std::string::npos)
+ line = line.substr (0, pound);
+
+ trim (line);
+
+ // Skip blank lines
+ if (line.length () > 0)
+ lines.push_back (line);
+ }
+
+ // Take a guess at the file type.
+ fileType type = determineFileType (lines);
+ std::string identifier;
+ switch (type)
+ {
+ case task_1_4_3: identifier = "This looks like an older task export file."; break;
+ case task_1_5_0: identifier = "This looks like a recent task export file."; break;
+ case task_1_6_0: identifier = "This looks like a current task export file."; break;
+ case task_cmd_line: identifier = "This looks like task command line arguments."; break;
+ case todo_sh_2_0: identifier = "This looks like a todo.sh 2.x file."; break;
+ case csv: identifier = "This looks like a CSV file, but not a task export file."; break;
+ case text: identifier = "This looks like a text file with one tasks per line."; break;
+ case not_a_clue:
+ throw std::string ("Task cannot determine which type of file this is, "
+ "and cannot proceed.");
+ }
+
+ // For tty users, confirm the import, as it is destructive.
+ if (isatty (fileno (stdout)))
+ if (! confirm (identifier + " Okay to proceed?"))
+ throw std::string ("Task will not import any data.");
+
+ // Determine which type it might be, then attempt an import.
+ switch (type)
+ {
+ case task_1_4_3: out << importTask_1_4_3 (tdb, conf, lines); break;
+ case task_1_5_0: out << importTask_1_5_0 (tdb, conf, lines); break;
+ case task_1_6_0: out << importTask_1_6_0 (tdb, conf, lines); break;
+ case task_cmd_line: out << importTaskCmdLine (tdb, conf, lines); break;
+ case todo_sh_2_0: out << importTodoSh_2_0 (tdb, conf, lines); break;
+ case csv: out << importCSV (tdb, conf, lines); break;
+ case text: out << importText (tdb, conf, lines); break;
+ case not_a_clue: /* to stop the compiler from complaining. */ break;
+ }
+ }
+ else
+ throw std::string ("You must specify a file to import.");
+
+ return out.str ();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
diff --git a/src/parse.cpp b/src/parse.cpp
index 4dacbc0d2..e6923c589 100644
--- a/src/parse.cpp
+++ b/src/parse.cpp
@@ -131,6 +131,7 @@ static const char* commands[] =
"help",
"history",
"ghistory",
+ "import",
"info",
"next",
"overdue",
@@ -234,7 +235,7 @@ bool validDate (std::string& date, Config& conf)
{
Date test (date, conf.get ("dateformat", "m/d/Y"));
- char epoch[12];
+ char epoch[16];
sprintf (epoch, "%d", (int) test.toEpoch ());
date = epoch;
diff --git a/src/task.cpp b/src/task.cpp
index af767d1bc..2bfa1214e 100644
--- a/src/task.cpp
+++ b/src/task.cpp
@@ -172,6 +172,10 @@ static std::string shortUsage (Config& conf)
table.addCell (row, 1, "task stats");
table.addCell (row, 2, "Shows task database statistics");
+ row = table.addRow ();
+ table.addCell (row, 1, "task import");
+ table.addCell (row, 2, "Imports tasks from a variety of formats");
+
row = table.addRow ();
table.addCell (row, 1, "task export");
table.addCell (row, 2, "Exports all tasks as a CSV file");
@@ -844,6 +848,7 @@ std::string runTaskCommand (
else if (command == "start") { cmdMod = true; out = handleStart (tdb, task, conf); }
else if (command == "stop") { cmdMod = true; out = handleStop (tdb, task, conf); }
else if (command == "undo") { cmdMod = true; out = handleUndo (tdb, task, conf); }
+ else if (command == "import") { cmdMod = true; out = handleImport (tdb, task, conf); }
// Command that display IDs and therefore need TDB::gc first.
diff --git a/src/task.h b/src/task.h
index a2c2c75b7..ab4faa514 100644
--- a/src/task.h
+++ b/src/task.h
@@ -108,12 +108,12 @@ std::string handleCustomReport (TDB&, T&, Config&, const std::string&);
void validReportColumns (const std::vector &);
void validSortColumns (const std::vector &, const std::vector &);
-// util.cpp
-bool confirm (const std::string&);
+// text.cpp
void wrapText (std::vector &, const std::string&, const int);
std::string trimLeft (const std::string& in, const std::string& t = " ");
std::string trimRight (const std::string& in, const std::string& t = " ");
std::string trim (const std::string& in, const std::string& t = " ");
+std::string unquoteText (const std::string&);
void extractLine (std::string&, std::string&, int);
void split (std::vector&, const std::string&, const char);
void split (std::vector&, const std::string&, const std::string&);
@@ -121,12 +121,15 @@ void join (std::string&, const std::string&, const std::vector&);
std::string commify (const std::string&);
std::string lowerCase (const std::string&);
std::string upperCase (const std::string&);
+const char* optionalBlankLine (Config&);
+
+// util.cpp
+bool confirm (const std::string&);
void delay (float);
-int autoComplete (const std::string&, const std::vector&, std::vector&);
void formatTimeDeltaDays (std::string&, time_t);
std::string formatSeconds (time_t);
+int autoComplete (const std::string&, const std::vector&, std::vector&);
const std::string uuid ();
-const char* optionalBlankLine (Config&);
int convertDuration (const std::string&);
std::string expandPath (const std::string&);
@@ -139,8 +142,13 @@ std::string expandPath (const std::string&);
int flock (int, int);
#endif
+bool slurp (const std::string&, std::vector &, bool trimLines = false);
+
// rules.cpp
void initializeColorRules (Config&);
void autoColorize (T&, Text::color&, Text::color&, Config&);
+// import.cpp
+std::string handleImport (TDB&, T&, Config&);
+
////////////////////////////////////////////////////////////////////////////////
diff --git a/src/tests/import.143.t b/src/tests/import.143.t
new file mode 100755
index 000000000..632d5814c
--- /dev/null
+++ b/src/tests/import.143.t
@@ -0,0 +1,70 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 8;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "'id','status','tags','entry','start','due','end','project','priority','fg','bg','description'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,'A','M',,,'foo bar'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,'A','M',,,'foo, bar'\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+A.+M.+foo bar/, 't1');
+like ($output, qr/2.+A.+M.+foo, bar/, 't2');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.150.t b/src/tests/import.150.t
new file mode 100755
index 000000000..4344ed7f1
--- /dev/null
+++ b/src/tests/import.150.t
@@ -0,0 +1,70 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 8;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "'id','uuid','status','tags','entry','start','due','recur','end','project','priority','fg','bg','description'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,,'A','M',,,'foo bar'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,,'A','M',,,'foo, bar'\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+A.+M.+foo bar/, 't1');
+like ($output, qr/2.+A.+M.+foo, bar/, 't2');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.160.t b/src/tests/import.160.t
new file mode 100755
index 000000000..33c34e2af
--- /dev/null
+++ b/src/tests/import.160.t
@@ -0,0 +1,70 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 8;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "'uuid','status','tags','entry','start','due','recur','end','project','priority','fg','bg','description'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,,'A','M',,,'foo bar'\n",
+ "'7f7a4191-c2f2-487f-8855-7a1eb378c267','pending','',1238037947,,,,,'A','M',,,'foo, bar'\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+A.+M.+foo bar/, 't1');
+like ($output, qr/2.+A.+M.+foo, bar/, 't2');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.cmd.t b/src/tests/import.cmd.t
new file mode 100755
index 000000000..220241eeb
--- /dev/null
+++ b/src/tests/import.cmd.t
@@ -0,0 +1,69 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 8;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "This is a test priority:H project:A\n",
+ "Another task\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+A.+H.+This is a test/, 't1');
+like ($output, qr/2.+Another task/, 't2');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.csv.t b/src/tests/import.csv.t
new file mode 100755
index 000000000..2d1eb718e
--- /dev/null
+++ b/src/tests/import.csv.t
@@ -0,0 +1,70 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 8;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "'id','priority','description'\n",
+ "1,H,'this is a test'\n",
+ "2,,'another task'\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 2 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+H.+this is a test/, 't1');
+like ($output, qr/2.+another task/, 't2');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.todo.t b/src/tests/import.todo.t
new file mode 100755
index 000000000..dac3a6cbe
--- /dev/null
+++ b/src/tests/import.todo.t
@@ -0,0 +1,76 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 10;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "x 2009-03-25 Walk the dog +project \@context\n",
+ "This is a test +project \@context\n",
+ "(A) A prioritized task\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 3 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+project.+This is a test/, 't1');
+like ($output, qr/2.+H.+A prioritized task/, 't2');
+
+$output = qx{../task rc:import.rc completed};
+like ($output, qr/3\/25\/2009.+Walk the dog/, 't3');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'completed.data';
+ok (!-r 'completed.data', 'Removed completed.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/import.txt.t b/src/tests/import.txt.t
new file mode 100755
index 000000000..65f3e0b69
--- /dev/null
+++ b/src/tests/import.txt.t
@@ -0,0 +1,71 @@
+#! /usr/bin/perl
+################################################################################
+## 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
+##
+################################################################################
+
+use strict;
+use warnings;
+use Test::More tests => 9;
+
+# Create the rc file.
+if (open my $fh, '>', 'import.rc')
+{
+ print $fh "data.location=.\n";
+ close $fh;
+ ok (-r 'import.rc', 'Created import.rc');
+}
+
+# Create import file.
+if (open my $fh, '>', 'import.txt')
+{
+ print $fh "Get milk, bread\n",
+ "Order cake\n",
+ "Clean house\n",
+ "\n";
+ close $fh;
+ ok (-r 'import.txt', 'Created sample import data');
+}
+
+my $output = qx{../task rc:import.rc import import.txt};
+is ($output, "Imported 3 tasks successfully, with 0 errors.\n", 'no errors');
+
+$output = qx{../task rc:import.rc list};
+like ($output, qr/1.+Get milk, bread/, 't1');
+like ($output, qr/2.+Order cake/, 't2');
+like ($output, qr/3.+Clean house/, 't3');
+
+# Cleanup.
+unlink 'import.txt';
+ok (!-r 'import.txt', 'Removed import.txt');
+
+unlink 'pending.data';
+ok (!-r 'pending.data', 'Removed pending.data');
+
+unlink 'import.rc';
+ok (!-r 'import.rc', 'Removed import.rc');
+
+exit 0;
+
diff --git a/src/tests/text.t.cpp b/src/tests/text.t.cpp
index 65c3d5e11..d959ea1d4 100644
--- a/src/tests/text.t.cpp
+++ b/src/tests/text.t.cpp
@@ -31,7 +31,7 @@
////////////////////////////////////////////////////////////////////////////////
int main (int argc, char** argv)
{
- UnitTest t (78);
+ UnitTest t (94);
// void wrapText (std::vector & lines, const std::string& text, const int width)
std::string text = "This is a test of the line wrapping code.";
@@ -183,6 +183,24 @@ int main (int argc, char** argv)
t.is (trim (" \t xxx \t "), "\t xxx \t", "trim ' \\t xxx \\t ' -> '\\t xxx \\t'");
t.is (trim (" \t xxx \t ", " \t"), "xxx", "trim ' \\t xxx \\t ' -> 'xxx'");
+ // std::string unquoteText (const std::string& text)
+ t.is (unquoteText (""), "", "unquoteText '' -> ''");
+ t.is (unquoteText ("x"), "x", "unquoteText 'x' -> 'x'");
+ t.is (unquoteText ("'x"), "'x", "unquoteText ''x' -> ''x'");
+ t.is (unquoteText ("x'"), "x'", "unquoteText 'x'' -> 'x''");
+ t.is (unquoteText ("\"x"), "\"x", "unquoteText '\"x' -> '\"x'");
+ t.is (unquoteText ("x\""), "x\"", "unquoteText 'x\"' -> 'x\"'");
+ t.is (unquoteText ("''"), "", "unquoteText '''' -> ''");
+ t.is (unquoteText ("'''"), "'", "unquoteText ''''' -> '''");
+ t.is (unquoteText ("\"\""), "", "unquoteText '\"\"' -> ''");
+ t.is (unquoteText ("\"\"\""), "\"", "unquoteText '\"\"\"' -> '\"'");
+ t.is (unquoteText ("''''"), "''", "unquoteText '''''' -> ''''");
+ t.is (unquoteText ("\"\"\"\""), "\"\"", "unquoteText '\"\"\"\"' -> '\"\"'");
+ t.is (unquoteText ("'\"\"'"), "\"\"", "unquoteText '''\"\"' -> '\"\"'");
+ t.is (unquoteText ("\"''\""), "''", "unquoteText '\"''\"' -> ''''");
+ t.is (unquoteText ("'x'"), "x", "unquoteText ''x'' -> 'x'");
+ t.is (unquoteText ("\"x\""), "x", "unquoteText '\"x\"' -> 'x'");
+
// std::string commify (const std::string& data)
t.is (commify (""), "", "commify '' -> ''");
t.is (commify ("1"), "1", "commify '1' -> '1'");
diff --git a/src/text.cpp b/src/text.cpp
index fced0b0aa..69689a5ac 100644
--- a/src/text.cpp
+++ b/src/text.cpp
@@ -127,12 +127,19 @@ std::string trim (const std::string& in, const std::string& t /*= " "*/)
////////////////////////////////////////////////////////////////////////////////
// Remove enclosing balanced quotes. Assumes trimmed text.
-void unquoteText (std::string& text)
+std::string unquoteText (const std::string& input)
{
- char quote = text[0];
- if (quote == '\'' || quote == '"')
- if (text[text.length () - 1] == quote)
- text = text.substr (1, text.length () - 3);
+ std::string output = input;
+
+ if (output.length () > 1)
+ {
+ char quote = output[0];
+ if ((quote == '\'' || quote == '"') &&
+ output[output.length () - 1] == quote)
+ return output.substr (1, output.length () - 2);
+ }
+
+ return output;
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/src/util.cpp b/src/util.cpp
index a506377af..1fe260c89 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -25,6 +25,7 @@
//
////////////////////////////////////////////////////////////////////////////////
#include
+#include
#include
#include
#include
@@ -404,3 +405,28 @@ int flock (int fd, int operation)
#endif
////////////////////////////////////////////////////////////////////////////////
+bool slurp (
+ const std::string& file,
+ std::vector & contents,
+ bool trimLines /* = false */)
+{
+ contents.clear ();
+
+ std::ifstream in (file.c_str ());
+ if (in.good ())
+ {
+ std::string line;
+ while (getline (in, line))
+ {
+ if (trimLines) line = trim (line);
+ contents.push_back (line);
+ }
+
+ in.close ();
+ return true;
+ }
+
+ return false;
+}
+
+////////////////////////////////////////////////////////////////////////////////