/* * whatis.c: search the index or whatis database(s) for words. * * Copyright (C) 1994, 1995 Graeme W. Wilford. (Wilf.) * Copyright (C) 2001, 2002, 2003, 2004, 2006, 2007, 2008, 2009, 2010, 2011, * 2012 Colin Watson. * * This file is part of man-db. * * man-db is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * man-db is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with man-db; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * routines for whatis and apropos programs. Whatis looks up the * keyword for the description, apropos searches the entire database * for word matches. * * Mon Aug 8 20:35:30 BST 1994 Wilf. (G.Wilford@ee.surrey.ac.uk) * * CJW: Add more safety in the face of corrupted databases. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif /* HAVE_CONFIG_H */ #include #include #include #include #include #include #include "gettext.h" #include #define _(String) gettext (String) #define N_(String) gettext_noop (String) #ifdef HAVE_ICONV # include #endif /* HAVE_ICONV */ #include #include #include "regex.h" #include "argp.h" #include "dirname.h" #include "fnmatch.h" #include "progname.h" #include "xvasprintf.h" #include "manconfig.h" #include "cleanup.h" #include "error.h" #include "pipeline.h" #include "pathsearch.h" #include "linelength.h" #include "hashtable.h" #include "lower.h" #include "wordfnmatch.h" #include "xregcomp.h" #include "encodings.h" #include "sandbox.h" #include "mydbm.h" #include "db_storage.h" #include "manp.h" static char *manpathlist[MAXDIRS]; extern char *user_config_file; static char **keywords; static int num_keywords; int am_apropos; char *database; int quiet = 1; man_sandbox *sandbox; #ifdef HAVE_ICONV iconv_t conv_to_locale; #endif /* HAVE_ICONV */ static regex_t *preg; static int regex_opt; static int exact; static int wildcard; static int require_all; static int long_output; static char **sections; static char *manp = NULL; static const char *alt_systems = ""; static const char *locale = NULL; static char *multiple_locale = NULL, *internal_locale; static struct hashtable *display_seen = NULL; const char *argp_program_version; /* initialised in main */ const char *argp_program_bug_address = PACKAGE_BUGREPORT; error_t argp_err_exit_status = FAIL; static const char args_doc[] = N_("KEYWORD..."); static const char apropos_doc[] = "\v" N_("The --regex option is enabled by default."); static struct argp_option options[] = { { "debug", 'd', 0, 0, N_("emit debugging messages") }, { "verbose", 'v', 0, 0, N_("print verbose warning messages") }, { "regex", 'r', 0, 0, N_("interpret each keyword as a regex"), 10 }, { "exact", 'e', 0, 0, N_("search each keyword for exact match") }, /* apropos only */ { "wildcard", 'w', 0, 0, N_("the keyword(s) contain wildcards") }, { "and", 'a', 0, 0, N_("require all keywords to match"), 20 }, /* apropos only */ { "long", 'l', 0, 0, N_("do not trim output to terminal width"), 30 }, { "sections", 's', N_("LIST"), 0, N_("search only these sections (colon-separated)"), 40 }, { "section", 0, 0, OPTION_ALIAS }, { "systems", 'm', N_("SYSTEM"), 0, N_("use manual pages from other systems") }, { "manpath", 'M', N_("PATH"), 0, N_("set search path for manual pages to PATH") }, { "locale", 'L', N_("LOCALE"), 0, N_("define the locale for this search") }, { "config-file", 'C', N_("FILE"), 0, N_("use this user configuration file") }, { "whatis", 'f', 0, OPTION_HIDDEN, 0 }, { "apropos", 'k', 0, OPTION_HIDDEN, 0 }, { 0, 'h', 0, OPTION_HIDDEN, 0 }, /* compatibility for --help */ { 0 } }; static char **split_sections (const char *sections_str) { int i = 0; char *str = xstrdup (sections_str); const char *section; char **out = NULL; /* Although this is documented as colon-separated, at least Solaris * man's -s option takes a comma-separated list, so we accept that * too for compatibility. */ for (section = strtok (str, ":,"); section; section = strtok (NULL, ":,")) { out = xnrealloc (out, i + 2, sizeof *out); out[i++] = xstrdup (section); } if (i) out[i] = NULL; free (str); return out; } static error_t parse_opt (int key, char *arg, struct argp_state *state) { switch (key) { case 'd': debug_level = 1; return 0; case 'v': quiet = 0; return 0; case 'r': regex_opt = 1; return 0; case 'e': /* Only makes sense for apropos, but has * historically been accepted by whatis anyway. */ regex_opt = 0; exact = 1; return 0; case 'w': regex_opt = 0; wildcard = 1; return 0; case 'a': if (am_apropos) require_all = 1; else argp_usage (state); return 0; case 'l': long_output = 1; return 0; case 's': sections = split_sections (arg); return 0; case 'm': alt_systems = arg; return 0; case 'M': manp = xstrdup (arg); return 0; case 'L': locale = arg; return 0; case 'C': user_config_file = arg; return 0; case 'f': /* helpful override if program name detection fails */ am_apropos = 0; return 0; case 'k': /* helpful override if program name detection fails */ am_apropos = 1; return 0; case 'h': argp_state_help (state, state->out_stream, ARGP_HELP_STD_HELP & ~ARGP_HELP_PRE_DOC); break; case ARGP_KEY_ARGS: keywords = state->argv + state->next; num_keywords = state->argc - state->next; return 0; case ARGP_KEY_NO_ARGS: /* Make sure that we have a keyword! */ printf (_("%s what?\n"), program_name); exit (FAIL); case ARGP_KEY_SUCCESS: if (am_apropos && !exact && !wildcard) regex_opt = 1; return 0; } return ARGP_ERR_UNKNOWN; } static char *help_filter (int key, const char *text, void *input ATTRIBUTE_UNUSED) { switch (key) { case ARGP_KEY_HELP_PRE_DOC: /* We have no pre-options help text, but the input * text may contain header junk due to gettext (""). */ return NULL; default: return (char *) text; } } static struct argp apropos_argp = { options, parse_opt, args_doc, apropos_doc, 0, help_filter }; static struct argp whatis_argp = { options, parse_opt, args_doc }; static char *locale_manpath (const char *manpath) { char *all_locales; char *new_manpath; if (multiple_locale && *multiple_locale) { if (internal_locale && *internal_locale) all_locales = xasprintf ("%s:%s", multiple_locale, internal_locale); else all_locales = xstrdup (multiple_locale); } else { if (internal_locale && *internal_locale) all_locales = xstrdup (internal_locale); else all_locales = NULL; } new_manpath = add_nls_manpaths (manpath, all_locales); free (all_locales); return new_manpath; } #ifdef HAVE_ICONV static char *simple_convert (iconv_t conv, char *string) { if (conv != (iconv_t) -1) { size_t string_conv_alloc = strlen (string) + 1; char *string_conv = xmalloc (string_conv_alloc); for (;;) { char *inptr = string, *outptr = string_conv; size_t inleft = strlen (string); size_t outleft = string_conv_alloc - 1; if (iconv (conv, (ICONV_CONST char **) &inptr, &inleft, &outptr, &outleft) == (size_t) -1 && errno == E2BIG) { string_conv_alloc <<= 1; string_conv = xrealloc (string_conv, string_conv_alloc); } else { /* Either we succeeded, or we've done our * best; go ahead and print what we've got. */ string_conv[string_conv_alloc - 1 - outleft] = '\0'; break; } } return string_conv; } else return xstrdup (string); } #else /* !HAVE_ICONV */ # define simple_convert(conv, string) xstrdup (string) #endif /* HAVE_ICONV */ /* Do the old thing, if we cannot find the relevant database. * This invokes grep once per argument; we can't do much about this because * we need to know which arguments failed. The only way to speed this up * would be to implement grep internally, but it hardly seems worth it for a * legacy mechanism. */ static void use_grep (const char * const *pages, int num_pages, char *manpath, int *found) { char *whatis_file = xasprintf ("%s/whatis", manpath); if (CAN_ACCESS (whatis_file, R_OK)) { const char *flags; int i; if (am_apropos) { if (regex_opt) flags = get_def_user ( "apropos_regex_grep_flags", APROPOS_REGEX_GREP_FLAGS); else flags = get_def_user ("apropos_grep_flags", APROPOS_GREP_FLAGS); } else flags = get_def_user ("whatis_grep_flags", WHATIS_GREP_FLAGS); for (i = 0; i < num_pages; ++i) { pipeline *grep_pl; pipecmd *grep_cmd; char *anchored_page; if (am_apropos) anchored_page = xstrdup (pages[i]); else anchored_page = xasprintf ("^%s", pages[i]); grep_cmd = pipecmd_new_argstr (get_def_user ("grep", GREP)); pipecmd_argstr (grep_cmd, flags); pipecmd_args (grep_cmd, anchored_page, whatis_file, (void *) 0); pipecmd_pre_exec (grep_cmd, sandbox_load, sandbox_free, sandbox); grep_pl = pipeline_new_commands (grep_cmd, (void *) 0); if (pipeline_run (grep_pl) == 0) found[i] = 1; free (anchored_page); } } else debug ("warning: can't read the fallback whatis text database " "%s/whatis\n", manpath); free (whatis_file); } static struct mandata *resolve_pointers (MYDBM_FILE dbf, struct mandata *info, const char *page) { int rounds; const char *newpage; if (*(info->pointer) == '-' || ((!info->name || STREQ (info->name, page)) && STREQ (info->pointer, page))) return info; /* Now we have to work through pointers. The limit of 10 is fairly * arbitrary: it's just there to avoid an infinite loop. */ newpage = info->pointer; info = dblookup_exact (dbf, newpage, info->ext, 1); for (rounds = 0; rounds < 10; rounds++) { struct mandata *newinfo; /* If the pointer lookup fails, do nothing. */ if (!info) return NULL; if (*(info->pointer) == '-' || ((!info->name || STREQ (info->name, newpage)) && STREQ (info->pointer, newpage))) return info; newinfo = dblookup_exact (dbf, info->pointer, info->ext, 1); free_mandata_struct (info); info = newinfo; } if (!quiet) error (0, 0, _("warning: %s contains a pointer loop"), page); return NULL; } /* fill_in_whatis() is really a ../libdb/db_lookup.c routine but whatis.c is the only file that actually requires access to the whatis text... */ /* Take mandata struct (earlier returned from a dblookup()) and return the relative whatis */ static char *get_whatis (struct mandata *info, const char *page) { if (!info) return xstrdup (_("(unknown subject)")); /* See if we need to fill in the whatis here. */ if (info->whatis != NULL && *(info->whatis)) return xstrdup (info->whatis); if (!quiet && *(info->pointer) != '-') error (0, 0, _("warning: %s contains a pointer loop"), page); return xstrdup (_("(unknown subject)")); } /* print out any matches found */ static void display (MYDBM_FILE dbf, struct mandata *info, const char *page) { struct mandata *newinfo; char *string, *whatis, *string_conv; const char *page_name; char *key; int line_len, rest; newinfo = resolve_pointers (dbf, info, page); whatis = get_whatis (newinfo, page); if (newinfo == NULL) newinfo = info; dbprintf (newinfo); if (newinfo->name) page_name = newinfo->name; else page_name = page; key = xasprintf ("%s (%s)", page_name, newinfo->ext); if (hashtable_lookup_structure (display_seen, key, strlen (key))) goto out; hashtable_install (display_seen, key, strlen (key), NULL); line_len = get_line_length (); if (!long_output && strlen (page_name) > (size_t) (line_len / 2)) string = xasprintf ("%.*s...", line_len / 2 - 3, page_name); else string = xstrdup (page_name); string = appendstr (string, " (", newinfo->ext, ")", (void *) 0); if (!STREQ (newinfo->pointer, "-") && !STREQ (newinfo->pointer, page)) string = appendstr (string, " [", newinfo->pointer, "]", (void *) 0); if (strlen (string) < (size_t) 20) { int i; string = xrealloc (string, 21); for (i = strlen (string); i < 20; ++i) string[i] = ' '; string[i] = '\0'; } string = appendstr (string, " - ", (void *) 0); rest = line_len - strlen (string); if (!long_output && strlen (whatis) > (size_t) rest) { whatis[rest - 3] = '\0'; string = appendstr (string, whatis, "...\n", (void *) 0); } else string = appendstr (string, whatis, "\n", (void *) 0); string_conv = simple_convert (conv_to_locale, string); fputs (string_conv, stdout); free (string_conv); free (string); out: free (key); free (whatis); if (newinfo != info) free_mandata_struct (newinfo); } /* lookup the page and display the results */ static int do_whatis_section (MYDBM_FILE dbf, const char *page, const char *section) { struct mandata *info; int count = 0; info = dblookup_all (dbf, page, section, 0); while (info) { struct mandata *pinfo; display (dbf, info, page); count++; pinfo = info->next; /* go on to next structure */ free_mandata_elements (info); free (info); info = pinfo; } return count; } static int suitable_manpath (const char *manpath, const char *page_dir) { char *page_manp, *pm; char *page_manpathlist[MAXDIRS], **mp; int ret; page_manp = get_manpath_from_path (page_dir, 0); if (!page_manp || !*page_manp) { free (page_manp); return 0; } pm = locale_manpath (page_manp); free (page_manp); page_manp = pm; create_pathlist (page_manp, page_manpathlist); ret = 0; for (mp = page_manpathlist; *mp; ++mp) { if (STREQ (*mp, manpath)) { ret = 1; break; } } for (mp = page_manpathlist; *mp; ++mp) free (*mp); free (page_manp); return ret; } static void do_whatis (MYDBM_FILE dbf, const char * const *pages, int num_pages, const char *manpath, int *found) { int i; for (i = 0; i < num_pages; ++i) { char *page = xstrdup (pages[i]); struct stat st; if (strchr (page, '/') && stat (page, &st) == 0 && !S_ISDIR (st.st_mode) && st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) { /* Perhaps an executable. If its directory is on * $PATH, then we only want to process this page for * matching manual hierarchies. */ char *page_dir = dir_name (page); if (directory_on_path (page_dir)) { if (suitable_manpath (manpath, page_dir)) { char *old_page = page; page = base_name (old_page); free (old_page); } else { debug ("%s not on manpath for %s\n", manpath, page); free (page_dir); free (page); continue; } } free (page_dir); } if (sections) { char * const *section; for (section = sections; *section; ++section) { if (do_whatis_section (dbf, page, *section)) found[i] = 1; } } else { if (do_whatis_section (dbf, page, NULL)) found[i] = 1; } free (page); } } static int any_set (int num_pages, int *found_here) { int i; for (i = 0; i < num_pages; ++i) if (found_here[i]) return 1; return 0; } static int all_set (int num_pages, int *found_here) { int i; for (i = 0; i < num_pages; ++i) if (!found_here[i]) return 0; return 1; } static void parse_name (const char * const *pages, int num_pages, const char *dbname, int *found, int *found_here) { int i; if (regex_opt) { for (i = 0; i < num_pages; ++i) { if (regexec (&preg[i], dbname, 0, (regmatch_t *) 0, 0) == 0) found[i] = found_here[i] = 1; } return; } if (am_apropos && !wildcard) { char *lowdbname = lower (dbname); for (i = 0; i < num_pages; ++i) { if (STREQ (lowdbname, pages[i])) found[i] = found_here[i] = 1; } free (lowdbname); return; } for (i = 0; i < num_pages; ++i) { if (fnmatch (pages[i], dbname, 0) == 0) found[i] = found_here[i] = 1; } } /* return 1 on word match */ static int match (const char *lowpage, const char *whatis) { char *lowwhatis = lower (whatis); size_t len = strlen (lowpage); char *p, *begin; begin = lowwhatis; /* check for string match, then see if it is a _word_ */ while (lowwhatis && (p = strstr (lowwhatis, lowpage))) { char *left = p - 1; char *right = p + len; if ((p == begin || (!CTYPE (islower, *left) && *left != '_')) && (!*right || (!CTYPE (islower, *right) && *right != '_'))) { free (begin); return 1; } lowwhatis = p + 1; } free (begin); return 0; } static void parse_whatis (const char * const *pages, char * const *lowpages, int num_pages, const char *whatis, int *found, int *found_here) { int i; if (regex_opt) { for (i = 0; i < num_pages; ++i) { if (regexec (&preg[i], whatis, 0, (regmatch_t *) 0, 0) == 0) found[i] = found_here[i] = 1; } return; } if (wildcard) { for (i = 0; i < num_pages; ++i) { if (exact) { if (fnmatch (pages[i], whatis, 0) == 0) found[i] = found_here[i] = 1; } else { if (word_fnmatch (pages[i], whatis)) found[i] = found_here[i] = 1; } } return; } for (i = 0; i < num_pages; ++i) { if (match (lowpages[i], whatis)) found[i] = found_here[i] = 1; } } /* cjwatson: Optimized functions don't seem to be correct in some * circumstances; disabled for now. */ #undef BTREE /* scan for the page, print any matches */ static void do_apropos (MYDBM_FILE dbf, const char * const *pages, int num_pages, int *found) { datum key, cont; char **lowpages; int *found_here; int (*combine) (int, int *); int i; #ifndef BTREE datum nextkey; #else /* BTREE */ int end; #endif /* !BTREE */ lowpages = XNMALLOC (num_pages, char *); for (i = 0; i < num_pages; ++i) { lowpages[i] = lower (pages[i]); debug ("lower(%s) = \"%s\"\n", pages[i], lowpages[i]); } found_here = XNMALLOC (num_pages, int); combine = require_all ? all_set : any_set; #ifndef BTREE key = MYDBM_FIRSTKEY (dbf); while (MYDBM_DPTR (key)) { cont = MYDBM_FETCH (dbf, key); #else /* BTREE */ end = btree_nextkeydata (dbf, &key, &cont); while (!end) { #endif /* !BTREE */ char *tab; struct mandata info; memset (&info, 0, sizeof (info)); /* bug#4372, NULL pointer dereference in MYDBM_DPTR (cont), * fix by dassen@wi.leidenuniv.nl (J.H.M.Dassen), thanx Ray. * cjwatson: In that case, complain and exit, otherwise we * might loop (bug #95052). */ if (!MYDBM_DPTR (cont)) { debug ("key was %s\n", MYDBM_DPTR (key)); error (FATAL, 0, _("Database %s corrupted; rebuild with " "mandb --create"), database); } if (*MYDBM_DPTR (key) == '$') goto nextpage; if (*MYDBM_DPTR (cont) == '\t') goto nextpage; /* a real page */ split_content (MYDBM_DPTR (cont), &info); /* If there are sections given, does any of them match * either the section or extension of this page? */ if (sections) { char * const *section; int matched = 0; for (section = sections; *section; ++section) { if (STREQ (*section, info.sec) || STREQ (*section, info.ext)) { matched = 1; break; } } if (!matched) goto nextpage; } tab = strrchr (MYDBM_DPTR (key), '\t'); if (tab) *tab = '\0'; memset (found_here, 0, num_pages * sizeof (*found_here)); if (am_apropos) { char *whatis; parse_name ((const char **) lowpages, num_pages, MYDBM_DPTR (key), found, found_here); whatis = info.whatis ? xstrdup (info.whatis) : NULL; if (!combine (num_pages, found_here) && whatis) parse_whatis (pages, lowpages, num_pages, whatis, found, found_here); free (whatis); } else parse_name (pages, num_pages, MYDBM_DPTR (key), found, found_here); if (combine (num_pages, found_here)) display (dbf, &info, MYDBM_DPTR (key)); if (tab) *tab = '\t'; nextpage: #ifndef BTREE nextkey = MYDBM_NEXTKEY (dbf, key); MYDBM_FREE_DPTR (cont); MYDBM_FREE_DPTR (key); key = nextkey; #else /* BTREE */ MYDBM_FREE_DPTR (cont); MYDBM_FREE_DPTR (key); end = btree_nextkeydata (dbf, &key, &cont); #endif /* !BTREE */ info.addr = NULL; /* == MYDBM_DPTR (cont), freed above */ free_mandata_elements (&info); } free (found_here); for (i = 0; i < num_pages; ++i) free (lowpages[i]); free (lowpages); } /* loop through the man paths, searching for a match */ static int search (const char * const *pages, int num_pages) { int *found = XCALLOC (num_pages, int); char *catpath, **mp; int any_found, i; for (mp = manpathlist; *mp; mp++) { MYDBM_FILE dbf; catpath = get_catpath (*mp, SYSTEM_CAT | USER_CAT); if (catpath) { database = mkdbname (catpath); free (catpath); } else database = mkdbname (*mp); debug ("path=%s\n", *mp); dbf = MYDBM_RDOPEN (database); if (dbf && dbver_rd (dbf)) { MYDBM_CLOSE (dbf); dbf = NULL; } if (!dbf) { use_grep (pages, num_pages, *mp, found); continue; } if (am_apropos) do_apropos (dbf, pages, num_pages, found); else { if (regex_opt || wildcard) do_apropos (dbf, pages, num_pages, found); else do_whatis (dbf, pages, num_pages, *mp, found); } free (database); database = NULL; MYDBM_CLOSE (dbf); } chkr_garbage_detector (); any_found = 0; for (i = 0; i < num_pages; ++i) { if (found[i]) any_found = 1; else fprintf (stderr, _("%s: nothing appropriate.\n"), pages[i]); } free (found); return any_found; } int main (int argc, char *argv[]) { char *program_base_name; #ifdef HAVE_ICONV char *locale_charset; #endif int status = OK; set_program_name (argv[0]); program_base_name = base_name (program_name); if (STREQ (program_base_name, APROPOS_NAME)) { am_apropos = 1; argp_program_version = "apropos " PACKAGE_VERSION; } else { struct argp_option *optionp; am_apropos = 0; argp_program_version = "whatis " PACKAGE_VERSION; for (optionp = (struct argp_option *) whatis_argp.options; optionp->name || optionp->key || optionp->arg || optionp->flags || optionp->doc || optionp->group; ++optionp) { if (!optionp->name) continue; if (STREQ (optionp->name, "exact") || STREQ (optionp->name, "and")) optionp->flags |= OPTION_HIDDEN; } } free (program_base_name); init_debug (); pipeline_install_post_fork (pop_all_cleanups); sandbox = sandbox_init (); init_locale (); internal_locale = setlocale (LC_MESSAGES, NULL); /* Use LANGUAGE only when LC_MESSAGES locale category is * neither "C" nor "POSIX". */ if (internal_locale && strcmp (internal_locale, "C") && strcmp (internal_locale, "POSIX")) multiple_locale = getenv ("LANGUAGE"); internal_locale = xstrdup (internal_locale ? internal_locale : "C"); if (argp_parse (am_apropos ? &apropos_argp : &whatis_argp, argc, argv, 0, 0, 0)) exit (FAIL); read_config_file (user_config_file != NULL); /* close this locale and reinitialise if a new locale was issued as an argument or in $MANOPT */ if (locale) { free (internal_locale); internal_locale = setlocale (LC_ALL, locale); if (internal_locale) internal_locale = xstrdup (internal_locale); else internal_locale = xstrdup (locale); debug ("main(): locale = %s, internal_locale = %s\n", locale, internal_locale); if (internal_locale) { setenv ("LANGUAGE", internal_locale, 1); locale_changed (); multiple_locale = NULL; } } /* sort out the internal manpath */ if (manp == NULL) manp = locale_manpath (get_manpath (alt_systems)); else free (get_manpath (NULL)); create_pathlist (manp, manpathlist); display_seen = hashtable_create (&null_hashtable_free); #ifdef HAVE_ICONV locale_charset = xasprintf ("%s//IGNORE", get_locale_charset ()); conv_to_locale = iconv_open (locale_charset, "UTF-8"); free (locale_charset); #endif /* HAVE_ICONV */ if (regex_opt) { int i; preg = XNMALLOC (num_keywords, regex_t); for (i = 0; i < num_keywords; ++i) xregcomp (&preg[i], keywords[i], REG_EXTENDED | REG_NOSUB | REG_ICASE); } if (!search ((const char **) keywords, num_keywords)) status = NOT_FOUND; if (regex_opt) { int i; for (i = 0; i < num_keywords; ++i) regfree (&preg[i]); free (preg); } #ifdef HAVE_ICONV if (conv_to_locale != (iconv_t) -1) iconv_close (conv_to_locale); #endif /* HAVE_ICONV */ hashtable_free (display_seen); free_pathlist (manpathlist); free (manp); free (internal_locale); exit (status); }