man.c revision 1.30 1 /* $NetBSD: man.c,v 1.30 2003/08/07 11:15:10 agc Exp $ */
2
3 /*
4 * Copyright (c) 1987, 1993, 1994, 1995
5 * The Regents of the University of California. All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * 3. Neither the name of the University nor the names of its contributors
16 * may be used to endorse or promote products derived from this software
17 * without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 */
31
32 #include <sys/cdefs.h>
33
34 #ifndef lint
35 __COPYRIGHT("@(#) Copyright (c) 1987, 1993, 1994, 1995\n\
36 The Regents of the University of California. All rights reserved.\n");
37 #endif /* not lint */
38
39 #ifndef lint
40 #if 0
41 static char sccsid[] = "@(#)man.c 8.17 (Berkeley) 1/31/95";
42 #else
43 __RCSID("$NetBSD: man.c,v 1.30 2003/08/07 11:15:10 agc Exp $");
44 #endif
45 #endif /* not lint */
46
47 #include <sys/param.h>
48 #include <sys/queue.h>
49 #include <sys/utsname.h>
50
51 #include <ctype.h>
52 #include <err.h>
53 #include <errno.h>
54 #include <fcntl.h>
55 #include <fnmatch.h>
56 #include <glob.h>
57 #include <signal.h>
58 #include <stdio.h>
59 #include <stdlib.h>
60 #include <string.h>
61 #include <unistd.h>
62
63 #include "manconf.h"
64 #include "pathnames.h"
65
66 int f_all, f_where;
67
68 int main __P((int, char **));
69 static void build_page __P((char *, char **));
70 static void cat __P((char *));
71 static const char *check_pager __P((const char *));
72 static int cleanup __P((void));
73 static void how __P((char *));
74 static void jump __P((char **, char *, char *));
75 static int manual __P((char *, TAG *, glob_t *, const char *));
76 static void onsig __P((int));
77 static void usage __P((void));
78
79 int
80 main(argc, argv)
81 int argc;
82 char *argv[];
83 {
84 TAG *defp, *section, *newpathp, *subp;
85 ENTRY *e_defp, *e_subp;
86 glob_t pg;
87 size_t len;
88 int ch, f_cat, f_how, found, abs_section;
89 char **ap, *cmd, *p, *p_add, *p_path;
90 const char *machine, *pager, *conffile, *pathsearch, *sectionname;
91 char buf[MAXPATHLEN * 2];
92
93 #ifdef __GNUC__
94 pager = NULL; /* XXX gcc -Wuninitialized */
95 #endif
96
97 f_cat = f_how = 0;
98 sectionname = pathsearch = conffile = p_add = p_path = NULL;
99 while ((ch = getopt(argc, argv, "-aC:cfhkM:m:P:s:S:w")) != -1)
100 switch (ch) {
101 case 'a':
102 f_all = 1;
103 break;
104 case 'C':
105 conffile = optarg;
106 break;
107 case 'c':
108 case '-': /* Deprecated. */
109 f_cat = 1;
110 break;
111 case 'h':
112 f_how = 1;
113 break;
114 case 'm':
115 p_add = optarg;
116 break;
117 case 'M':
118 case 'P': /* Backward compatibility. */
119 p_path = strdup(optarg);
120 break;
121 /*
122 * The -f and -k options are backward compatible,
123 * undocumented ways of calling whatis(1) and apropos(1).
124 */
125 case 'f':
126 jump(argv, "-f", "whatis");
127 /* NOTREACHED */
128 case 'k':
129 jump(argv, "-k", "apropos");
130 /* NOTREACHED */
131 case 's':
132 if (sectionname != NULL)
133 usage();
134 sectionname = optarg;
135 break;
136 case 'S':
137 pathsearch = optarg;
138 break;
139 case 'w':
140 f_all = f_where = 1;
141 break;
142 case '?':
143 default:
144 usage();
145 }
146 argc -= optind;
147 argv += optind;
148
149 if (!argc)
150 usage();
151
152 if (!f_cat && !f_how && !f_where) {
153 if (!isatty(STDOUT_FILENO)) {
154 f_cat = 1;
155 } else {
156 if ((pager = getenv("PAGER")) != NULL &&
157 pager[0] != '\0')
158 pager = check_pager(pager);
159 else
160 pager = _PATH_PAGER;
161 }
162 }
163
164 /* Read the configuration file. */
165 config(conffile);
166
167 /* Get the machine type. */
168 if ((machine = getenv("MACHINE")) == NULL) {
169 struct utsname utsname;
170
171 if (uname(&utsname) == -1) {
172 perror("uname");
173 exit(1);
174 }
175 machine = utsname.machine;
176 }
177
178 /* create an empty _default list if the config file didn't have one */
179 defp = getlist("_default", 1);
180
181 /* if -M wasn't specified, check for MANPATH */
182 if (p_path == NULL)
183 p_path = getenv("MANPATH");
184
185 /*
186 * get section. abs_section will be non-zero iff the user
187 * specified a section and it had absolute (rather than
188 * relative) paths in the man.conf file.
189 */
190 if ((argc > 1 || sectionname != NULL) &&
191 (section = getlist(sectionname ? sectionname : *argv, 0)) != NULL) {
192 if (sectionname == NULL) {
193 argv++;
194 argc--;
195 }
196 abs_section = (! TAILQ_EMPTY(§ion->list) &&
197 *(TAILQ_FIRST(§ion->list)->s) == '/');
198 } else {
199 section = NULL;
200 abs_section = 0;
201 }
202
203 /* get subdir list */
204 subp = getlist("_subdir", 1);
205
206 /*
207 * now that we have all the inputs we must generate a search path.
208 */
209
210 /*
211 * 1: If user specified a section and it has absolute paths
212 * in the config file, then that overrides _default, MANPATH and
213 * path passed via -M.
214 */
215 if (abs_section) {
216 p_path = NULL; /* zap -M/MANPATH */
217 defp = section; /* zap _default */
218 section = NULL; /* promoted to defp */
219 }
220
221
222 /*
223 * 2: Section can be non-null only if a section was specified
224 * and the config file has relative paths - the section list
225 * overrides _subdir in this case.
226 */
227 if (section)
228 subp = section;
229
230
231 /*
232 * 3: now we either have text string path (p_path) or a tag
233 * based path (defp). we need to append subp and machine
234 * to each element in the path.
235 *
236 * for backward compat, we do not append subp if abs_section
237 * and the path does not end in "/".
238 */
239 newpathp = getlist("_new_path", 1);
240 if (p_path) {
241 /* use p_path */
242 for (; (p = strtok(p_path, ":")) != NULL; p_path = NULL) {
243 TAILQ_FOREACH(e_subp, &subp->list, q) {
244 snprintf(buf, sizeof(buf), "%s/%s{/%s,}",
245 p, e_subp->s, machine);
246 addentry(newpathp, buf, 0);
247 }
248 }
249 } else {
250 /* use defp rather than p_path */
251 TAILQ_FOREACH(e_defp, &defp->list, q) {
252
253 /* handle trailing "/" magic here ... */
254 if (abs_section &&
255 e_defp->s[strlen(e_defp->s) - 1] != '/') {
256
257 (void)snprintf(buf, sizeof(buf),
258 "%s{/%s,}", e_defp->s, machine);
259 addentry(newpathp, buf, 0);
260 continue;
261 }
262
263 TAILQ_FOREACH(e_subp, &subp->list, q) {
264 snprintf(buf, sizeof(buf), "%s%s%s{/%s,}",
265 e_defp->s, (abs_section) ? "" : "/",
266 e_subp->s, machine);
267 addentry(newpathp, buf, 0);
268 }
269 }
270 } /* using defp ... */
271
272 /* now replace the current path with the new one */
273 defp = newpathp;
274
275 /*
276 * 4: prepend the "-m" path, if specified. we always add
277 * subp and machine to this part of the path.
278 */
279
280 if (p_add) {
281 for (p = strtok(p_add, ":") ; p ; p = strtok(NULL, ":")) {
282 TAILQ_FOREACH(e_subp, &subp->list, q) {
283 snprintf(buf, sizeof(buf), "%s/%s{/%s,}",
284 p, e_subp->s, machine);
285 addentry(newpathp, buf, 1);
286 }
287 }
288 }
289
290
291 /*
292 * 5: Search for the files. Set up an interrupt handler, so the
293 * temporary files go away.
294 */
295 (void)signal(SIGINT, onsig);
296 (void)signal(SIGHUP, onsig);
297 (void)signal(SIGPIPE, onsig);
298
299 memset(&pg, 0, sizeof(pg));
300 for (found = 0; *argv; ++argv)
301 if (manual(*argv, defp, &pg, pathsearch))
302 found = 1;
303
304 /* 6: If nothing found, we're done. */
305 if (!found) {
306 (void)cleanup();
307 exit (1);
308 }
309
310 /* 7: If it's simple, display it fast. */
311 if (f_cat) {
312 for (ap = pg.gl_pathv; *ap != NULL; ++ap) {
313 if (**ap == '\0')
314 continue;
315 cat(*ap);
316 }
317 exit (cleanup());
318 }
319 if (f_how) {
320 for (ap = pg.gl_pathv; *ap != NULL; ++ap) {
321 if (**ap == '\0')
322 continue;
323 how(*ap);
324 }
325 exit(cleanup());
326 }
327 if (f_where) {
328 for (ap = pg.gl_pathv; *ap != NULL; ++ap) {
329 if (**ap == '\0')
330 continue;
331 (void)printf("%s\n", *ap);
332 }
333 exit(cleanup());
334 }
335
336 /*
337 * 8: We display things in a single command; build a list of things
338 * to display.
339 */
340 for (ap = pg.gl_pathv, len = strlen(pager) + 1; *ap != NULL; ++ap) {
341 if (**ap == '\0')
342 continue;
343 len += strlen(*ap) + 1;
344 }
345 if ((cmd = malloc(len)) == NULL) {
346 warn("malloc");
347 (void)cleanup();
348 exit(1);
349 }
350 p = cmd;
351 len = strlen(pager);
352 memmove(p, pager, len);
353 p += len;
354 *p++ = ' ';
355 for (ap = pg.gl_pathv; *ap != NULL; ++ap) {
356 if (**ap == '\0')
357 continue;
358 len = strlen(*ap);
359 memmove(p, *ap, len);
360 p += len;
361 *p++ = ' ';
362 }
363 *--p = '\0';
364
365 /* Use system(3) in case someone's pager is "pager arg1 arg2". */
366 (void)system(cmd);
367
368 exit(cleanup());
369 }
370
371 /*
372 * manual --
373 * Search the manuals for the pages.
374 */
375 static int
376 manual(page, tag, pg, pathsearch)
377 char *page;
378 TAG *tag;
379 glob_t *pg;
380 const char *pathsearch;
381 {
382 ENTRY *ep, *e_sufp, *e_tag;
383 TAG *missp, *sufp;
384 int anyfound, cnt, error, found;
385 char *p, buf[MAXPATHLEN], *escpage, *eptr;
386 static const char escglob[] = "\\~?*{}[]";
387
388 anyfound = 0;
389 buf[0] = '*';
390
391 /*
392 * Fixup page which may contain glob(3) special characters, e.g.
393 * the famous "No man page for [" FAQ.
394 */
395 if ((escpage = malloc((2 * strlen(page)) + 1)) == NULL) {
396 warn("malloc");
397 (void)cleanup();
398 exit(1);
399 }
400
401 p = page;
402 eptr = escpage;
403
404 while (*p) {
405 if (strchr(escglob, *p) != NULL) {
406 *eptr++ = '\\';
407 *eptr++ = *p++;
408 } else
409 *eptr++ = *p++;
410 }
411
412 *eptr = '\0';
413
414 /* For each element in the list... */
415 TAILQ_FOREACH(e_tag, &tag->list, q) {
416 (void)snprintf(buf, sizeof(buf), "%s/%s.*", e_tag->s, escpage);
417 if ((error = glob(buf,
418 GLOB_APPEND | GLOB_BRACE | GLOB_NOSORT, NULL, pg)) != 0) {
419 if (error == GLOB_NOMATCH)
420 continue;
421 else {
422 warn("globbing");
423 (void)cleanup();
424 exit(1);
425 }
426 }
427 if (pg->gl_matchc == 0)
428 continue;
429
430 /* Find out if it's really a man page. */
431 for (cnt = pg->gl_pathc - pg->gl_matchc;
432 cnt < pg->gl_pathc; ++cnt) {
433
434 if (pathsearch) {
435 p = strstr(pg->gl_pathv[cnt], pathsearch);
436 if (!p || strchr(p, '/') == NULL) {
437 pg->gl_pathv[cnt] = "";
438 continue;
439 }
440 }
441
442 /*
443 * Try the _suffix key words first.
444 *
445 * XXX
446 * Older versions of man.conf didn't have the suffix
447 * key words, it was assumed that everything was a .0.
448 * We just test for .0 first, it's fast and probably
449 * going to hit.
450 */
451 (void)snprintf(buf, sizeof(buf), "*/%s.0", escpage);
452 if (!fnmatch(buf, pg->gl_pathv[cnt], 0))
453 goto next;
454
455 sufp = getlist("_suffix", 1);
456 found = 0;
457 TAILQ_FOREACH(e_sufp, &sufp->list, q) {
458 (void)snprintf(buf,
459 sizeof(buf), "*/%s%s", escpage,
460 e_sufp->s);
461 if (!fnmatch(buf, pg->gl_pathv[cnt], 0)) {
462 found = 1;
463 break;
464 }
465 }
466 if (found)
467 goto next;
468
469 /* Try the _build key words next. */
470 sufp = getlist("_build", 1);
471 found = 0;
472 TAILQ_FOREACH(e_sufp, &sufp->list, q) {
473 for (p = e_sufp->s;
474 *p != '\0' && !isspace((unsigned char)*p);
475 ++p)
476 continue;
477 if (*p == '\0')
478 continue;
479 *p = '\0';
480 (void)snprintf(buf,
481 sizeof(buf), "*/%s%s", escpage,
482 e_sufp->s);
483 if (!fnmatch(buf, pg->gl_pathv[cnt], 0)) {
484 if (!f_where)
485 build_page(p + 1,
486 &pg->gl_pathv[cnt]);
487 *p = ' ';
488 found = 1;
489 break;
490 }
491 *p = ' ';
492 }
493 if (found) {
494 next: anyfound = 1;
495 if (!f_all) {
496 /* Delete any other matches. */
497 while (++cnt< pg->gl_pathc)
498 pg->gl_pathv[cnt] = "";
499 break;
500 }
501 continue;
502 }
503
504 /* It's not a man page, forget about it. */
505 pg->gl_pathv[cnt] = "";
506 }
507
508 if (anyfound && !f_all)
509 break;
510 }
511
512 /* If not found, enter onto the missing list. */
513 if (!anyfound) {
514 missp = getlist("_missing", 1);
515 if ((ep = malloc(sizeof(ENTRY))) == NULL ||
516 (ep->s = strdup(page)) == NULL) {
517 warn("malloc");
518 (void)cleanup();
519 exit(1);
520 }
521 TAILQ_INSERT_TAIL(&missp->list, ep, q);
522 }
523
524 free(escpage);
525 return (anyfound);
526 }
527
528 /*
529 * build_page --
530 * Build a man page for display.
531 */
532 static void
533 build_page(fmt, pathp)
534 char *fmt, **pathp;
535 {
536 static int warned;
537 ENTRY *ep;
538 TAG *intmpp;
539 int fd, n;
540 char *p, *b;
541 char buf[MAXPATHLEN], cmd[MAXPATHLEN], tpath[MAXPATHLEN];
542 const char *tmpdir;
543
544 /* Let the user know this may take awhile. */
545 if (!warned) {
546 warned = 1;
547 warnx("Formatting manual page...");
548 }
549
550 /*
551 * Historically man chdir'd to the root of the man tree.
552 * This was used in man pages that contained relative ".so"
553 * directives (including other man pages for command aliases etc.)
554 * It even went one step farther, by examining the first line
555 * of the man page and parsing the .so filename so it would
556 * make hard(?) links to the cat'ted man pages for space savings.
557 * (We don't do that here, but we could).
558 */
559
560 /* copy and find the end */
561 for (b = buf, p = *pathp; (*b++ = *p++) != '\0';)
562 continue;
563
564 /* skip the last two path components, page name and man[n] */
565 for (--b, --p, n = 2; b != buf; b--, p--)
566 if (*b == '/')
567 if (--n == 0) {
568 *b = '\0';
569 (void) chdir(buf);
570 p++;
571 break;
572 }
573
574
575 /* Add a remove-when-done list. */
576 intmpp = getlist("_intmp", 1);
577
578 /* Move to the printf(3) format string. */
579 for (; *fmt && isspace((unsigned char)*fmt); ++fmt)
580 continue;
581
582 /*
583 * Get a temporary file and build a version of the file
584 * to display. Replace the old file name with the new one.
585 */
586 if ((tmpdir = getenv("TMPDIR")) == NULL)
587 tmpdir = _PATH_TMP;
588 (void)snprintf(tpath, sizeof (tpath), "%s/%s", tmpdir, TMPFILE);
589 if ((fd = mkstemp(tpath)) == -1) {
590 warn("%s", tpath);
591 (void)cleanup();
592 exit(1);
593 }
594 (void)snprintf(buf, sizeof(buf), "%s > %s", fmt, tpath);
595 (void)snprintf(cmd, sizeof(cmd), buf, p);
596 (void)system(cmd);
597 (void)close(fd);
598 if ((*pathp = strdup(tpath)) == NULL) {
599 warn("malloc");
600 (void)cleanup();
601 exit(1);
602 }
603
604 /* Link the built file into the remove-when-done list. */
605 if ((ep = malloc(sizeof(ENTRY))) == NULL) {
606 warn("malloc");
607 (void)cleanup();
608 exit(1);
609 }
610 ep->s = *pathp;
611 TAILQ_INSERT_TAIL(&intmpp->list, ep, q);
612 }
613
614 /*
615 * how --
616 * display how information
617 */
618 static void
619 how(fname)
620 char *fname;
621 {
622 FILE *fp;
623
624 int lcnt, print;
625 char *p, buf[256];
626
627 if (!(fp = fopen(fname, "r"))) {
628 warn("%s", fname);
629 (void)cleanup();
630 exit (1);
631 }
632 #define S1 "SYNOPSIS"
633 #define S2 "S\bSY\bYN\bNO\bOP\bPS\bSI\bIS\bS"
634 #define D1 "DESCRIPTION"
635 #define D2 "D\bDE\bES\bSC\bCR\bRI\bIP\bPT\bTI\bIO\bON\bN"
636 for (lcnt = print = 0; fgets(buf, sizeof(buf), fp);) {
637 if (!strncmp(buf, S1, sizeof(S1) - 1) ||
638 !strncmp(buf, S2, sizeof(S2) - 1)) {
639 print = 1;
640 continue;
641 } else if (!strncmp(buf, D1, sizeof(D1) - 1) ||
642 !strncmp(buf, D2, sizeof(D2) - 1))
643 return;
644 if (!print)
645 continue;
646 if (*buf == '\n')
647 ++lcnt;
648 else {
649 for(; lcnt; --lcnt)
650 (void)putchar('\n');
651 for (p = buf; isspace((unsigned char)*p); ++p)
652 continue;
653 (void)fputs(p, stdout);
654 }
655 }
656 (void)fclose(fp);
657 }
658
659 /*
660 * cat --
661 * cat out the file
662 */
663 static void
664 cat(fname)
665 char *fname;
666 {
667 int fd, n;
668 char buf[2048];
669
670 if ((fd = open(fname, O_RDONLY, 0)) < 0) {
671 warn("%s", fname);
672 (void)cleanup();
673 exit(1);
674 }
675 while ((n = read(fd, buf, sizeof(buf))) > 0)
676 if (write(STDOUT_FILENO, buf, n) != n) {
677 warn("write");
678 (void)cleanup();
679 exit (1);
680 }
681 if (n == -1) {
682 warn("read");
683 (void)cleanup();
684 exit(1);
685 }
686 (void)close(fd);
687 }
688
689 /*
690 * check_pager --
691 * check the user supplied page information
692 */
693 static const char *
694 check_pager(name)
695 const char *name;
696 {
697 const char *p;
698
699 /*
700 * if the user uses "more", we make it "more -s"; watch out for
701 * PAGER = "mypager /usr/ucb/more"
702 */
703 for (p = name; *p && !isspace((unsigned char)*p); ++p)
704 continue;
705 for (; p > name && *p != '/'; --p);
706 if (p != name)
707 ++p;
708
709 /* make sure it's "more", not "morex" */
710 if (!strncmp(p, "more", 4) && (!p[4] || isspace((unsigned char)p[4]))){
711 char *newname;
712 (void)asprintf(&newname, "%s %s", p, "-s");
713 name = newname;
714 }
715
716 return (name);
717 }
718
719 /*
720 * jump --
721 * strip out flag argument and jump
722 */
723 static void
724 jump(argv, flag, name)
725 char **argv, *flag, *name;
726 {
727 char **arg;
728
729 argv[0] = name;
730 for (arg = argv + 1; *arg; ++arg)
731 if (!strcmp(*arg, flag))
732 break;
733 for (; *arg; ++arg)
734 arg[0] = arg[1];
735 execvp(name, argv);
736 (void)fprintf(stderr, "%s: Command not found.\n", name);
737 exit(1);
738 }
739
740 /*
741 * onsig --
742 * If signaled, delete the temporary files.
743 */
744 static void
745 onsig(signo)
746 int signo;
747 {
748 sigset_t set;
749
750 (void)cleanup();
751
752 (void)signal(signo, SIG_DFL);
753
754 /* unblock the signal */
755 sigemptyset(&set);
756 sigaddset(&set, signo);
757 sigprocmask(SIG_UNBLOCK, &set, (sigset_t *) NULL);
758
759 (void)kill(getpid(), signo);
760
761 /* NOTREACHED */
762 exit (1);
763 }
764
765 /*
766 * cleanup --
767 * Clean up temporary files, show any error messages.
768 */
769 static int
770 cleanup()
771 {
772 TAG *intmpp, *missp;
773 ENTRY *ep;
774 int rval;
775
776 rval = 0;
777 /*
778 * get missing list, but don't create missing _missing,
779 * as we don't want to try & allocate memory in getlist()
780 */
781 if ((missp = getlist("_missing", 0)) != NULL)
782 TAILQ_FOREACH(ep, &missp->list, q) {
783 warnx("no entry for %s in the manual.", ep->s);
784 rval = 1;
785 }
786
787 /*
788 * get tempfile list, but don't create missing _intmp,
789 * as we don't want to try & allocate memory in getlist()
790 */
791 if ((intmpp = getlist("_intmp", 0)) != NULL)
792 TAILQ_FOREACH(ep, &intmpp->list, q)
793 (void)unlink(ep->s);
794 return (rval);
795 }
796
797 /*
798 * usage --
799 * print usage message and die
800 */
801 static void
802 usage()
803 {
804
805 (void)fprintf(stderr, "Usage: %s [-achw] [-C file] [-M path] [-m path]"
806 "[-S srch] [[-s] section] title ...\n", getprogname());
807 exit(1);
808 }
809