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 @@ + + + + Data Import + + + + + +
+ + + + + + +
+ + +
+
+
+
+

Data Import

+
+

+ Tasks can be imported from files with this command: + +

% task import file
+ + A variety of different file types are recognized by task, namely: + +
    +
  • Tasks exported from task prior to version 1.5.0. +
  • Tasks exported from task version 1.5.0 and later. The file + format changed with 1.5.0. +
  • todo.sh files. +
  • CSV files with a variety of recognized column names. +
  • Plain text files, with one task listed per line. +
  • Task command line format. +
+ + Task makes a good effort to determine which of these formats a + file is, and then imports accordingly. +

+ +

+ It would be wise to backup your task data files before an import. +

+
+ +
+
+
+

+ Copyright 2006-2009, P. Beckingham. All rights reserved. +

+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ +
+ + + + + + + diff --git a/html/task.html b/html/task.html index 3a2da9e51..6bc817a08 100644 --- a/html/task.html +++ b/html/task.html @@ -58,7 +58,11 @@
  • Filters
  • Shadow Files
  • Custom Reports +<<<<<<< HEAD:html/task.html
  • Frequently Asked Questions +======= +
  • Data Import +>>>>>>> import:html/task.html

    @@ -119,6 +123,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.

    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; +} + +////////////////////////////////////////////////////////////////////////////////