fixed some clang warnings
[k8lowj.git] / src / cmdline.c
blob0e680da091582fb1e859cbc21f96e15a0bae5e6f
1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2004 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
5 */
6 #ifdef HAVE_GTK
7 # include "gtk-all.h"
8 #else
9 # include "glib-all.h"
10 #endif
12 #include <sys/types.h>
13 #include <regex.h>
15 #include <unistd.h>
17 #include <string.h>
19 #include "liblj/consolecommand.h"
20 #include "liblj/login.h"
22 #include "getopt.h"
24 #include "conf_xml.h"
25 #include "journalstore.h"
26 #include "network.h"
27 #include "checkfriends.h"
28 #include "sync.h"
30 #include "jamdoc.h"
31 #include "cmdline.h"
32 #include "cmdline_data.h"
35 typedef struct _Command Command;
38 typedef struct {
39 JamDoc *doc;
40 char *username, *password, *postas;
41 char *filename;
43 Command *curcmd;
45 gboolean use_editor;
46 } Cmdline;
49 struct _Command {
50 const char *cmdname;
51 const char *desc;
52 gboolean requireuser, existinguser, needpw;
53 void (*action) (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]);
57 static void command_dispatch (Cmdline *cmdline, Command *commands, const char *help, int argc, gchar *argv[]);
60 static void print_header (void) {
61 g_print(_("LogJam %s\nCopyright (C) 2000-2004 Evan Martin\n\n"), PACKAGE_VERSION);
65 static void list_commands (Command *commands) {
66 int maxlen = 0, len;
67 Command *c;
68 char *fmt;
69 for (c = commands; c->cmdname; ++c) {
70 len = strlen(c->cmdname);
71 if (len > maxlen)
72 maxlen = len;
74 fmt = g_strdup_printf(" %%-%ds %%s\n", maxlen);
75 for (c = commands; c->cmdname; ++c) g_print(fmt, _(c->cmdname), _(c->desc));
76 g_free(fmt);
80 static void help_commands (Command *commands) {
81 g_print(_("Subcommands:\n"));
82 list_commands(commands);
86 static void print_help (char *argv0, Command *commands) {
87 print_header();
88 g_print(_("Use: %s [options] [command [commandargs]]\n"), argv0);
89 g_print("\n");
90 #ifdef HAVE_GTK
91 g_print(_("If no subcommand is provided, the default behavior is to load the GUI.\n"));
92 g_print("\n");
93 #endif
94 g_print(_(LOGJAM_HELP_TEXT));
95 help_commands(commands);
96 #ifdef HAVE_GTK
97 g_print("\n");
98 g_print(_("Also, GTK+ command line options (such as --display) can be used.\n"));
99 #endif
103 static void print_command_help(Command *cmd, const char *helptext, Command *subcmds) {
104 g_print("%s -- %s", cmd->cmdname, helptext);
105 if (subcmds)
106 help_commands(subcmds);
107 exit(EXIT_SUCCESS);
110 #define OPT_LIST_SUBCOMMANDS (1)
113 #define _GETOPT_LOOP(name) { \
114 const char *short_options = name ## _SHORT_OPTIONS; \
115 struct option long_options[] = name ## _LONG_OPTIONS; \
116 optind = 0; \
117 for (;;) { \
118 int c = getopt_long(argc, argv, short_options, long_options, NULL); \
119 if (c == -1) break; \
120 switch (c) { \
121 case '?': /* unknown option character. */ \
122 case ':': /* missing option character. */ \
123 g_printerr(_("Unknown or missing option character\n")); \
124 exit(EXIT_FAILURE);
127 #define GETOPT_LOOP_END \
133 #define _GETOPT_LOOP_SUBCOMMANDS(name) \
134 Command commands[] = name ## _SUBCOMMANDS; \
135 _GETOPT_LOOP(name) \
136 case OPT_LIST_SUBCOMMANDS: \
137 list_commands(commands); \
138 exit(EXIT_SUCCESS);
141 #define GETOPT_LOOP_SUBCOMMANDS_END(name) \
142 GETOPT_LOOP_END \
143 command_dispatch(cmdline, commands, name ## _HELP_TEXT, argc, argv);
146 #define GETOPT_LOOP(name) \
147 _GETOPT_LOOP(name) \
148 case 'h': \
149 print_command_help(cmdline->curcmd, _(name ## _HELP_TEXT), NULL);
152 #define GETOPT_LOOP_SUBCOMMANDS(name) \
153 _GETOPT_LOOP_SUBCOMMANDS(name) \
154 case 'h': \
155 print_command_help(cmdline->curcmd, _(name ## _HELP_TEXT), commands);
158 static void do_console (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
159 GString *command = g_string_new(NULL);
160 LJConsoleCommand *cc;
161 GError *err = NULL;
162 int i;
163 GETOPT_LOOP(CONSOLE)
164 GETOPT_LOOP_END if (!JAM_ACCOUNT_IS_LJ(acc)) {
165 g_printerr(_("Blogger accounts do not have a console interface.\n"));
166 exit(EXIT_FAILURE);
168 --argc;
169 ++argv; /* skip 'console' */
170 if (argc <= 0) {
171 g_printerr(_("Must specify a console command. Try \"help\".\n"));
172 exit(EXIT_FAILURE);
174 while (argc--) {
175 if (argc) g_string_append_printf(command, "%s ", argv++[0]); else g_string_append(command, argv++[0]);
177 cc = lj_consolecommand_new(jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc)), command->str);
178 g_string_free(command, TRUE);
179 if (!net_run_verb_ctx((LJVerb *) cc, network_ctx_cmdline, &err)) {
180 g_print("Error: %s\n", err->message);
181 g_error_free(err);
182 exit(EXIT_FAILURE);
184 for (i = 0; i < cc->linecount; ++i) {
185 g_print("%s\n", cc->lines[i].text);
187 lj_consolecommand_free(cc);
188 exit(EXIT_SUCCESS);
192 static void do_post (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
193 /*gboolean use_editor = FALSE; */
194 GETOPT_LOOP(POST)
195 case 'e': break; /*use_editor = TRUE; */
196 GETOPT_LOOP_END
197 if (!jam_host_do_post(jam_account_get_host(acc), network_ctx_cmdline, cmdline->doc, NULL)) {
198 exit(EXIT_FAILURE);
200 if (!app.quiet) g_print(_("Success.\n"));
201 exit(EXIT_SUCCESS);
205 static void do_checkfriends (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
206 JamAccountLJ *acclj;
207 char *subcommand = (argc > 1 ? argv[1] : NULL);
208 app.cli = TRUE;
209 GETOPT_LOOP(CHECKFRIENDS)
210 GETOPT_LOOP_END
211 if (!JAM_ACCOUNT_IS_LJ(acc)) {
212 g_printerr(_("Checkfriends is only supported by LiveJournal accounts.\n"));
213 exit(EXIT_FAILURE);
215 acclj = JAM_ACCOUNT_LJ(acc);
216 if (!subcommand) {
217 if (checkfriends_cli(acclj)) {
218 g_print("1\n");
219 exit(EXIT_SUCCESS); /* NEW */
220 } else {
221 g_print("0\n");
222 exit(EXIT_FAILURE); /* no new entries, or some error */
224 } else if (!g_ascii_strcasecmp("purge", subcommand)) {
225 char *id = jam_account_id_strdup(acc);
226 checkfriends_cli_purge(acclj);
227 g_print(_("Checkfriends information for %s purged.\n"), id);
228 g_free(id);
229 exit(EXIT_SUCCESS);
230 } else {
231 g_printerr(_("Unknown argument %s to --checkfriends\n"), subcommand);
232 exit(EXIT_FAILURE);
237 static void do_offline_sync (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
238 GETOPT_LOOP(OFFLINE_SYNC)
239 GETOPT_LOOP_END
240 app.cli = TRUE;
241 if (JAM_ACCOUNT_IS_LJ(acc)) {
242 if (sync_run(JAM_ACCOUNT_LJ(acc), NULL)) exit(EXIT_SUCCESS);
243 } else {
244 g_printerr(_("Sync is only supported by LiveJournal accounts.\n"));
246 exit(EXIT_FAILURE);
250 static void do_offline_cat (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
251 LJEntryFileType output_type = LJ_ENTRY_FILE_RFC822;
252 JournalStore *js = NULL;
253 GError *err = NULL;
254 GString *text;
255 gint counter = 0; /* how many entries written. for rfc822 separator. */
256 gboolean mailbox = FALSE;
257 GETOPT_LOOP(OFFLINE_CAT)
258 case 'm': mailbox = TRUE; break;
259 case 't': output_type = LJ_ENTRY_FILE_RFC822; break;
260 case 'x': output_type = LJ_ENTRY_FILE_XML; break;
261 GETOPT_LOOP_END
262 if (optind >= argc) {
263 fprintf(stderr, _("Specify at least one itemid or 'latest'.\n"));
264 exit(EXIT_FAILURE);
266 if (!(js = journal_store_open(acc, FALSE, &err))) {
267 fprintf(stderr, "%s", err->message);
268 g_error_free(err);
269 exit(EXIT_FAILURE);
271 while (optind < argc) {
272 gchar snum[16];
273 gint num = atoi(argv[optind]);
274 LJEntry *entry;
275 g_snprintf(snum, 16, "%d", num);
276 if (strncmp(snum, argv[optind], 16)) {
277 if (strncmp(argv[optind], "latest", 7)) {
278 fprintf(stderr, _("expected itemid, got %s\n"), argv[optind]);
279 exit(EXIT_FAILURE);
280 } else {
281 num = journal_store_get_latest_id(js);
284 if (!(entry = journal_store_get_entry(js, num))) {
285 fprintf(stderr, _("entry %d not found\n"), num);
286 exit(EXIT_FAILURE);
288 switch (output_type) {
289 case LJ_ENTRY_FILE_RFC822:
290 if (counter++) {
291 /* entry separator lead-in: *two* newlines unlike RFC822
292 * email, so that later on in reading a entrybox we can
293 * tell an entry doesn't end with a newline. */
294 g_print("\n\n");
296 text = lj_entry_to_rfc822(entry, FALSE);
297 if (mailbox) {
298 time_t now = time(NULL);
299 g_print("From %s@%s %s"
300 "X-LogJam-Itemid: %d\n" "%s", jam_account_get_username(acc), jam_account_get_host(acc)->name, ctime(&now), num, text->str);
301 } else {
302 g_print("X-LogJam-Itemid: %d\n"
303 "X-LogJam-User: %s\n"
304 "X-LogJam-Server: %s\n" "%s", num, jam_account_get_username(acc), jam_account_get_host(acc)->name, text->str);
306 g_string_free(text, TRUE);
307 break;
308 case LJ_ENTRY_FILE_XML:
309 default:
310 fprintf(stderr, "(xml cat not yet implemented.)\n");
311 exit(EXIT_FAILURE);
313 lj_entry_free(entry);
314 ++optind;
316 exit(EXIT_SUCCESS);
320 /* from regexp documentation in glibc */
321 static char *get_regerror (int errcode, regex_t *compiled) {
322 size_t length = regerror(errcode, compiled, NULL, 0);
323 char *buffer = g_new(char, length);
324 regerror(errcode, compiled, buffer, length);
325 return buffer;
329 static void do_offline_grep (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
330 LJEntryFileType output_type = LJ_ENTRY_FILE_RFC822;
331 JournalStore *js = NULL;
332 GError *err = NULL;
333 GString *text;
334 gint counter = 0; /* how many entries written. for rfc822 separator. */
335 gint cflags = 0;
336 gint res;
337 gint latest, itemid;
338 regex_t regexp;
339 GETOPT_LOOP(OFFLINE_GREP)
340 case 't': output_type = LJ_ENTRY_FILE_RFC822; break;
341 case 'x': output_type = LJ_ENTRY_FILE_XML; break;
342 case 'e': cflags |= REG_EXTENDED; break;
343 case 'i': cflags |= REG_ICASE; break;
344 GETOPT_LOOP_END
345 if (optind >= argc) {
346 g_printerr(_("Specify a regexp to match\n"));
347 exit(EXIT_FAILURE);
349 if (!(js = journal_store_open(acc, FALSE, &err))) {
350 g_printerr("%s", err->message);
351 g_error_free(err);
352 exit(EXIT_FAILURE);
354 if ((res = regcomp(&regexp, argv[optind], cflags|REG_NOSUB))) {
355 gchar *re_error = get_regerror(res, &regexp);
356 g_printerr(_("Regexp error: %s.\n"), re_error);
357 g_free(re_error);
358 exit(EXIT_FAILURE);
360 latest = journal_store_get_latest_id(js);
361 for (itemid = 1; itemid <= latest; itemid++) {
362 LJEntry *entry;
363 if (!(entry = journal_store_get_entry(js, itemid))) continue; /* deleted item */
364 // XXX body only
365 switch (regexec(&regexp, entry->event, 0, NULL, 0)) {
366 case REG_NOMATCH: continue; /* item does not match */
367 case REG_ESPACE: g_printerr(_("Regexp error: out of memory.\n")); exit(EXIT_FAILURE);
368 default: ; /* proceed */
370 switch (output_type) {
371 case LJ_ENTRY_FILE_RFC822:
372 if (counter++) {
373 /* entry separator lead-in: *two* newlines unlike RFC822
374 * email, so that later on in reading a entrybox we can
375 * tell an entry doesn't end with a newline. */
376 g_print("\n\n");
378 text = lj_entry_to_rfc822(entry, FALSE);
379 g_print("X-LogJam-Itemid: %d\n"
380 "X-LogJam-User: %s\n"
381 "X-LogJam-Server: %s\n" "%s", itemid, jam_account_get_username(acc), jam_account_get_host(acc)->name, text->str);
382 g_string_free(text, TRUE);
383 break;
384 case LJ_ENTRY_FILE_XML:
385 default:
386 g_printerr("Not yet implemented.\n");
387 exit(EXIT_FAILURE);
389 lj_entry_free(entry);
391 exit(EXIT_SUCCESS);
395 static void do_offline_summary (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
396 GError *err = NULL;
397 JournalStore *js = NULL;
398 char *id, *lastsync;
399 GETOPT_LOOP(OFFLINE_SUMMARY)
400 GETOPT_LOOP_END
401 command_dispatch(cmdline, NULL, _(OFFLINE_SUMMARY_HELP_TEXT), argc, argv);
402 if (!(js = journal_store_open(acc, FALSE, &err))) {
403 fprintf(stderr, "%s", err->message);
404 g_error_free(err);
405 exit(EXIT_FAILURE);
407 id = jam_account_id_strdup(acc);
408 g_print(_("Offline journal for '%s':\n"), id);
409 g_free(id);
410 lastsync = journal_store_get_lastsync(js);
411 g_print(_(" Last synced: %s.\n"), lastsync);
412 g_free(lastsync);
413 g_print(_(" Entry count: %d.\n"), journal_store_get_count(js));
414 g_print(_(" Latest itemid: %d.\n"), journal_store_get_latest_id(js));
415 exit(EXIT_SUCCESS);
419 static void do_offline_reindex (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
420 GError *err = NULL;
421 GETOPT_LOOP(OFFLINE_REINDEX)
422 GETOPT_LOOP_END
423 command_dispatch(cmdline, NULL, _(OFFLINE_REINDEX_HELP_TEXT), argc, argv);
424 g_print(_("Rebuilding index..."));
425 fflush(stdout);
426 if (!journal_store_reindex(acc, &err)) {
427 g_print(_(" ERROR: %s.\n"), err->message);
428 g_error_free(err);
429 exit(EXIT_FAILURE);
430 } else {
431 g_print(_("done.\n"));
432 exit(EXIT_SUCCESS);
437 static void do_offline (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
438 GETOPT_LOOP_SUBCOMMANDS(OFFLINE)
439 GETOPT_LOOP_SUBCOMMANDS_END(OFFLINE)
440 print_command_help(cmdline->curcmd, _(OFFLINE_HELP_TEXT), commands);
444 static void do_user_add (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
445 const char *username, *password = NULL;
446 gboolean remember_password = TRUE;
447 JamHost *host;
448 GETOPT_LOOP(USER_ADD)
449 case 'p': remember_password = FALSE; break;
450 GETOPT_LOOP_END
451 argc -= optind;
452 argv += optind;
453 if (argc < 1) {
454 g_print(_("Error: specify a user.\n"));
455 g_print(_(USER_ADD_HELP_TEXT));
456 exit(EXIT_FAILURE);
458 username = argv[0];
459 if (remember_password) {
460 if (argc < 2) {
461 password = getpass("Password: ");
462 } else {
463 password = argv[1];
466 conf_verify_a_host_exists();
467 if (conf.lasthost) {
468 host = conf.lasthost;
469 } else {
470 host = conf.hosts->data;
471 conf.lasthost = host;
473 acc = jam_host_get_account_by_username(host, username, /*create = */ TRUE);
474 if (password) jam_account_set_password(acc, password);
475 jam_account_set_remember(acc, TRUE, remember_password);
476 conf_write(&conf, app.conf_dir);
477 exit(EXIT_SUCCESS);
481 static void do_user (Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
482 GETOPT_LOOP_SUBCOMMANDS(USER)
483 GETOPT_LOOP_SUBCOMMANDS_END(USER)
484 print_command_help(cmdline->curcmd, _(USER_HELP_TEXT), commands);
488 JamAccount *cmdline_load_account (Cmdline *cmdline, gboolean existinguser, gboolean needpw) {
489 JamHost *host;
490 JamAccount *acc;
491 if (!cmdline->username && conf.lasthost && conf.lasthost->lastaccount && conf.lasthost->lastaccount->remember_password) {
492 acc = conf.lasthost->lastaccount;
493 } else {
494 if (!cmdline->username) return NULL;
495 if (conf.lasthost) host = conf.lasthost;
496 else if (conf.hosts) host = conf.hosts->data;
497 else return NULL;
498 acc = jam_host_get_account_by_username(host, cmdline->username, !existinguser);
500 if (!acc) {
501 g_printerr(_("Unknown account '%s'.\n"), cmdline->username);
502 return NULL;
504 if (cmdline->password) jam_account_set_password(acc, cmdline->password);
505 if (!jam_account_get_password(acc) && needpw) {
506 char *password;
507 if ((password = getpass(_("Password: "))) != NULL) jam_account_set_password(acc, password);
509 // XXX usejournal
510 // if (cmdline->postas)
511 // string_replace(&conf.usejournal, g_strdup(cmdline->postas));
512 /* if they're running a console command, we exit before we
513 * get a chance to write this new conf out. so we do it here. */
514 conf_write(&conf, app.conf_dir);
515 return acc;
519 gboolean cmdline_load_file (JamDoc *doc, char *filename, GError **err) {
520 if (strcmp(filename, "-") == 0) {
521 LJEntry *entry;
522 entry = lj_entry_new_from_file(stdin, LJ_ENTRY_FILE_AUTODETECT, NULL, err);
523 if (!entry) return FALSE;
524 jam_doc_load_entry(doc, entry);
525 lj_entry_free(entry);
526 return TRUE;
527 } else {
528 return jam_doc_load_file(doc, filename, LJ_ENTRY_FILE_AUTODETECT, err);
533 static void command_dispatch (Cmdline *cmdline, Command *commands, const char *help, int argc, gchar *argv[]) {
534 JamAccount *acc = NULL;
535 Command *command = NULL;
536 char *cmdname;
537 argc -= optind;
538 argv += optind;
539 cmdname = argv[0];
540 if (argc <= 0) return;
541 if (g_ascii_strcasecmp(cmdname, "help") == 0) {
542 g_print("%s", help);
543 exit(EXIT_SUCCESS);
545 for (int i = 0; commands && commands[i].cmdname; ++i) {
546 if (g_ascii_strcasecmp(cmdname, commands[i].cmdname) == 0) {
547 command = &commands[i];
548 break;
551 if (!command) {
552 g_printerr(_("Error: Unknown action '%s'.\n"), cmdname);
553 exit(EXIT_FAILURE);
555 cmdline->curcmd = command;
556 if (command->requireuser) {
557 acc = cmdline_load_account(cmdline, command->existinguser, command->needpw);
558 if (!acc) {
559 g_printerr(_("Error: Must specify account.\n"));
560 exit(EXIT_FAILURE);
562 jam_doc_set_account(cmdline->doc, acc);
564 command->action(cmdline, acc, argc, argv);
565 /* should terminate. */
566 g_error("not reached");
570 void cmdline_parse (JamDoc *doc, int argc, char *argv[]) {
571 Cmdline cmdline_real = {.doc = doc };
572 Cmdline *cmdline = &cmdline_real;
573 _GETOPT_LOOP_SUBCOMMANDS(LOGJAM)
574 case 'h': print_help(argv[0], commands); exit(EXIT_SUCCESS);
575 case 'v': print_header(); exit(EXIT_SUCCESS);
576 case 'u': cmdline->username = optarg; break;
577 case 'a': cmdline->postas = optarg; break;
578 case 'p': cmdline->password = optarg; break;
579 case 'e': cmdline->use_editor = TRUE; break;
580 case 'q': app.quiet = TRUE; break;
581 case 'f': cmdline_load_file(doc, optarg, NULL); break; /* XXX error */
582 GETOPT_LOOP_SUBCOMMANDS_END(LOGJAM)
583 /* if we get here, there wasn't a command to run. */
584 if (cmdline->username) {
585 JamAccount *acc;
586 acc = cmdline_load_account(cmdline, FALSE, TRUE);
587 jam_doc_set_account(doc, acc);
589 if (cmdline->use_editor) {
590 GError *err = NULL;
591 LJEntry *entry = jam_doc_get_entry(doc);
592 if (!lj_entry_edit_with_usereditor(entry, app.conf_dir, &err)) {
593 g_printerr("%s", err->message);
594 exit(EXIT_FAILURE);
596 jam_doc_load_entry(doc, entry);
597 lj_entry_free(entry);