/* $OpenBSD: man.c,v 1.7 1998/03/09 23:20:13 millert Exp $ */ /* $NetBSD: man.c,v 1.7 1995/09/28 06:05:34 tls Exp $ */ /* * Copyright (c) 1987, 1993, 1994, 1995 * The Regents of the University of California. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * This product includes software developed by the University of * California, Berkeley and its contributors. * 4. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ #ifndef lint static char copyright[] = "@(#) Copyright (c) 1987, 1993, 1994, 1995\n\ The Regents of the University of California. All rights reserved.\n"; #endif /* not lint */ #ifndef lint #if 0 static char sccsid[] = "@(#)man.c 8.17 (Berkeley) 1/31/95"; #else static char rcsid[] = "$OpenBSD: man.c,v 1.7 1998/03/09 23:20:13 millert Exp $"; #endif #endif /* not lint */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "pathnames.h" #define MANSUBDIR "&&&" /* must be 3 bytes, for replacing with ``man'' * or ``cat'' */ int f_all, f_where; char *slashp; static void build_page __P((int,char *, char **)); static void cat __P((char *)); static char *check_pager __P((char *)); static int cleanup __P((void)); static void how __P((char *)); static void jump __P((char **, char *, char *)); static int manual __P((char *, TAG *, glob_t *)); static void onsig __P((int)); static void usage __P((int)); static void addsubdir __P((struct tqh*, char *, char *, char *)); static void pathmerge __P((char*, int)); char *__progname; static char *sflag; static int tflag; static char *roff_command; int main(argc, argv) int argc; char *argv[]; { extern char *optarg; extern int optind; TAG *defp, *defnewp, *section, *sectnewp, *subp; TAG *tagp; ENTRY *e_defp, *e_sectp, *e_subp, *ep; glob_t pg; size_t len; int manpath = 0; int ch, f_cat, f_how, found; char **ap, *cmd, *p, *p_add, *p_path, *pager, *s_path; char *conffile, buf[MAXPATHLEN * 2]; char *options; char *section_name; __progname = (char*)basename(argv[0]); DPRINT(("__progname = (%s)\n", __progname)); if (strcmp(__progname, "manpath") == 0) { options = "M:m:"; manpath = 1; } else options = "aC:cfhkM:m:P:s:twz"; sflag = NULL; f_cat = f_how = 0; conffile = p_add = p_path = NULL; while ((ch = getopt(argc, argv, options)) != -1) switch (ch) { case 'a': f_all = 1; break; case 'C': conffile = optarg; break; case 'c': case '-': /* Deprecated. */ f_cat = 1; break; case 'h': f_how = 1; break; case 'm': p_add = optarg; break; case 'M': case 'P': /* Backward compatibility. */ p_path = optarg; break; case 's': /* SVR4 compatibility. */ sflag = optarg; break; /* The -f and -k options are backward compatible, undocumented ways of calling whatis(1) and apropos(1). */ case 't': tflag = 1; f_cat = 1; break; case 'f': jump(argv, "-f", "whatis"); /* NOTREACHED */ case 'k': jump(argv, "-k", "apropos"); /* NOTREACHED */ case 'w': f_all = f_where = 1; break; case 'z': manpath = 1; break; case '?': default: usage(manpath); } argc -= optind; argv += optind; if (! (manpath || *argv)) usage(manpath); if (!f_cat && !f_how && !f_where) if (!isatty(1)) f_cat = 1; else if ((pager = getenv("PAGER")) != NULL) pager = check_pager(pager); else pager = _PATH_PAGER; /* Read the configuration file. */ config(conffile); if (tflag) { if ( ((tagp = getlist("_ps")) == 0 || tagp->list.tqh_first->s == 0) ) errx(1, "-t option not supported"); roff_command = strdup(tagp->list.tqh_first->s); } else { if ( ((tagp = getlist("_text")) == 0 || tagp->list.tqh_first->s == 0) ) errx(1, "there is no way to format manpages"); roff_command = strdup(tagp->list.tqh_first->s); } DPRINT(("roff_command is [%s]\n", roff_command)); /* If there's no _manpath list, create an empty one. */ if ((defp = getlist("_manpath")) == NULL) defp = addlist("_manpath"); /* .5: If the user specified a section (-s or first argument), blow away the generated _order and replace it with our own. */ if (sflag || argc >= optind+1) { if ((defnewp = getlist("_order")) != NULL) { /* out with the old order */ while ((e_defp = defnewp->list.tqh_first) != NULL) { free(e_defp->s); TAILQ_REMOVE(&defnewp->list, e_defp, q); } } else defnewp = addlist("_order"); /* in with the new order */ if ((e_subp = malloc(sizeof *e_subp)) == NULL || (e_subp->s = strdup(sflag ? sflag : argv[0])) == NULL) err(1, NULL); TAILQ_INSERT_TAIL(&defnewp->list, e_subp, q); DPRINT(("sflag = (%s), argc = (%d), optind = (%d)\n", sflag ? sflag : "0", argc, optind)); if (!sflag) { sflag = argv[0]; argc--, argv++; } } /* 1: If the user specified a MANPATH variable, or set the -M option, we replace the generated _manpath with what the user wants */ if (p_path == NULL) p_path = getenv("MANPATH"); if (p_path != NULL) { while ((e_defp = defp->list.tqh_first) != NULL) { free(e_defp->s); TAILQ_REMOVE(&defp->list, e_defp, q); } pathmerge(p_path, 0); } else { /* 1.125: If no MANPATH or -M, add selected directories from your $PATH */ pathmerge(getenv("PATH"), 1); } /* 1.25: any -m paths get added to the existing _manpath */ pathmerge(p_add, 0); /* 1.5: if we're the manpath command, spit out our current manpath and exit gracefully */ if (manpath) { for (e_subp = defp->list.tqh_first; e_subp != NULL; e_subp = e_subp->q.tqe_next) { printf("%s%c", e_subp->s, e_subp->q.tqe_next ? ':' : '\n'); } exit(0); } /* 2: rewrite the _manpath list to include the _order list. */ defnewp = addlist("_manpath_new"); e_defp = defp->list.tqh_first == NULL ? NULL : defp->list.tqh_first; for (; e_defp != NULL; e_defp = e_defp->q.tqe_next) { slashp = e_defp->s[strlen(e_defp->s) - 1] == '/' ? "" : "/"; e_subp = (subp = getlist("_order")) == NULL ? NULL : subp->list.tqh_first; for (; e_subp != NULL; e_subp = e_subp->q.tqe_next) addsubdir(&defnewp->list, e_defp->s, MANSUBDIR, e_subp->s); } defp = getlist("_manpath"); while ((e_defp = defp->list.tqh_first) != NULL) { free(e_defp->s); TAILQ_REMOVE(&defp->list, e_defp, q); } free(defp->s); TAILQ_REMOVE(&head, defp, q); defnewp = getlist("_manpath_new"); free(defnewp->s); defnewp->s = "_manpath"; defp = defnewp; /* 5: Search for the files. Set up an interrupt handler, so the temporary files go away. */ signal(SIGINT, onsig); signal(SIGHUP, onsig); memset(&pg, 0, sizeof (pg)); for (found = 0; *argv; ++argv) if (manual(*argv, defp, &pg)) found = 1; /* 6: If nothing found, we're done. */ if (!found) { cleanup(); exit(1); } /* 7: If it's simple, display it fast. */ if (f_cat) { for (ap = pg.gl_pathv; *ap != NULL; ++ap) { if (**ap == '\0') continue; cat(*ap); } exit(cleanup()); } if (f_how) { for (ap = pg.gl_pathv; *ap != NULL; ++ap) { if (**ap == '\0') continue; how(*ap); } exit(cleanup()); } if (f_where) { for (ap = pg.gl_pathv; *ap != NULL; ++ap) { if (**ap == '\0') continue; printf("%s\n", *ap); } exit(cleanup()); } /* 8: We display things in a single command; build a list of things to display. */ for (ap = pg.gl_pathv, len = strlen(pager) + 1; *ap != NULL; ++ap) { if (**ap == '\0') continue; len += strlen(*ap) + 1; } if ((cmd = malloc(len)) == NULL) { warn(NULL); cleanup(); exit(1); } p = cmd; len = strlen(pager); memmove(p, pager, len); p += len; *p++ = ' '; for (ap = pg.gl_pathv; *ap != NULL; ++ap) { if (**ap == '\0') continue; len = strlen(*ap); memmove(p, *ap, len); p += len; *p++ = ' '; } *p = '\0'; /* Use system(3) in case someone's pager is "pager arg1 arg2". */ system(cmd); exit(cleanup()); } /* * foundpage -- * Checks a list of possible manpages to make sure we know what to do * with them. Weed out all the unknown ones, and return 1 if at least * one of them is okay. */ static int foundpage(catman, page, lastglobc, pg) int catman; char *page; int lastglobc; glob_t *pg; { int cnt, found; int anyfound = 0; ENTRY *ep, *e_sufp, *e_tag; TAG *missp, *sufp; char *p, buf[MAXPATHLEN]; struct stat st; if (pg->gl_pathc == lastglobc) return 0; /* Find out if it's really a man page. */ for (cnt = lastglobc; cnt < pg->gl_pathc; ++cnt) { DPRINT(("%d: %s\n", cnt, pg->gl_pathv[cnt])); if (catman) { stat(pg->gl_pathv[cnt], &st); if (st.st_size == 0) { /* zero size? Definitely bogus */ pg->gl_pathv[cnt] = ""; continue; } } /* Try the _build key words next. */ e_sufp = (sufp = getlist( "_cat" )) == NULL ? NULL : sufp->list.tqh_first; for (found = 0; e_sufp != NULL; e_sufp = e_sufp->q.tqe_next) { for (p = e_sufp->s; *p != '\0' && !isspace(*p); ++p); if (*p == '\0') continue; *p = '\0'; snprintf(buf, sizeof (buf), "*/%s.%s", page, e_sufp->s); if (!fnmatch(buf, pg->gl_pathv[cnt], 0)) { if (!f_where) build_page(catman, p + 1, &pg->gl_pathv[cnt]); *p = ' '; found = 1; break; } *p = ' '; } if (catman && !found) { /* see if any _suffix will work */ e_sufp = (sufp = getlist("_suffix")) == NULL ? NULL : sufp->list.tqh_first; for (found = 0; e_sufp != NULL; e_sufp = e_sufp->q.tqe_next) { snprintf(buf, sizeof(buf), "*/%s.%s", page, e_sufp->s); DPRINT(("Suffix try: %s\n", buf)); if (!fnmatch(buf, pg->gl_pathv[cnt], 0)) { found = 1; break; } } } if (found) { next: anyfound = 1; if (!f_all) { /* Delete any other matches. */ while (++cnt < pg->gl_pathc) pg->gl_pathv[cnt] = ""; break; } continue; } /* It's not a man page, forget about it. */ pg->gl_pathv[cnt] = ""; } DPRINT(("anyfound = %d\n", anyfound)); return anyfound; } /* foundpage */ /* * manual -- * Search the manuals for the pages. */ static int manual(page, tag, pg) char *page; TAG *tag; glob_t *pg; { ENTRY *ep, *e_sufp, *e_tag; TAG *missp, *sufp; int anyfound, cnt, found; char *p, buf[MAXPATHLEN]; int autotype, lastglobc; anyfound = 0; buf[0] = '*'; /* For each element in the list... */ e_tag = tag == NULL ? NULL : tag->list.tqh_first; for (; e_tag != NULL; e_tag = e_tag->q.tqe_next) { /* is this a cat or man page? */ p = strrchr(e_tag->s, '/'); autotype = (p && (strncmp(p, "/" MANSUBDIR, 4) == 0)); lastglobc = pg->gl_pathc; if (autotype ) { snprintf(buf, sizeof buf, "%s/%s.[0-9]*", e_tag->s, page); if (tflag) { /* dumping postscript, so we have to use the unformatted * version */ memcpy(buf+(p - e_tag->s) + 1, "man", 3); glob(buf, GLOB_NOSORT|GLOB_APPEND, NULL, pg); anyfound |= foundpage(0, page, lastglobc, pg); } else { /* dumping text, so the formatted version is * usable; search for it first, then the * unformatted page. */ memcpy(buf+(p - e_tag->s) + 1, "cat", 3); glob(buf, GLOB_NOSORT|GLOB_APPEND, NULL, pg); if (foundpage(1, page, lastglobc, pg) == 0) { memcpy(buf+(p - e_tag->s) + 1, "man", 3); glob(buf, GLOB_NOSORT|GLOB_APPEND, NULL, pg); anyfound |= foundpage(0, page, lastglobc, pg); } else anyfound |= 1; } } else { snprintf(buf, sizeof (buf), "%s/%s.*", e_tag->s, page); glob(buf, GLOB_NOSORT|GLOB_APPEND, NULL, pg); anyfound |= foundpage(0, page, lastglobc, pg); } #ifdef DEBUG for (cnt=lastglobc; cntgl_pathc; cnt++) printf("%d: %s\n", cnt, pg->gl_pathv[cnt]); #endif lastglobc = pg->gl_pathc; if (anyfound && !f_all) break; } /* If not found, enter onto the missing list. */ if (!anyfound) { if ((missp = getlist("_missing")) == NULL) missp = addlist("_missing"); if ((ep = malloc(sizeof (ENTRY))) == NULL || (ep->s = strdup(page)) == NULL) { warn(NULL); cleanup(); exit(1); } TAILQ_INSERT_TAIL(&missp->list, ep, q); } return (anyfound); } /* * build_page -- * Build a man page for display. */ static void build_page(iscat, fmt, pathp) int iscat; char *fmt, **pathp; { static int warned; ENTRY *ep; TAG *intmpp; TAG *save = 0; int fd, n; char *p, *b; char buf[MAXPATHLEN], cmd[MAXPATHLEN], tpath[MAXPATHLEN]; /* Let the user know this may take awhile. */ if ( !(warned || iscat || tflag) ) { warned = 1; warnx("Formatting manual page..."); } /* Historically man chdir'd to the root of the man tree. This was used in man pages that contained relative ".so" directives (including other man pages for command aliases etc.) It even went one step farther, by examining the first line of the man page and parsing the .so filename so it would make hard(?) links to the cat'ted man pages for space savings. (We don't do that here, but we could). */ /* copy and find the end */ for (b = buf, p = *pathp; (*b++ = *p++) != '\0';) continue; /* skip the last two path components, page name and man[n] */ for (--b, n = 2; b != buf; b--) if (*b == '/') if (--n == 0) { *b = '\0'; DPRINT(("chdir(%s)\n", buf)); chdir(buf); } /* Add a remove-when-done list. */ if ((intmpp = getlist("_intmp")) == NULL) intmpp = addlist("_intmp"); /* Move to the printf(3) format string. */ for (; *fmt && isspace(*fmt); ++fmt); /* Get a temporary file and build a version of the file to display. Replace the old file name with the new one. */ strcpy(tpath, _PATH_TMPFILE); if ((fd = mkstemp(tpath)) == -1) { warn("%s", tpath); cleanup(); exit(1); } if (iscat) { snprintf(buf, sizeof (buf), "%s > %s", fmt, tpath); snprintf(cmd, sizeof (cmd), buf, *pathp); } else { snprintf(buf, sizeof (buf), "%s | %s > %s", fmt, roff_command, tpath); snprintf(cmd, sizeof (cmd), buf, *pathp); } DPRINT(("cmd is [%s]\n", cmd)); system(cmd); close(fd); /* try to store it as a catman page. We don't do anything fancy with * uids here; we simply try to write the formatted page and if it * fails we silently continue on. If we are setuid, this gives * an amazingly large security hole for hackers to drive through, * which is why we refuse to honor _save directives if our euid * is zero. */ if ( (tflag == 0) && (save = getlist("_save")) != NULL) ep = save->list.tqh_first; DPRINT(("save = (%x), ep = (%x), iscat = %d, strlen(%s) = %d, sizeof buf = %d\n", save, ep, iscat,*pathp, strlen(*pathp), sizeof buf)); if ( save && ep && !iscat && (strlen(*pathp) < sizeof buf) ) { char *p; strcpy(buf, *pathp); if (p = strrchr(buf, '/')) { for (--p; p >= buf && *p != '/'; --p) ; } else p = "no"; /* back up to /man and replace it with /cat */ if (strncmp(p, "/man", 4) == 0) { pid_t child; int status; memcpy(p, "/cat", 4); /* cosmetics: remove any compressed extensions. */ for (p = buf+strlen(buf)-1; *p != '/'; --p) if (*p == '.') { if (isdigit(p[1])) break; else *p = 0; } if (ep->q.tqe_next && ep->q.tqe_next != ep && ep->q.tqe_next->s) strcat(buf, ep->q.tqe_next->s); #ifdef DEBUG DPRINT(("produce preformat (%s) <- (%s) -> (%s)\n", ep->s, tpath, buf)); #else if ((child = fork()) == 0) { int s_out = open(buf, O_WRONLY|O_CREAT, 0444); int s_in = open(tpath, O_RDONLY); if (s_out != -1 && s_in != -1) { dup2(s_out, 1); dup2(s_in, 0); execl(ep->s, ep->s, 0L); } exit(1); } else wait(&status); #endif } } if ((*pathp = strdup(tpath)) == NULL) { warn(NULL); cleanup(); exit(1); } /* Link the built file into the remove-when-done list. */ if ((ep = malloc(sizeof (ENTRY))) == NULL) { warn(NULL); cleanup(); exit(1); } ep->s = *pathp; TAILQ_INSERT_TAIL(&intmpp->list, ep, q); } /* * how -- * display how information */ static void how(fname) char *fname; { FILE *fp; int lcnt, print; char *p, buf[256]; if (!(fp = fopen(fname, "r"))) { warn("%s", fname); cleanup(); exit(1); } #define S1 "SYNOPSIS" #define S2 "S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS" #define D1 "DESCRIPTION" #define D2 "D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN" for (lcnt = print = 0; fgets(buf, sizeof (buf), fp);) { if (!strncmp(buf, S1, sizeof (S1) - 1) || !strncmp(buf, S2, sizeof (S2) - 1)) { print = 1; continue; } else if (!strncmp(buf, D1, sizeof (D1) - 1) || !strncmp(buf, D2, sizeof (D2) - 1)) return; if (!print) continue; if (*buf == '\n') ++lcnt; else { for (; lcnt; --lcnt) putchar('\n'); for (p = buf; isspace(*p); ++p); fputs(p, stdout); } } fclose(fp); } /* * cat -- * cat out the file */ static void cat(fname) char *fname; { int fd, n; char buf[2048]; if ((fd = open(fname, O_RDONLY, 0)) < 0) { warn("%s", fname); cleanup(); exit(1); } while ((n = read(fd, buf, sizeof (buf))) > 0) if (write(STDOUT_FILENO, buf, n) != n) { warn("write"); cleanup(); exit(1); } if (n == -1) { warn("read"); cleanup(); exit(1); } close(fd); } /* * check_pager -- * check the user supplied page information */ static char * check_pager(name) char *name; { char *p, *save; /* if the user uses "more", we make it "more -s"; watch out for PAGER = "mypager /usr/ucb/more" */ for (p = name; *p && !isspace(*p); ++p); for (; p > name && *p != '/'; --p); if (p != name) ++p; /* make sure it's "more", not "morex" */ if (!strncmp(p, "more", 4) && (!p[4] || isspace(p[4]))) { save = name; /* allocate space to add the "-s" */ if (!(name = malloc((u_int) (strlen(save) + sizeof ("-s") + 1)))) err(1, NULL); sprintf(name, "%s %s", save, "-s"); } return (name); } /* * jump -- * strip out flag argument and jump */ static void jump(argv, flag, name) char **argv, *flag, *name; { char **arg; argv[0] = name; for (arg = argv + 1; *arg; ++arg) if (!strcmp(*arg, flag)) break; for (; *arg; ++arg) arg[0] = arg[1]; execvp(name, argv); if (sflag) fprintf(stderr, "%s: Command not found in section %s.\n", name, sflag); else fprintf(stderr, "%s: Command not found.\n", name); exit(1); } /* * onsig -- * If signaled, delete the temporary files. */ static void onsig(signo) int signo; { cleanup(); signal(signo, SIG_DFL); kill(getpid(), signo); /* NOTREACHED */ exit(1); } /* * cleanup -- * Clean up temporary files, show any error messages. */ static int cleanup() { TAG *intmpp, *missp; ENTRY *ep; int rval; rval = 0; ep = (missp = getlist("_missing")) == NULL ? NULL : missp->list.tqh_first; if (ep != NULL) for (; ep != NULL; ep = ep->q.tqe_next) { if (sflag) warnx("no entry for %s in section %s.", ep->s, sflag); else warnx("no entry for %s in the manual.", ep->s); rval = 1; } ep = (intmpp = getlist("_intmp")) == NULL ? NULL : intmpp->list.tqh_first; for (; ep != NULL; ep = ep->q.tqe_next) unlink(ep->s); return (rval); } /* * usage -- * print usage message and die */ static void usage(manpath) { fprintf(stderr, "usage: %s %s\n", __progname, manpath ? "[-M path] [-m path]" : "[-achw] [-C file] [-M path] [-m path] [section] title ..."); exit(1); } /* * addsubdir -- * add a subdirectory to a given tqh */ static void addsubdir(struct tqh *listp, char *prefix, char *sub, char *extension) { char buf[500]; ENTRY *ep; snprintf(buf, sizeof (buf), "%s%s%s%s", prefix, slashp, sub, extension); DPRINT(("addsubdir %s\n", buf)); if ((ep = malloc(sizeof (ENTRY))) == NULL || (ep->s = strdup(buf)) == NULL) err(1, NULL); TAILQ_INSERT_TAIL(listp, ep, q); } /* * pathmerge -- * merge parts of a user-supplied path into the _manpath. If make_path * is set, we only merge path elements that end in /bin, and are are * longer than /. In any case, we don't merge paths that already exist * in _manpath. */ static void pathmerge(char *path, int make_path) { TAG *manpath = getlist("_manpath"); ENTRY *ent; char *p, *q, *bfr; int len; if (path == 0) return; bfr = strdup(path); for (p = strtok(bfr, ":"); p != NULL; p = strtok(NULL, ":")) { if (make_path) { len = strlen(p); /* each path element needs to be rewritten from * Xxx/bin to Xxx/man; if we can't do this, * we won't bother to handle the path. */ for (q = p+len; q > p && *--q != '/'; ) ; if (q == p || strcmp(q, "/bin") != 0) continue; memcpy(q, "/man", 4); } /* check to see if Xxx/man is already in the manpath */ for (ent = manpath->list.tqh_first; ent != NULL; ent = ent->q.tqe_next) if (strcmp(p, ent->s) == 0) break; if (ent == NULL) { if ((ent = malloc(sizeof *ent)) == NULL || (ent->s = strdup(p)) == NULL) err(1, NULL); TAILQ_INSERT_TAIL(&manpath->list, ent, q); } } } /* merge */