0590466e8e660b82504c8400b5dfdfe66cb85829
[tdl.git] / main.c
1 /*
2    $Header: /cvs/src/tdl/main.c,v 1.39.2.6 2004/02/03 22:17:22 richard Exp $
3   
4    tdl - A console program for managing to-do lists
5    Copyright (C) 2001,2002,2003,2004,2005  Richard P. Curnow
6
7    This program is free software; you can redistribute it and/or modify
8    it under the terms of the GNU General Public License as published by
9    the Free Software Foundation; either version 2 of the License, or
10    (at your option) any later version.
11
12    This program is distributed in the hope that it will be useful,
13    but WITHOUT ANY WARRANTY; without even the implied warranty of
14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15    GNU General Public License for more details.
16
17    You should have received a copy of the GNU General Public License
18    along with this program; if not, write to the Free Software
19    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
20    */
21
22 #include "tdl.h"
23 #include "version.h"
24 #include <assert.h>
25 #include <string.h>
26 #include <errno.h>
27 #include <ctype.h>
28 #include <fcntl.h>
29 #include <sys/types.h>
30 #include <sys/stat.h>
31 #include <unistd.h>
32 #include <signal.h>
33
34 #ifdef USE_DOTLOCK
35 #include <sys/utsname.h>
36 #include <pwd.h>
37 #endif
38
39 /* The name of the database file (in whichever directory it may be) */
40 #define DBNAME ".tdldb"
41
42 /* Set if db doesn't exist in this directory */
43 static char *current_database_path = NULL;
44
45 /* The currently loaded database */
46 struct links top;
47
48 /* Flag for whether data is actually loaded yet */
49 static int is_loaded = 0;
50
51 #ifdef USE_DOTLOCK
52 static char *lock_file_name = NULL;
53 #endif
54
55 /* Flag if currently loaded database has been changed and needs writing back to
56  * the filesystem */
57 static int currently_dirty = 0;
58
59 /* Flag indicating whether to load databases read only */
60 static int read_only = 0;
61
62 /* Whether to complain about problems with file operations */
63 static int is_noisy = 1;
64
65 /* Whether to forcibly unlock the database before the next lock attempt (e.g.
66  * if the program crashed for some reason before.) */
67 static int forced_unlock = 0;
68
69 static int is_interactive = 0;
70
71 static void set_descendent_priority(struct node *x, enum Priority priority)/*{{{*/
72 {
73   struct node *y;
74   for (y = x->kids.next; y != (struct node *) &x->kids; y = y->chain.next) {
75     y->priority = priority;
76     set_descendent_priority(y, priority);
77   }
78 }
79 /*}}}*/
80
81 /* This will be variable eventually */
82 static char default_database_path[] = "./" DBNAME;
83
84 #ifdef USE_DOTLOCK
85 static void unlock_database(void)/*{{{*/
86 {
87   if (lock_file_name) unlink(lock_file_name);
88   return;
89 }
90 /*}}}*/
91 static volatile void unlock_and_exit(int code)/*{{{*/
92 {
93   unlock_database();
94   exit(code);
95 }
96 /*}}}*/
97 static void lock_database(char *path)/*{{{*/
98 {
99   struct utsname uu;
100   struct passwd *pw;
101   int pid;
102   int len;
103   char *tname;
104   struct stat sb;
105   FILE *out;
106   
107   if (uname(&uu) < 0) {
108     perror("uname");
109     exit(1);
110   }
111   pw = getpwuid(getuid());
112   if (!pw) {
113     perror("getpwuid");
114     exit(1);
115   }
116   pid = getpid();
117   len = 1 + strlen(path) + 5;
118   lock_file_name = new_array(char, len);
119   sprintf(lock_file_name, "%s.lock", path);
120
121   if (forced_unlock) {
122     unlock_database();
123     forced_unlock = 0;
124   }
125   
126   len += strlen(uu.nodename);
127   /* add on max width of pid field (allow up to 32 bit pid_t) + 2 '.' chars */
128   len += (10 + 2);
129   tname = new_array(char, len);
130   sprintf(tname, "%s.%d.%s", lock_file_name, pid, uu.nodename);
131   out = fopen(tname, "w");
132   if (!out) {
133     fprintf(stderr, "Cannot open lock file %s for writing\n", tname);
134     exit(1);
135   }
136   fprintf(out, "%d,%s,%s\n", pid, uu.nodename, pw->pw_name);
137   fclose(out);
138
139   if (link(tname, lock_file_name) < 0) {
140     /* check if link count==2 */
141     if (stat(tname, &sb) < 0) {
142       fprintf(stderr, "Could not stat the lock file\n");
143       unlink(tname);
144       exit(1);
145     } else {
146       if (sb.st_nlink != 2) {
147         FILE *in;
148         in = fopen(lock_file_name, "r");
149         if (in) {
150           char line[2048];
151           fgets(line, sizeof(line), in);
152           line[strlen(line)-1] = 0; /* strip trailing newline */
153           fprintf(stderr, "Database %s appears to be locked by (pid,node,user)=(%s)\n", path, line);
154           unlink(tname);
155           exit(1);
156         }
157       } else {
158         /* lock succeeded apparently */
159       }
160     }
161   } else {
162     /* lock succeeded apparently */
163   }
164   unlink(tname);
165   free(tname);
166   return;
167 }
168 /*}}}*/
169 #else
170 static volatile void unlock_and_exit(int code)/*{{{*/
171 {
172   exit(code);
173 }
174 /*}}}*/
175 #endif /* USE_DOTLOCK */
176
177 static char *get_database_path(int traverse_up)/*{{{*/
178 {
179   char *env_var;
180   env_var = getenv("TDL_DATABASE");
181   if (env_var) {
182     return env_var;
183   } else {
184     int at_root, orig_size, size, dbname_len, found, stat_result;
185     char *orig_cwd, *cwd, *result, *filename;
186     struct stat statbuf;
187     
188     dbname_len = strlen(DBNAME);
189     size = 16;
190     orig_size = 16;
191     found = 0;
192     at_root = 0;
193     cwd = new_array(char, size);
194     orig_cwd = new_array(char, orig_size);
195     do {
196       result = getcwd(orig_cwd, orig_size);
197       if (!result) {
198         if (errno == ERANGE) {
199           orig_size <<= 1;
200           orig_cwd = grow_array(char, orig_size, orig_cwd);
201         } else {
202           fprintf(stderr, "Unexpected error reading current directory\n");
203           unlock_and_exit(1);
204         }
205       }
206     } while (!result);
207     filename = new_array(char, size + dbname_len + 2);
208     filename[0] = 0;
209     do {
210       result = getcwd(cwd, size);
211       if (!result && (errno == ERANGE)) {
212         size <<= 1;
213         cwd = grow_array(char, size, cwd);
214         filename = grow_array(char, size + dbname_len + 2, filename);
215       } else {
216         if (!strcmp(cwd, "/")) {
217           at_root = 1;
218         }
219         strcpy(filename, cwd);
220         strcat(filename, "/");
221         strcat(filename, DBNAME);
222         stat_result = stat(filename, &statbuf);
223         if ((stat_result >= 0) && (statbuf.st_mode & 0600)) {
224           found = 1;
225           break;
226         }
227
228         if (!traverse_up) break;
229         
230         /* Otherwise, go up a level */
231         chdir ("..");
232       }
233     } while (!at_root);
234
235     free(cwd);
236     
237     /* Reason for this : if using create in a subdirectory of a directory
238      * already containing a .tdldb, the cwd after the call here from main would
239      * get left pointing at the directory containing the .tdldb that already
240      * exists, making the call here from process_create() fail.  So go back to
241      * the directory where we started.  */
242     chdir(orig_cwd);
243     free(orig_cwd);
244
245     if (found) {
246       return filename;
247     } else {
248       return default_database_path;
249     }
250   }
251
252 }
253 /*}}}*/
254 static void rename_database(char *path)/*{{{*/
255 {
256   int len;
257   char *pathbak;
258
259   len = strlen(path);
260   pathbak = new_array(char, len + 5);
261   strcpy(pathbak, path);
262   strcat(pathbak, ".bak");
263   if (rename(path, pathbak) < 0) {
264     if (is_noisy) {
265       perror("warning, couldn't save backup database:");
266     }
267   }
268   free(pathbak);
269   return;
270
271 /*}}}*/
272 static char *executable_name(char *argv0)/*{{{*/
273 {
274   char *p;
275   for (p=argv0; *p; p++) ;
276   for (; p>=argv0; p--) {
277     if (*p == '/') return (p+1);
278   }
279   return argv0;
280 }
281 /*}}}*/
282 static void load_database(char *path) /*{{{*/
283   /* Return 1 if successful, 0 if no database was found */
284 {
285   FILE *in;
286   currently_dirty = 0;
287 #ifdef USE_DOTLOCK
288   if (!read_only) {
289     lock_database(path);
290   }
291 #endif
292   in = fopen(path, "rb");
293   if (in) {
294     /* Database may not exist, e.g. if the program has never been run before.
295        */
296     read_database(in, &top);
297     fclose(in);
298     is_loaded = 1;
299   } else {
300     if (is_noisy) {
301       fprintf(stderr, "warning: no database found above this directory\n");
302     }
303   }
304 }
305 /*}}}*/
306 void load_database_if_not_loaded(void)/*{{{*/
307 {
308   if (!is_loaded) {
309     load_database(current_database_path);
310     is_loaded = 1;
311   }
312 }
313 /*}}}*/
314 static mode_t get_mode(const char *path)/*{{{*/
315 {
316   mode_t result;
317   const mode_t default_result = 0600; /* access to user only. */
318   struct stat sb;
319   if (stat(path, &sb) < 0) {
320     result = default_result;
321   } else {
322     if (!S_ISREG(sb.st_mode)) {
323       fprintf(stderr, "Warning : existing database is not a regular file!\n");
324       result = default_result;
325     } else {
326       result = sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO);
327     }
328   }
329   return result;
330 }
331 /*}}}*/
332 static void save_database(char *path)/*{{{*/
333 {
334   FILE *out = NULL;
335   int out_fd;
336   mode_t database_mode;
337   if (read_only) {
338     fprintf(stderr, "Warning : database opened read-only. Not saving.\n");
339     return;
340   }
341   if (is_loaded && currently_dirty) {
342     database_mode = get_mode(path);
343     
344     /* The next line only used to happen if the command wasn't 'create'.
345      * However, it should quietly fail for create, where the existing database
346      * doesn't exist */
347     rename_database(path);
348
349     /* Open database this way so that the permissions from the existing
350        database can be duplicated onto the new one in way free of race
351        conditions. */
352     out_fd = open(path, O_WRONLY | O_CREAT | O_EXCL, database_mode);
353     if (out_fd < 0) {
354       fprintf(stderr, "Could not open new database %s for writing : %s\n",
355               path, strerror(errno));
356       unlock_and_exit(1);
357     } else {
358       /* Normal case */
359       out = fdopen(out_fd, "wb");
360     }
361     if (!out) {
362       fprintf(stderr, "Cannot open database %s for writing\n", path);
363       unlock_and_exit(1);
364     }
365     write_database(out, &top);
366     fclose(out);
367   }
368   currently_dirty = 0;
369   return;
370 }
371 /*}}}*/
372 void free_database(struct links *x)/*{{{*/
373 {
374   struct node *y;
375   struct node *next;
376   for (y=x->next; y != (struct node *) x; y = next) {
377     free_database(&y->kids);
378     free(y->text);
379     next = y->chain.next;
380     free(y);
381   }
382   x->next = x->prev = (struct node *) x;
383 }
384 /*}}}*/
385
386 /* {{{ One line descriptions of the subcommands */
387 static char desc_above[] = "Move entries above (before) another entry";
388 static char desc_add[] = "Add a new entry to the database";
389 static char desc_after[] = "Move entries after (below) another entry";
390 static char desc_before[] = "Move entries before (above) another entry";
391 static char desc_below[] = "Move entries below (after) another entry";
392 static char desc_clone[] = "Make deep copy of one or more entries";
393 static char desc_copyto[] = "Insert deep copy of one or more entries under another entry";
394 static char desc_create[] = "Create a new database in the current directory";
395 static char desc_defer[] = "Put off starting some tasks until a given time";
396 static char desc_delete[] = "Remove 1 or more entries from the database";
397 static char desc_done[] = "Mark 1 or more entries as done";
398 static char desc_edit[] = "Change the text of an entry";
399 static char desc_exit[] = "Exit program, saving database";
400 static char desc_export[] = "Export entries to another database";
401 static char desc_help[] = "Display help information";
402 static char desc_import[] = "Import entries from another database";
403 static char desc_ignore[] = "Postpone or partially remove 1 or more entries";
404 static char desc_into[] = "Move entries to end of new parent";
405 static char desc_list[] = "List entries in database (default from top node)";
406 static char desc_log[] = "Add a new entry to the database, mark it done as well";
407 static char desc_moveto[] = "Move entries to end of new parent";
408 static char desc_narrow[] = "Restrict actions to part of the database";
409 static char desc_open[] = "Move one or more entries out of postponed/deferred state";
410 static char desc_postpone[] = "Make one or more entries postponed indefinitely";
411 static char desc_priority[] = "Change the priority of 1 or more entries";
412 static char desc_purge[] = "Remove old done entries in subtrees";
413 static char desc_quit[] = "Exit program, NOT saving database";
414 static char desc_remove[] = "Remove 1 or more entries from the database";
415 static char desc_report[] = "Report completed tasks in interval";
416 static char desc_revert[] = "Discard changes and reload previous database from disc";
417 static char desc_save[] = "Save the database back to disc and keep working";
418 static char desc_undo[] = "Mark 1 or more entries as not done (cancel effect of 'done')";
419 static char desc_usage[] = "Display help information";
420 static char desc_version[] = "Display program version";
421 static char desc_which[] = "Display filename of database being used";
422 static char desc_widen[] = "Widen the part of the database to which actions apply";
423
424 /* }}} */
425 /* {{{ Synopsis of each subcommand */
426 static char synop_above[] = "<index_to_insert_above> <index_to_move> ...";
427 static char synop_add[] = "[@<datespec>] [<parent_index>] [<priority>] <entry_text>";
428 static char synop_after[] = "<index_to_insert_below> <index_to_move> ...";
429 static char synop_before[] = "<index_to_insert_above> <index_to_move> ...";
430 static char synop_below[] = "<index_to_insert_below> <index_to_move> ...";
431 static char synop_clone[] = "<index_to_clone> ...";
432 static char synop_copyto[] = "<parent_index> <index_to_clone> ...";
433 static char synop_create[] = "";
434 static char synop_defer[] = "[@]<datespec> <entry_index>{...] ...";
435 static char synop_delete[] = "<entry_index>[...] ...";
436 static char synop_done[] = "[@<datespec>] <entry_index>[...] ...";
437 static char synop_edit[] = "<entry_index> [<new_text>]";
438 static char synop_exit[] = "";
439 static char synop_export[] = "<filename> <entry_index> ...";
440 static char synop_help[] = "[<command-name>]";
441 static char synop_ignore[] = "<entry_index>[...] ...";
442 static char synop_import[] = "<filename>";
443 static char synop_into[] = "<new_parent_index> <index_to_move> ...";
444 static char synop_list[] = "[-v] [-a] [-p] [-m] [-1..9] [<min-priority>] [<parent_index>|/<search_condition>...]\n"
445                            "-v                 : verbose (show dates, priorities etc)\n"
446                            "-a                 : show all entries, including 'done' ones\n"
447                            "-p                 : show deferred and postponed entries\n"
448                            "-m                 : don't use colours (monochrome)\n"
449                            "-1,-2,..,-9        : summarise (and don't show) entries below this depth\n"
450                            "<search_condition> : word to match on";
451 static char synop_log[] = "[@<datespec>] [<parent_index>] [<priority>] <entry_text>";
452 static char synop_moveto[] = "<new_parent_index> <index_to_move> ...";
453 static char synop_narrow[] = "<entry_index>";
454 static char synop_open[] = "<entry_index>[...] ...";
455 static char synop_postpone[] = "<entry_index>[...] ...";
456 static char synop_priority[] = "<new_priority> <entry_index>[...] ...";
457 static char synop_purge[] = "<since_datespec> [<ancestor_index> ...]";
458 static char synop_quit[] = "";
459 static char synop_remove[] = "<entry_index>[...] ...";
460 static char synop_report[] = "<start_datespec> [<end_datespec>]\n"
461                              "(end defaults to now)";
462 static char synop_revert[] = "";
463 static char synop_save[] = "";
464 static char synop_undo[] = "<entry_index>[...] ...";
465 static char synop_usage[] = "[<command-name>]";
466 static char synop_version[] = "";
467 static char synop_which[] = "";
468 static char synop_widen[] = "[<levels>]";
469 /* }}} */
470
471 static int process_create(char **x)/*{{{*/
472 {
473   char *dbpath = get_database_path(0);
474   struct stat sb;
475   int result;
476
477   result = stat(dbpath, &sb);
478   if (result >= 0) {
479     fprintf(stderr, "Can't create database <%s>, it already exists!\n", dbpath);
480     return -1;
481   } else {
482     if (read_only) {
483       fprintf(stderr, "Can't create database <%s> in read-only mode!\n", dbpath);
484        return -1;
485     }
486     /* Should have an empty database, and the dirty flag will be set */
487     current_database_path = dbpath;
488     /* don't emit complaint about not being able to move database to its backup */
489     is_noisy = 0;
490     /* Force empty database to be written out */
491     is_loaded = currently_dirty = 1;
492     return 0;
493   }
494 }
495 /*}}}*/
496 static int process_priority(char **x)/*{{{*/
497 {
498   int error;
499   enum Priority priority;
500   struct node *n;
501   int do_descendents;
502  
503   priority = parse_priority(*x, &error);
504   if (error < 0) {
505     fprintf(stderr, "usage: priority %s\n", synop_priority);
506     return error;
507   }
508
509   while (*++x) {
510     do_descendents = include_descendents(*x); /* May modify *x */
511     n = lookup_node(*x, 0, NULL);
512     if (!n) return -1;
513     n->priority = priority;
514     if (do_descendents) {
515       set_descendent_priority(n, priority);
516     }
517   }
518
519   return 0;
520 }/*}}}*/
521 static int process_which(char **argv)/*{{{*/
522 {
523   printf("%s\n", current_database_path);
524   return 0;
525 }
526 /*}}}*/
527 static int process_version(char **x)/*{{{*/
528 {
529   fprintf(stderr, "tdl %s\n", PROGRAM_VERSION);
530   return 0;
531 }
532 /*}}}*/
533 static int process_exit(char **x)/*{{{*/
534 {
535   save_database(current_database_path);
536   free_database(&top);
537   unlock_and_exit(0);
538   return 0; /* moot */
539 }
540 /*}}}*/
541 static int process_quit(char **x)/*{{{*/
542 {
543   /* Just get out quick, don't write the database back */
544   char ans[4];
545   if (currently_dirty) {
546     printf(" WARNING: if you quit, all changes to database will be lost!\n"
547         " Use command 'exit' instead of 'quit' if you wish to save data.\n"
548         " Really quit [y/N]? ");
549     fgets (ans, 4, stdin);
550     if (strcasecmp(ans,"y\n") != 0) {
551       printf(" Quit canceled.\n");
552       return 0;
553     }
554   }
555   free_database(&top);
556   unlock_and_exit(0);
557   return 0; /* moot */
558 }
559 /*}}}*/
560 static int process_save(char **x)/*{{{*/
561 {
562   /* FIXME: I'm not sure whether the behaviour here should include renaming the
563    * existing disc database to become the backup file.  I think the precedent
564    * would be how vi or emacs handle backup files when multiple saves are done
565    * within a session. */
566   save_database(current_database_path);
567   return 0;
568 }
569 /*}}}*/
570 static int process_revert(char **x)/*{{{*/
571 {
572   if (is_loaded) {
573     free_database(&top);
574   }
575   is_loaded = currently_dirty = 0;
576   return 0;
577 }
578 /*}}}*/
579 /* Forward prototype */
580 static int usage(char **x);
581
582 struct command cmds[] = {/*{{{*/
583   {"--help",   NULL,   usage,            desc_help,    NULL,          NULL,              0, 0, 3, 0, 1},
584   {"-h",       NULL,   usage,            desc_help,    NULL,          NULL,              0, 0, 2, 0, 1},
585   {"-V",       NULL,   process_version,  desc_version, NULL,          NULL,              0, 0, 2, 0, 1},
586   {"above",    NULL,   process_above,    desc_above,   synop_above,   NULL,              1, 1, 2, 1, 1},
587   {"add",      "tdla", process_add,      desc_add,     synop_add,     NULL,              1, 1, 2, 1, 1},
588   {"after",    NULL,   process_below,    desc_after,   synop_after,   NULL,              1, 1, 2, 1, 1},
589   {"before",   NULL,   process_above,    desc_before,  synop_before,  NULL,              1, 1, 3, 1, 1},
590   {"below",    NULL,   process_below,    desc_below,   synop_below,   NULL,              1, 1, 3, 1, 1},
591   {"clone",    NULL,   process_clone,    desc_clone,   synop_clone,   NULL,              1, 1, 2, 1, 1}, 
592   {"copyto",   NULL,   process_copyto,   desc_copyto,  synop_copyto,  NULL,              1, 1, 2, 1, 1}, 
593   {"create",   NULL,   process_create,   desc_create,  synop_create,  NULL,              1, 0, 2, 0, 1},
594   {"defer",    NULL,   process_defer,    desc_defer,   synop_defer,   NULL,              1, 1, 3, 1, 1},
595   {"delete",   NULL,   process_remove,   desc_delete,  synop_delete,  NULL,              1, 1, 3, 1, 1},
596   {"done",     "tdld", process_done,     desc_done,    synop_done,    complete_done,     1, 1, 2, 1, 1},
597   {"edit",     NULL,   process_edit,     desc_edit,    synop_edit,    NULL,              1, 1, 2, 1, 1},
598   {"exit",     NULL,   process_exit,     desc_exit,    synop_exit,    NULL,              0, 0, 3, 1, 0},
599   {"export",   NULL,   process_export,   desc_export,  synop_export,  NULL,              0, 1, 3, 1, 1},
600   {"help",     NULL,   usage,            desc_help,    synop_help,    complete_help,     0, 0, 1, 1, 1},
601   {"ignore",   NULL,   process_ignore,   desc_ignore,  synop_ignore,  complete_done,     1, 1, 2, 1, 1},
602   {"import",   NULL,   process_import,   desc_import,  synop_import,  NULL,              1, 1, 2, 1, 1},
603   {"into",     NULL,   process_into,     desc_into,    synop_into,    NULL,              1, 1, 2, 1, 1},
604   {"list",     "tdll", process_list,     desc_list,    synop_list,    complete_list,     0, 1, 2, 1, 1},
605   {"ls",       "tdls", process_list,     desc_list,    synop_list,    complete_list,     0, 1, 2, 1, 1},
606   {"log",      "tdlg", process_log,      desc_log,     synop_log,     NULL,              1, 1, 2, 1, 1},
607   {"moveto",   NULL,   process_into,     desc_moveto,  synop_moveto,  NULL,              1, 1, 1, 1, 1},
608   {"narrow",   NULL,   process_narrow,   desc_narrow,  synop_narrow,  NULL,              0, 1, 1, 1, 0},
609   {"open",     NULL,   process_open,     desc_open,    synop_open,    complete_open,     1, 1, 1, 1, 1},
610   {"postpone", NULL,   process_postpone, desc_postpone,synop_postpone,complete_postpone, 1, 1, 2, 1, 1},
611   {"priority", NULL,   process_priority, desc_priority,synop_priority,complete_priority, 1, 1, 2, 1, 1},
612   {"purge",    NULL,   process_purge,    desc_purge,   synop_purge,   NULL,              1, 1, 2, 1, 1},
613   {"quit",     NULL,   process_quit,     desc_quit,    synop_quit,    NULL,              0, 0, 1, 1, 0},
614   {"remove",   NULL,   process_remove,   desc_remove,  synop_remove,  NULL,              1, 1, 3, 1, 1},
615   {"report",   NULL,   process_report,   desc_report,  synop_report,  NULL,              0, 1, 3, 1, 1},
616   {"revert",   NULL,   process_revert,   desc_revert,  synop_revert,  NULL,              0, 0, 3, 1, 0},
617   {"save",     NULL,   process_save,     desc_save,    synop_save,    NULL,              0, 1, 1, 1, 0},
618   {"undo",     NULL,   process_undo,     desc_undo,    synop_undo,    NULL,              1, 1, 2, 1, 1},
619   {"usage",    NULL,   usage,            desc_usage,   synop_usage,   complete_help,     0, 0, 2, 1, 1},
620   {"version",  NULL,   process_version,  desc_version, synop_version, NULL,              0, 0, 1, 1, 1},
621   {"which",    NULL,   process_which,    desc_which,   synop_which,   NULL,              0, 0, 2, 1, 1},
622   {"widen",    NULL,   process_widen,    desc_widen,   synop_widen,   NULL,              0, 1, 2, 1, 0}
623 };/*}}}*/
624 int n_cmds = 0;
625
626 #define N(x) (sizeof(x) / sizeof(x[0]))
627
628 static int is_processing = 0;
629 static int signal_count = 0;
630
631 static void handle_signal(int a)/*{{{*/
632 {
633   signal_count++;
634   /* And close stdin, which should cause readline() in inter.c to return
635    * immediately if it was active when the signal arrived. */
636   close(0);
637
638   if (signal_count == 3) {
639     /* User is desperately hitting ^C to stop the program.  Bail out without tidying up, and give a warning. */
640     static char msg[] = "About to force exit due to repeated termination signals.\n"
641        "Database changes since the last save will be LOST.\n";
642     write(2, msg, strlen(msg));
643   }
644   if (signal_count == 4) {
645     static char msg[] = "The database may be left locked.\n"
646       "You will need to run 'tdl -u' next time to unlock it.\n";
647     write(2, msg, strlen(msg));
648     exit(1);
649   }
650 }
651 /*}}}*/
652 static void guarded_sigaction(int signum, struct sigaction *sa)/*{{{*/
653 {
654   if (sigaction(signum, sa, NULL) < 0) {
655     perror("sigaction");
656     unlock_and_exit(1);
657   }
658 }
659 /*}}}*/
660 static void setup_signals(void)/*{{{*/
661 {
662   struct sigaction sa;
663   if (sigemptyset(&sa.sa_mask) < 0) {
664     perror("sigemptyset");
665     unlock_and_exit(1);
666   }
667   sa.sa_handler = handle_signal;
668   sa.sa_flags = 0;
669 #if 0
670   guarded_sigaction(SIGHUP, &sa);
671   guarded_sigaction(SIGINT, &sa);
672   guarded_sigaction(SIGQUIT, &sa);
673   guarded_sigaction(SIGTERM, &sa);
674 #endif
675
676   return;
677 }
678 /*}}}*/
679 static void print_copyright(void)/*{{{*/
680 {
681   fprintf(stderr,
682           "tdl %s, Copyright (C) 2001,2002,2003,2004,2005 Richard P. Curnow\n"
683           "tdl comes with ABSOLUTELY NO WARRANTY.\n"
684           "This is free software, and you are welcome to redistribute it\n"
685           "under certain conditions; see the GNU General Public License for details.\n\n",
686           PROGRAM_VERSION);
687 }
688 /*}}}*/
689 void dispatch(char **argv) /* and other args *//*{{{*/
690 {
691   int i, p_len, matchlen, index=-1;
692   char *executable;
693   int is_tdl;
694   char **p, **pp;
695
696   if (signal_count > 0) {
697     save_database(current_database_path);
698     unlock_and_exit(0);
699   }
700
701   executable = executable_name(argv[0]);
702   is_tdl = (!strcmp(executable, "tdl"));
703   
704   p = argv + 1;
705   while (*p && *p[0] == '-') p++;
706   
707   /* Parse command line */
708   if (is_tdl && !*p) {
709     /* If no arguments, go into interactive mode, but only if we didn't come from there (!) */
710     if (!is_interactive) {
711       setup_signals();
712       print_copyright();
713       is_interactive = 1;
714       interactive();
715     }
716     return;
717   }
718
719   if (is_tdl) {
720     p_len = strlen(*p);
721     for (i=0; i<n_cmds; i++) {
722       matchlen = p_len < cmds[i].matchlen ? cmds[i].matchlen : p_len;
723       if ((is_interactive ? cmds[i].interactive_ok : cmds[i].non_interactive_ok) &&
724           !strncmp(cmds[i].name, *p, matchlen)) {
725         index = i;
726         break;
727       }
728     }
729   } else {
730     for (i=0; i<n_cmds; i++) {
731       if (cmds[i].shortcut && !strcmp(cmds[i].shortcut, executable)) {
732         index = i;
733         break;
734       }
735     }
736   }
737
738   /* Skip commands that dirty the database, if it was opened read-only. */
739   if (index >= 0 && cmds[index].dirty && read_only) {
740     fprintf(stderr, "Can't use command <%s> in read-only mode\n",
741                 cmds[index].name);
742     if (!is_interactive) {
743       unlock_and_exit(-1);
744     }
745   } else if (index >= 0) {
746     int result;
747
748     is_processing = 1;
749
750     if (!is_loaded && cmds[index].load_db) {
751       load_database(current_database_path);
752     }
753
754     pp = is_tdl ? (p + 1) : p;
755     result = (cmds[index].func)(pp);
756     
757     /* Check for failure */
758     if (result < 0) {
759       if (!is_interactive) {
760         unlock_and_exit(-result);
761       }
762
763       /* If interactive, the handling function has emitted its error message.
764        * Just 'abort' this command and go back to the prompt */
765
766     } else {
767     
768       if (cmds[index].dirty) {
769         currently_dirty = 1;
770       }
771     }
772
773     is_processing = 0;
774     if (signal_count > 0) {
775       save_database(current_database_path);
776       unlock_and_exit(0);
777     }
778     
779   } else {
780     if (is_tdl && *p) {
781       fprintf(stderr, "tdl: Unknown command <%s>\n", *p);
782     } else {
783       fprintf(stderr, "tdl: Unknown command\n");
784     }
785     if (!is_interactive) {
786       unlock_and_exit(1);
787     }
788   }
789   
790 }
791 /*}}}*/
792 static int usage(char **x)/*{{{*/
793 {
794   int i, index;
795   char *cmd = *x;
796
797   if (cmd) {
798     /* Detailed help for the one command */
799     index = -1;
800     for (i=0; i<n_cmds; i++) {
801       if (!strncmp(cmds[i].name, cmd, 3) &&
802           (is_interactive ? cmds[i].interactive_ok : cmds[i].non_interactive_ok)) {
803         index = i;
804         break;
805       }
806     }
807     if (index >= 0) {
808       fprintf(stdout, "Description\n  %s\n\n", cmds[i].descrip);
809       fprintf(stdout, "Synopsis\n");
810       
811       if (is_interactive) {
812         fprintf(stdout, "  %s %s\n", cmds[i].name, cmds[i].synopsis ? cmds[i].synopsis : "");
813       } else {
814         fprintf(stdout, "  tdl  [-qR] %s %s\n", cmds[i].name, cmds[i].synopsis ? cmds[i].synopsis : "");
815         if (cmds[i].shortcut) {
816           fprintf(stdout, "  %s [-qR] %s\n", cmds[i].shortcut, cmds[i].synopsis ? cmds[i].synopsis : "");
817         }
818       }
819
820       fprintf(stdout,
821               "\n"
822               "General notes (where they apply to a command):\n"
823               "\n"
824               "<*_index>  : 1, 1.1 etc (see output of 'tdl list')\n"
825               "<priority> : urgent|high|normal|low|verylow\n"
826               "<datespec> : [-|+][0-9]+[shdwmy][-hh[mm[ss]]]  OR\n"
827               "             [-|+](sun|mon|tue|wed|thu|fri|sat)[-hh[mm[ss]]] OR\n"
828               "             [[[cc]yy]mm]dd[-hh[mm[ss]]]\n"
829               "<text>     : Any text (you'll need to quote it if >1 word)\n"
830               );
831
832     } else {
833       fprintf(stderr, "Unrecognized command <%s>, no help available\n", cmd);
834     }
835         
836   } else {
837     print_copyright();
838
839     if (!is_interactive) {
840       fprintf(stdout, "tdl  [-qR]          : Enter interactive mode\n");
841     }
842     for (i=0; i<n_cmds; i++) {
843       if (is_interactive) {
844         if (cmds[i].interactive_ok) {
845           fprintf(stdout, "%-8s : %s\n", cmds[i].name, cmds[i].descrip);
846         }
847       } else {
848         if (cmds[i].non_interactive_ok) {
849           fprintf(stdout, "tdl  [-qR] %-8s : %s\n", cmds[i].name, cmds[i].descrip);
850           if (cmds[i].shortcut) {
851             fprintf(stdout, "%s [-qR]          : %s\n", cmds[i].shortcut, cmds[i].descrip);
852           }
853         }
854       }
855     }
856     if (is_interactive) {
857       fprintf(stdout, "\nEnter 'help <command-name>' for more help on a particular command\n");
858     } else {
859       fprintf(stdout, "\nEnter 'tdl help <command-name>' for more help on a particular command\n");
860     }
861   }
862
863   fprintf(stdout, "\n");
864
865   return 0;
866
867 }
868 /*}}}*/
869
870 /*{{{  int main (int argc, char **argv)*/
871 int main (int argc, char **argv)
872 {
873   int i = 0;
874   n_cmds = N(cmds);
875   is_interactive = 0;
876   
877   /* Initialise database */
878   top.prev = (struct node *) &top;
879   top.next = (struct node *) &top;
880
881   if (argc > 1) {
882     for (i=1; i<argc && argv[i][0] == '-'; i++) {
883       if (strspn(argv[i]+1, "qRu")+1 != strlen(argv[i])) {
884         fprintf(stderr, "Unknown flag <%s>\n", argv[i]);
885         return 1;
886       }
887
888       if (strchr(argv[i], 'q')) {
889         is_noisy = 0;
890       }
891       if (strchr(argv[i], 'R')) {
892         read_only = 1;
893       }
894       if (strchr(argv[i], 'u')) {
895         forced_unlock = 1;
896       }
897     }
898   }
899
900   current_database_path = get_database_path(1);
901
902   dispatch(argv);
903
904   if (! read_only) {
905     save_database(current_database_path);
906   }
907   free_database(&top);
908   unlock_and_exit(0);
909   return 0; /* moot */
910 }
911 /*}}}*/
912
This page took 0.111032 seconds and 2 git commands to generate.