2 $Header: /cvs/src/tdl/main.c,v 1.39.2.6 2004/02/03 22:17:22 richard Exp $
4 tdl - A console program for managing to-do lists
5 Copyright (C) 2001,2002,2003,2004,2005 Richard P. Curnow
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.
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.
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
29 #include <sys/types.h>
35 #include <sys/utsname.h>
39 /* The name of the database file (in whichever directory it may be) */
40 #define DBNAME ".tdldb"
42 /* Set if db doesn't exist in this directory */
43 static char *current_database_path = NULL;
45 /* The currently loaded database */
48 /* Flag for whether data is actually loaded yet */
49 static int is_loaded = 0;
52 static char *lock_file_name = NULL;
55 /* Flag if currently loaded database has been changed and needs writing back to
57 static int currently_dirty = 0;
59 /* Flag indicating whether to load databases read only */
60 static int read_only = 0;
62 /* Whether to complain about problems with file operations */
63 static int is_noisy = 1;
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;
69 static int is_interactive = 0;
71 static void set_descendent_priority(struct node *x, enum Priority priority)/*{{{*/
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);
81 /* This will be variable eventually */
82 static char default_database_path[] = "./" DBNAME;
85 static void unlock_database(void)/*{{{*/
87 if (lock_file_name) unlink(lock_file_name);
91 static volatile void unlock_and_exit(int code)/*{{{*/
97 static void lock_database(char *path)/*{{{*/
107 if (uname(&uu) < 0) {
111 pw = getpwuid(getuid());
117 len = 1 + strlen(path) + 5;
118 lock_file_name = new_array(char, len);
119 sprintf(lock_file_name, "%s.lock", path);
126 len += strlen(uu.nodename);
127 /* add on max width of pid field (allow up to 32 bit pid_t) + 2 '.' chars */
129 tname = new_array(char, len);
130 sprintf(tname, "%s.%d.%s", lock_file_name, pid, uu.nodename);
131 out = fopen(tname, "w");
133 fprintf(stderr, "Cannot open lock file %s for writing\n", tname);
136 fprintf(out, "%d,%s,%s\n", pid, uu.nodename, pw->pw_name);
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");
146 if (sb.st_nlink != 2) {
148 in = fopen(lock_file_name, "r");
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);
158 /* lock succeeded apparently */
162 /* lock succeeded apparently */
170 static volatile void unlock_and_exit(int code)/*{{{*/
175 #endif /* USE_DOTLOCK */
177 static char *get_database_path(int traverse_up)/*{{{*/
180 env_var = getenv("TDL_DATABASE");
184 int at_root, orig_size, size, dbname_len, found, stat_result;
185 char *orig_cwd, *cwd, *result, *filename;
188 dbname_len = strlen(DBNAME);
193 cwd = new_array(char, size);
194 orig_cwd = new_array(char, orig_size);
196 result = getcwd(orig_cwd, orig_size);
198 if (errno == ERANGE) {
200 orig_cwd = grow_array(char, orig_size, orig_cwd);
202 fprintf(stderr, "Unexpected error reading current directory\n");
207 filename = new_array(char, size + dbname_len + 2);
210 result = getcwd(cwd, size);
211 if (!result && (errno == ERANGE)) {
213 cwd = grow_array(char, size, cwd);
214 filename = grow_array(char, size + dbname_len + 2, filename);
216 if (!strcmp(cwd, "/")) {
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)) {
228 if (!traverse_up) break;
230 /* Otherwise, go up a level */
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. */
248 return default_database_path;
254 static void rename_database(char *path)/*{{{*/
260 pathbak = new_array(char, len + 5);
261 strcpy(pathbak, path);
262 strcat(pathbak, ".bak");
263 if (rename(path, pathbak) < 0) {
265 perror("warning, couldn't save backup database:");
272 static char *executable_name(char *argv0)/*{{{*/
275 for (p=argv0; *p; p++) ;
276 for (; p>=argv0; p--) {
277 if (*p == '/') return (p+1);
282 static void load_database(char *path) /*{{{*/
283 /* Return 1 if successful, 0 if no database was found */
292 in = fopen(path, "rb");
294 /* Database may not exist, e.g. if the program has never been run before.
296 read_database(in, &top);
301 fprintf(stderr, "warning: no database found above this directory\n");
306 void load_database_if_not_loaded(void)/*{{{*/
309 load_database(current_database_path);
314 static mode_t get_mode(const char *path)/*{{{*/
317 const mode_t default_result = 0600; /* access to user only. */
319 if (stat(path, &sb) < 0) {
320 result = default_result;
322 if (!S_ISREG(sb.st_mode)) {
323 fprintf(stderr, "Warning : existing database is not a regular file!\n");
324 result = default_result;
326 result = sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO);
332 static void save_database(char *path)/*{{{*/
336 mode_t database_mode;
338 fprintf(stderr, "Warning : database opened read-only. Not saving.\n");
341 if (is_loaded && currently_dirty) {
342 database_mode = get_mode(path);
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
347 rename_database(path);
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
352 out_fd = open(path, O_WRONLY | O_CREAT | O_EXCL, database_mode);
354 fprintf(stderr, "Could not open new database %s for writing : %s\n",
355 path, strerror(errno));
359 out = fdopen(out_fd, "wb");
362 fprintf(stderr, "Cannot open database %s for writing\n", path);
365 write_database(out, &top);
372 void free_database(struct links *x)/*{{{*/
376 for (y=x->next; y != (struct node *) x; y = next) {
377 free_database(&y->kids);
379 next = y->chain.next;
382 x->next = x->prev = (struct node *) x;
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";
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>]";
471 static int process_create(char **x)/*{{{*/
473 char *dbpath = get_database_path(0);
477 result = stat(dbpath, &sb);
479 fprintf(stderr, "Can't create database <%s>, it already exists!\n", dbpath);
483 fprintf(stderr, "Can't create database <%s> in read-only mode!\n", dbpath);
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 */
490 /* Force empty database to be written out */
491 is_loaded = currently_dirty = 1;
496 static int process_priority(char **x)/*{{{*/
499 enum Priority priority;
503 priority = parse_priority(*x, &error);
505 fprintf(stderr, "usage: priority %s\n", synop_priority);
510 do_descendents = include_descendents(*x); /* May modify *x */
511 n = lookup_node(*x, 0, NULL);
513 n->priority = priority;
514 if (do_descendents) {
515 set_descendent_priority(n, priority);
521 static int process_which(char **argv)/*{{{*/
523 printf("%s\n", current_database_path);
527 static int process_version(char **x)/*{{{*/
529 fprintf(stderr, "tdl %s\n", PROGRAM_VERSION);
533 static int process_exit(char **x)/*{{{*/
535 save_database(current_database_path);
541 static int process_quit(char **x)/*{{{*/
543 /* Just get out quick, don't write the database back */
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");
560 static int process_save(char **x)/*{{{*/
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);
570 static int process_revert(char **x)/*{{{*/
575 is_loaded = currently_dirty = 0;
579 /* Forward prototype */
580 static int usage(char **x);
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}
626 #define N(x) (sizeof(x) / sizeof(x[0]))
628 static int is_processing = 0;
629 static int signal_count = 0;
631 static void handle_signal(int a)/*{{{*/
634 /* And close stdin, which should cause readline() in inter.c to return
635 * immediately if it was active when the signal arrived. */
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));
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));
652 static void guarded_sigaction(int signum, struct sigaction *sa)/*{{{*/
654 if (sigaction(signum, sa, NULL) < 0) {
660 static void setup_signals(void)/*{{{*/
663 if (sigemptyset(&sa.sa_mask) < 0) {
664 perror("sigemptyset");
667 sa.sa_handler = handle_signal;
670 guarded_sigaction(SIGHUP, &sa);
671 guarded_sigaction(SIGINT, &sa);
672 guarded_sigaction(SIGQUIT, &sa);
673 guarded_sigaction(SIGTERM, &sa);
679 static void print_copyright(void)/*{{{*/
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",
689 void dispatch(char **argv) /* and other args *//*{{{*/
691 int i, p_len, matchlen, index=-1;
696 if (signal_count > 0) {
697 save_database(current_database_path);
701 executable = executable_name(argv[0]);
702 is_tdl = (!strcmp(executable, "tdl"));
705 while (*p && *p[0] == '-') p++;
707 /* Parse command line */
709 /* If no arguments, go into interactive mode, but only if we didn't come from there (!) */
710 if (!is_interactive) {
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)) {
730 for (i=0; i<n_cmds; i++) {
731 if (cmds[i].shortcut && !strcmp(cmds[i].shortcut, executable)) {
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",
742 if (!is_interactive) {
745 } else if (index >= 0) {
750 if (!is_loaded && cmds[index].load_db) {
751 load_database(current_database_path);
754 pp = is_tdl ? (p + 1) : p;
755 result = (cmds[index].func)(pp);
757 /* Check for failure */
759 if (!is_interactive) {
760 unlock_and_exit(-result);
763 /* If interactive, the handling function has emitted its error message.
764 * Just 'abort' this command and go back to the prompt */
768 if (cmds[index].dirty) {
774 if (signal_count > 0) {
775 save_database(current_database_path);
781 fprintf(stderr, "tdl: Unknown command <%s>\n", *p);
783 fprintf(stderr, "tdl: Unknown command\n");
785 if (!is_interactive) {
792 static int usage(char **x)/*{{{*/
798 /* Detailed help for the one command */
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)) {
808 fprintf(stdout, "Description\n %s\n\n", cmds[i].descrip);
809 fprintf(stdout, "Synopsis\n");
811 if (is_interactive) {
812 fprintf(stdout, " %s %s\n", cmds[i].name, cmds[i].synopsis ? cmds[i].synopsis : "");
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 : "");
822 "General notes (where they apply to a command):\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"
833 fprintf(stderr, "Unrecognized command <%s>, no help available\n", cmd);
839 if (!is_interactive) {
840 fprintf(stdout, "tdl [-qR] : Enter interactive mode\n");
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);
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);
856 if (is_interactive) {
857 fprintf(stdout, "\nEnter 'help <command-name>' for more help on a particular command\n");
859 fprintf(stdout, "\nEnter 'tdl help <command-name>' for more help on a particular command\n");
863 fprintf(stdout, "\n");
870 /*{{{ int main (int argc, char **argv)*/
871 int main (int argc, char **argv)
877 /* Initialise database */
878 top.prev = (struct node *) ⊤
879 top.next = (struct node *) ⊤
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]);
888 if (strchr(argv[i], 'q')) {
891 if (strchr(argv[i], 'R')) {
894 if (strchr(argv[i], 'u')) {
900 current_database_path = get_database_path(1);
905 save_database(current_database_path);