it builds! (note that some autogenerated files are hardcoded now, and you can't build...
[k8lowj.git] / src / cmdline.c
blob4a30d63d6c9e2c1aa9f5203a5cd0bc5df3690e4f
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 */
7 #ifdef HAVE_GTK
8 #include "gtk-all.h"
9 #else
10 #include "glib-all.h"
11 #endif
13 #ifdef HAVE_REGEX_H
14 #include <sys/types.h>
15 #include <regex.h>
16 #endif
18 #ifdef HAVE_UNISTD_H
19 #include <unistd.h>
20 #endif
22 #include <string.h>
24 #include "liblj/consolecommand.h"
25 #include "liblj/login.h"
27 #include "getopt.h"
29 #include "conf_xml.h"
30 #include "journalstore.h"
31 #include "network.h"
32 #include "checkfriends.h"
33 #include "sync.h"
35 #include "jamdoc.h"
36 #include "cmdline.h"
37 #include "cmdline_data.h"
39 gboolean net_run_verb_ctx(LJVerb *verb, NetContext *ctx, GError **err);
42 typedef struct _Command Command;
44 typedef struct {
45 JamDoc *doc;
46 char *username, *password, *postas;
47 char *filename;
49 Command *curcmd;
51 gboolean use_editor;
52 } Cmdline;
54 struct _Command {
55 const char *cmdname; const char *desc;
56 gboolean requireuser, existinguser, needpw;
57 void (*action)(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]);
60 static void command_dispatch(Cmdline *cmdline, Command *commands, const char *help, int argc, gchar *argv[]);
62 #ifndef HAVE_UNISTD_H
63 /* this is a lame function. it isn't available on windows. */
64 static gchar*
65 getpass(const gchar* prompt) {
66 static gchar buf[64];
67 gint i;
68 g_print("%s", prompt);
69 for (i = 0; i < 64; i++) {
70 gint ch = getchar(); /* FIXME: this echoes */
71 if (ch == EOF || ch == (gint)'\n' || ch == (gint)'\r' /* win32 */)
72 break;
73 buf[i] = (gchar)ch;
75 buf[63] = '\0';
76 return buf;
78 #endif /* HAVE_UNISTD_H */
80 static void
81 print_header(void) {
82 g_print(_("LogJam %s\nCopyright (C) 2000-2004 Evan Martin\n\n"), PACKAGE_VERSION);
85 static void
86 list_commands(Command *commands) {
87 int maxlen = 0, len;
88 Command *c;
89 char *fmt;
91 for (c = commands; c->cmdname; c++) {
92 len = strlen(c->cmdname);
93 if (len > maxlen)
94 maxlen = len;
96 fmt = g_strdup_printf(" %%-%ds %%s\n", maxlen);
97 for (c = commands; c->cmdname; c++)
98 g_print(fmt, _(c->cmdname), _(c->desc));
99 g_free(fmt);
102 static void
103 help_commands(Command *commands) {
104 g_print(_("Subcommands:\n"));
105 list_commands(commands);
108 static void
109 print_help(char *argv0, Command *commands) {
110 print_header();
112 g_print(_("Use: %s [options] [command [commandargs]]\n"), argv0);
113 g_print("\n");
115 #ifdef HAVE_GTK
116 g_print(_("If no subcommand is provided, the default behavior is to load the GUI.\n"));
117 g_print("\n");
118 #endif
120 g_print(_(LOGJAM_HELP_TEXT));
122 help_commands(commands);
124 #ifdef HAVE_GTK
125 g_print("\n");
126 g_print(_("Also, GTK+ command line options (such as --display) can be used.\n"));
127 #endif
130 static void
131 print_command_help(Command *cmd, const char *helptext, Command *subcmds) {
132 g_print("%s -- %s", cmd->cmdname, helptext);
133 if (subcmds)
134 help_commands(subcmds);
135 exit(EXIT_SUCCESS);
138 #define OPT_LIST_SUBCOMMANDS 1
140 #define _GETOPT_LOOP(name) \
142 const char *short_options = name ## _SHORT_OPTIONS; \
143 struct option long_options[] = name ## _LONG_OPTIONS; \
144 optind = 0; \
145 for (;;) { \
146 int c = getopt_long(argc, argv, short_options, long_options, NULL); \
147 if (c == -1) break; \
149 switch (c) { \
150 case '?': /* unknown option character. */ \
151 case ':': /* missing option character. */ \
152 g_printerr(_("Unknown or missing option character\n")); \
153 exit(EXIT_FAILURE);
155 #define GETOPT_LOOP_END \
160 #define _GETOPT_LOOP_SUBCOMMANDS(name) \
161 Command commands[] = name ## _SUBCOMMANDS; \
162 _GETOPT_LOOP(name) \
163 case OPT_LIST_SUBCOMMANDS: \
164 list_commands(commands); \
165 exit(EXIT_SUCCESS);
167 #define GETOPT_LOOP_SUBCOMMANDS_END(name) \
168 GETOPT_LOOP_END \
169 command_dispatch(cmdline, commands, name ## _HELP_TEXT, argc, argv);
171 #define GETOPT_LOOP(name) \
172 _GETOPT_LOOP(name) \
173 case 'h': \
174 print_command_help(cmdline->curcmd, \
175 _(name ## _HELP_TEXT), NULL);
177 #define GETOPT_LOOP_SUBCOMMANDS(name) \
178 _GETOPT_LOOP_SUBCOMMANDS(name) \
179 case 'h': \
180 print_command_help(cmdline->curcmd, \
181 _(name ## _HELP_TEXT), commands);
184 static void
185 do_console(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
186 GString *command = g_string_new(NULL);
187 LJConsoleCommand *cc;
188 GError *err = NULL;
189 int i;
191 GETOPT_LOOP(CONSOLE)
192 GETOPT_LOOP_END
194 if (!JAM_ACCOUNT_IS_LJ(acc)) {
195 g_printerr(_("Blogger accounts do not have a console interface.\n"));
196 exit(EXIT_FAILURE);
199 argc--; argv++; /* skip 'console' */
200 if (argc <= 0) {
201 g_printerr(_("Must specify a console command. Try \"help\".\n"));
202 exit(EXIT_FAILURE);
204 while (argc--)
205 if (argc)
206 g_string_append_printf(command, "%s ", argv++[0]);
207 else
208 g_string_append(command, argv++[0]);
210 cc = lj_consolecommand_new(jam_account_lj_get_user(JAM_ACCOUNT_LJ(acc)),
211 command->str);
212 g_string_free(command, TRUE);
214 if (!net_run_verb_ctx((LJVerb*)cc, network_ctx_cmdline, &err)) {
215 g_print("Error: %s\n", err->message);
216 g_error_free(err);
217 exit(EXIT_FAILURE);
220 for (i = 0; i < cc->linecount; i++) {
221 g_print("%s\n", cc->lines[i].text);
224 lj_consolecommand_free(cc);
226 exit(EXIT_SUCCESS);
229 static void
230 do_post(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
231 gboolean use_editor = FALSE;
233 GETOPT_LOOP(POST)
234 case 'e':
235 use_editor = TRUE;
236 break;
237 GETOPT_LOOP_END
239 if (!jam_host_do_post(jam_account_get_host(acc),
240 network_ctx_cmdline, cmdline->doc, NULL)) {
241 exit(EXIT_FAILURE);
243 if (!app.quiet)
244 g_print(_("Success.\n"));
245 exit(EXIT_SUCCESS);
248 static void
249 do_checkfriends(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
250 JamAccountLJ *acclj;
251 char *subcommand = argc > 1 ? argv[1] : NULL;
253 app.cli = TRUE;
255 GETOPT_LOOP(CHECKFRIENDS)
256 GETOPT_LOOP_END
258 if (!JAM_ACCOUNT_IS_LJ(acc)) {
259 g_printerr(_("Checkfriends is only supported by LiveJournal accounts.\n"));
260 exit(EXIT_FAILURE);
262 acclj = JAM_ACCOUNT_LJ(acc);
264 if (!subcommand) {
265 if (checkfriends_cli(acclj)) {
266 g_print("1\n"); exit(EXIT_SUCCESS); /* NEW */
267 } else {
268 g_print("0\n"); exit(EXIT_FAILURE); /* no new entries, or some error */
270 } else if (!g_ascii_strcasecmp("purge", subcommand)) {
271 char *id = jam_account_id_strdup(acc);
272 checkfriends_cli_purge(acclj);
273 g_print(_("Checkfriends information for %s purged.\n"), id);
274 g_free(id);
275 exit(EXIT_SUCCESS);
276 } else {
277 g_printerr(_("Unknown argument %s to --checkfriends\n"), subcommand);
278 exit(EXIT_FAILURE);
282 static void
283 do_offline_sync(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
284 GETOPT_LOOP(OFFLINE_SYNC)
285 GETOPT_LOOP_END
287 app.cli = TRUE;
288 if (JAM_ACCOUNT_IS_LJ(acc)) {
289 if (sync_run(JAM_ACCOUNT_LJ(acc), NULL))
290 exit(EXIT_SUCCESS);
291 } else {
292 g_printerr(_("Sync is only supported by LiveJournal accounts.\n"));
294 exit(EXIT_FAILURE);
297 static void
298 do_offline_cat(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
299 LJEntryFileType output_type = LJ_ENTRY_FILE_RFC822;
300 JournalStore *js = NULL;
301 GError *err = NULL;
302 GString *text;
303 gint counter = 0; /* how many entries written. for rfc822 separator. */
304 gboolean mailbox = FALSE;
306 GETOPT_LOOP(OFFLINE_CAT)
307 case 'm':
308 mailbox = TRUE;
309 break;
310 case 't':
311 output_type = LJ_ENTRY_FILE_RFC822;
312 break;
313 case 'x':
314 output_type = LJ_ENTRY_FILE_XML;
315 break;
316 GETOPT_LOOP_END
318 if (optind >= argc) {
319 fprintf(stderr,
320 _("Specify at least one itemid or 'latest'.\n"));
321 exit(EXIT_FAILURE);
323 if (!(js = journal_store_open(acc, FALSE, &err))) {
324 fprintf(stderr,
325 "%s", err->message);
326 g_error_free(err);
327 exit(EXIT_FAILURE);
329 while (optind < argc) {
330 gchar snum[16];
331 gint num = atoi(argv[optind]);
332 LJEntry *entry;
334 g_snprintf(snum, 16, "%d", num);
335 if (strncmp(snum, argv[optind], 16)) {
336 if (strncmp(argv[optind], "latest", 7)) {
337 fprintf(stderr,
338 _("expected itemid, got %s\n"), argv[optind]);
339 exit(EXIT_FAILURE);
340 } else {
341 num = journal_store_get_latest_id(js);
344 if (!(entry = journal_store_get_entry(js, num))) {
345 fprintf(stderr,
346 _("entry %d not found\n"), num);
347 exit(EXIT_FAILURE);
349 switch (output_type) {
350 case LJ_ENTRY_FILE_RFC822:
351 if (counter++)
352 /* entry separator lead-in: *two* newlines unlike RFC822
353 * email, so that later on in reading a entrybox we can
354 * tell an entry doesn't end with a newline. */
355 g_print("\n\n");
356 text = lj_entry_to_rfc822(entry, FALSE);
357 if (mailbox) {
358 time_t now = time(NULL);
359 g_print("From %s@%s %s"
360 "X-LogJam-Itemid: %d\n"
361 "%s",
362 jam_account_get_username(acc),
363 jam_account_get_host(acc)->name,
364 ctime(&now),
365 num, text->str);
366 } else {
367 g_print("X-LogJam-Itemid: %d\n"
368 "X-LogJam-User: %s\n"
369 "X-LogJam-Server: %s\n"
370 "%s",
371 num, jam_account_get_username(acc),
372 jam_account_get_host(acc)->name, text->str);
374 g_string_free(text, TRUE);
375 break;
376 case LJ_ENTRY_FILE_XML:
377 default:
378 fprintf(stderr, "(xml cat not yet implemented.)\n");
379 exit(EXIT_FAILURE);
381 lj_entry_free(entry);
382 optind++;
384 exit(EXIT_SUCCESS);
387 #ifdef HAVE_REGEX_H
388 /* from regexp documentation in glibc */
389 static char *get_regerror (int errcode, regex_t *compiled)
391 size_t length = regerror (errcode, compiled, NULL, 0);
392 char *buffer = g_new (char, length);
393 (void) regerror (errcode, compiled, buffer, length);
394 return buffer;
396 #endif /* HAVE_REGEX_H */
398 static void
399 do_offline_grep(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
400 #ifdef HAVE_REGEX_H
401 LJEntryFileType output_type = LJ_ENTRY_FILE_RFC822;
402 JournalStore *js = NULL;
403 GError *err = NULL;
404 GString *text;
405 gint counter = 0; /* how many entries written. for rfc822 separator. */
406 gint cflags = 0;
407 gint res;
408 gint latest, itemid;
409 regex_t regexp;
411 GETOPT_LOOP(OFFLINE_GREP)
412 case 't':
413 output_type = LJ_ENTRY_FILE_RFC822;
414 break;
415 case 'x':
416 output_type = LJ_ENTRY_FILE_XML;
417 break;
418 case 'e':
419 cflags |= REG_EXTENDED;
420 break;
421 case 'i':
422 cflags |= REG_ICASE;
423 break;
424 GETOPT_LOOP_END
426 if (optind >= argc) {
427 g_printerr(_("Specify a regexp to match\n"));
428 exit(EXIT_FAILURE);
430 if (!(js = journal_store_open(acc, FALSE, &err))) {
431 g_printerr("%s", err->message);
432 g_error_free(err);
433 exit(EXIT_FAILURE);
435 if ((res = regcomp(&regexp, argv[optind], cflags | REG_NOSUB))) {
436 gchar *re_error = get_regerror(res, &regexp);
437 g_printerr(_("Regexp error: %s.\n"), re_error);
438 g_free(re_error);
439 exit(EXIT_FAILURE);
442 latest = journal_store_get_latest_id(js);
443 for (itemid = 1; itemid <= latest; itemid++) {
444 LJEntry *entry;
446 if (!(entry = journal_store_get_entry(js, itemid)))
447 continue; /* deleted item */
449 // XXX body only
450 switch (regexec(&regexp, entry->event, 0, NULL, 0)) {
451 case REG_NOMATCH:
452 continue; /* item does not match */
453 case REG_ESPACE:
454 g_printerr(_("Regexp error: out of memory.\n"));
455 exit(EXIT_FAILURE);
456 default:
457 ; /* proceed */
460 switch (output_type) {
461 case LJ_ENTRY_FILE_RFC822:
462 if (counter++)
463 /* entry separator lead-in: *two* newlines unlike RFC822
464 * email, so that later on in reading a entrybox we can
465 * tell an entry doesn't end with a newline. */
466 g_print("\n\n");
467 text = lj_entry_to_rfc822(entry, FALSE);
468 g_print("X-LogJam-Itemid: %d\n"
469 "X-LogJam-User: %s\n"
470 "X-LogJam-Server: %s\n"
471 "%s",
472 itemid, jam_account_get_username(acc),
473 jam_account_get_host(acc)->name, text->str);
474 g_string_free(text, TRUE);
475 break;
476 case LJ_ENTRY_FILE_XML:
477 default:
478 g_printerr("Not yet implemented.\n");
479 exit(EXIT_FAILURE);
481 lj_entry_free(entry);
483 exit(EXIT_SUCCESS);
484 #else /* HAVE_REGEX_H */
485 g_printerr(_("LogJam was compiled without regular expression support.\n"));
486 exit(EXIT_FAILURE);
487 #endif /* HAVE_REGEX_H */
490 static void
491 do_offline_summary(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
492 GError *err = NULL;
493 JournalStore *js = NULL;
494 char *id, *lastsync;
496 GETOPT_LOOP(OFFLINE_SUMMARY)
497 GETOPT_LOOP_END
498 command_dispatch(cmdline, NULL, _(OFFLINE_SUMMARY_HELP_TEXT), argc, argv);
500 if (!(js = journal_store_open(acc, FALSE, &err))) {
501 fprintf(stderr,
502 "%s", err->message);
503 g_error_free(err);
504 exit(EXIT_FAILURE);
507 id = jam_account_id_strdup(acc);
508 g_print(_("Offline journal for '%s':\n"), id);
509 g_free(id);
511 lastsync = journal_store_get_lastsync(js);
512 g_print(_(" Last synced: %s.\n"), lastsync);
513 g_free(lastsync);
515 g_print(_(" Entry count: %d.\n"), journal_store_get_count(js));
516 g_print(_(" Latest itemid: %d.\n"), journal_store_get_latest_id(js));
517 exit(EXIT_SUCCESS);
520 static void
521 do_offline_reindex(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
522 GError *err = NULL;
524 GETOPT_LOOP(OFFLINE_REINDEX)
525 GETOPT_LOOP_END
526 command_dispatch(cmdline, NULL, _(OFFLINE_REINDEX_HELP_TEXT), argc, argv);
528 g_print(_("Rebuilding index..."));
529 fflush(stdout);
530 if (!journal_store_reindex(acc, &err)) {
531 g_print(_(" ERROR: %s.\n"), err->message);
532 g_error_free(err);
533 exit(EXIT_FAILURE);
534 } else {
535 g_print(_("done.\n"));
536 exit(EXIT_SUCCESS);
540 static void
541 do_offline(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
542 GETOPT_LOOP_SUBCOMMANDS(OFFLINE)
543 GETOPT_LOOP_SUBCOMMANDS_END(OFFLINE)
544 print_command_help(cmdline->curcmd, _(OFFLINE_HELP_TEXT), commands);
548 static void
549 do_user_add(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
550 const char *username, *password = NULL;
551 gboolean remember_password = TRUE;
552 JamHost *host;
554 GETOPT_LOOP(USER_ADD)
555 case 'p':
556 remember_password = FALSE;
557 break;
558 GETOPT_LOOP_END
560 argc -= optind;
561 argv += optind;
563 if (argc < 1) {
564 g_print(_("Error: specify a user.\n"));
565 g_print(_(USER_ADD_HELP_TEXT));
566 exit(EXIT_FAILURE);
568 username = argv[0];
570 if (remember_password) {
571 if (argc < 2) {
572 password = getpass("Password: ");
573 } else {
574 password = argv[1];
578 conf_verify_a_host_exists();
579 if (conf.lasthost) {
580 host = conf.lasthost;
581 } else {
582 host = conf.hosts->data;
583 conf.lasthost = host;
586 acc = jam_host_get_account_by_username(host, username, /*create = */TRUE);
587 if (password)
588 jam_account_set_password(acc, password);
589 jam_account_set_remember(acc, TRUE, remember_password);
590 conf_write(&conf, app.conf_dir);
591 exit(EXIT_SUCCESS);
594 static void
595 do_user(Cmdline *cmdline, JamAccount *acc, gint argc, gchar *argv[]) {
596 GETOPT_LOOP_SUBCOMMANDS(USER)
597 GETOPT_LOOP_SUBCOMMANDS_END(USER)
598 print_command_help(cmdline->curcmd, _(USER_HELP_TEXT), commands);
601 JamAccount*
602 cmdline_load_account(Cmdline *cmdline, gboolean existinguser, gboolean needpw) {
603 JamHost *host;
604 JamAccount *acc;
606 if (!cmdline->username &&
607 conf.lasthost && conf.lasthost->lastaccount &&
608 conf.lasthost->lastaccount->remember_password) {
609 acc = conf.lasthost->lastaccount;
610 } else {
611 if (!cmdline->username)
612 return NULL;
614 if (conf.lasthost)
615 host = conf.lasthost;
616 else if (conf.hosts)
617 host = conf.hosts->data;
618 else
619 return NULL;
621 acc = jam_host_get_account_by_username(host, cmdline->username,
622 !existinguser);
625 if (!acc) {
626 g_printerr(_("Unknown account '%s'.\n"), cmdline->username);
627 return NULL;
630 if (cmdline->password)
631 jam_account_set_password(acc, cmdline->password);
632 if (!jam_account_get_password(acc) && needpw) {
633 char *password;
634 if ((password = getpass(_("Password: "))) != NULL)
635 jam_account_set_password(acc, password);
638 // XXX usejournal
639 // if (cmdline->postas)
640 // string_replace(&conf.usejournal, g_strdup(cmdline->postas));
642 /* if they're running a console command, we exit before we
643 * get a chance to write this new conf out. so we do it here. */
644 conf_write(&conf, app.conf_dir);
646 return acc;
649 gboolean
650 cmdline_load_file(JamDoc *doc, char *filename, GError **err) {
651 if (strcmp(filename, "-") == 0) {
652 LJEntry *entry;
653 entry = lj_entry_new_from_file(stdin,
654 LJ_ENTRY_FILE_AUTODETECT, NULL, err);
655 if (!entry) return FALSE;
656 jam_doc_load_entry(doc, entry);
657 lj_entry_free(entry);
658 return TRUE;
659 } else {
660 return jam_doc_load_file(doc, filename, LJ_ENTRY_FILE_AUTODETECT, err);
664 static void
665 command_dispatch(Cmdline *cmdline, Command *commands, const char *help, int argc, gchar *argv[]) {
666 JamAccount *acc;
667 Command *command = NULL;
668 char *cmdname;
669 int i;
671 argc -= optind;
672 argv += optind;
673 cmdname = argv[0];
675 if (argc <= 0)
676 return;
678 if (g_ascii_strcasecmp(cmdname, "help") == 0) {
679 g_print(help);
680 exit(EXIT_SUCCESS);
682 for (i = 0; commands && commands[i].cmdname; i++) {
683 if (g_ascii_strcasecmp(cmdname, commands[i].cmdname) == 0) {
684 command = &commands[i];
685 break;
688 if (!command) {
689 g_printerr(_("Error: Unknown action '%s'.\n"), cmdname);
690 exit(EXIT_FAILURE);
692 cmdline->curcmd = command;
693 if (command->requireuser) {
694 acc = cmdline_load_account(cmdline,
695 command->existinguser, command->needpw);
696 if (!acc) {
697 g_printerr(_("Error: Must specify account.\n"));
698 exit(EXIT_FAILURE);
700 jam_doc_set_account(cmdline->doc, acc);
702 command->action(cmdline, acc, argc, argv);
703 /* should terminate. */
704 g_error("not reached");
707 void
708 cmdline_parse(JamDoc *doc, int argc, char *argv[]) {
709 Cmdline cmdline_real = { .doc = doc };
710 Cmdline *cmdline = &cmdline_real;
712 _GETOPT_LOOP_SUBCOMMANDS(LOGJAM)
713 case 'h':
714 print_help(argv[0], commands);
715 exit(EXIT_SUCCESS);
716 case 'v':
717 print_header();
718 exit(EXIT_SUCCESS);
719 case 'u':
720 cmdline->username = optarg;
721 break;
722 case 'a':
723 cmdline->postas = optarg;
724 break;
725 case 'p':
726 cmdline->password = optarg;
727 break;
728 case 'e':
729 cmdline->use_editor = TRUE;
730 break;
731 case 'q':
732 app.quiet = TRUE;
733 break;
734 case 'f':
735 cmdline_load_file(doc, optarg, NULL); /* XXX error */
736 break;
737 GETOPT_LOOP_SUBCOMMANDS_END(LOGJAM)
739 /* if we get here, there wasn't a command to run. */
740 if (cmdline->username) {
741 JamAccount *acc;
742 acc = cmdline_load_account(cmdline, FALSE, TRUE);
743 jam_doc_set_account(doc, acc);
745 if (cmdline->use_editor) {
746 GError *err = NULL;
747 LJEntry *entry = jam_doc_get_entry(doc);
748 if (!lj_entry_edit_with_usereditor(entry, app.conf_dir, &err)) {
749 g_printerr("%s", err->message);
750 exit(EXIT_FAILURE);
752 jam_doc_load_entry(doc, entry);
753 lj_entry_free(entry);