1 /***********************************************************************
2 * *
3 * This software is part of the ast package *
4 * Copyright (c) 1992-2012 AT&T Intellectual Property *
5 * and is licensed under the *
6 * Eclipse Public License, Version 1.0 *
7 * by AT&T Intellectual Property *
8 * *
9 * A copy of the License is available at *
10 * http://www.eclipse.org/org/documents/epl-v10.html *
11 * (with md5 checksum b35adb5213ca9657e911e9befb180842) *
12 * *
13 * Information and Software Systems Research *
14 * AT&T Research *
15 * Florham Park NJ *
16 * *
17 * Glenn Fowler <gsf@research.att.com> *
18 * David Korn <dgk@research.att.com> *
19 * *
20 ***********************************************************************/
21 #pragma prototyped
22
23 /*
24 * print the tail of one or more files
25 *
26 * David Korn
27 * Glenn Fowler
28 */
29
30 static const char usage[] =
31 "+[-?\n@(#)$Id: tail (AT&T Research) 2012-06-19 $\n]"
32 USAGE_LICENSE
33 "[+NAME?tail - output trailing portion of one or more files ]"
34 "[+DESCRIPTION?\btail\b copies one or more input files to standard output "
35 "starting at a designated point for each file. Copying starts "
36 "at the point indicated by the options and is unlimited in size.]"
37 "[+?By default a header of the form \b==> \b\afilename\a\b <==\b "
38 "is output before all but the first file but this can be changed "
39 "with the \b-q\b and \b-v\b options.]"
40 "[+?If no \afile\a is given, or if the \afile\a is \b-\b, \btail\b "
41 "copies from standard input. The start of the file is defined "
42 "as the current offset.]"
43 "[+?The option argument for \b-c\b can optionally be "
44 "followed by one of the following characters to specify a different "
45 "unit other than a single byte:]{"
46 "[+b?512 bytes.]"
47 "[+k?1 KiB.]"
48 "[+m?1 MiB.]"
49 "[+g?1 GiB.]"
50 "}"
51 "[+?For backwards compatibility, \b-\b\anumber\a is equivalent to "
52 "\b-n\b \anumber\a and \b+\b\anumber\a is equivalent to "
53 "\b-n -\b\anumber\a. \anumber\a may also have these option "
54 "suffixes: \bb c f g k l m r\b.]"
55
56 "[n:lines]:[lines:=10?Copy \alines\a lines from each file. A negative value "
57 "for \alines\a indicates an offset from the end of the file.]"
58 "[b:blocks?Copy units of 512 bytes.]"
59 "[c:bytes]:?[chars?Copy \achars\a bytes from each file. A negative value "
60 "for \achars\a indicates an offset from the end of the file.]"
61 "[f:forever|follow?Loop forever trying to read more characters as the "
62 "end of each file to copy new data. Ignored if reading from a pipe "
63 "or fifo.]"
64 "[h!:headers?Output filename headers.]"
65 "[l:lines?Copy units of lines. This is the default.]"
66 "[L:log?When a \b--forever\b file times out via \b--timeout\b, verify that "
67 "the curent file has not been renamed and replaced by another file "
68 "of the same name (a common log file practice) before giving up on "
69 "the file.]"
70 "[q:quiet?Don't output filename headers. For GNU compatibility.]"
71 "[r:reverse?Output lines in reverse order.]"
72 "[s:silent?Don't warn about timeout expiration and log file changes.]"
73 "[t:timeout?Stop checking after \atimeout\a elapses with no additional "
74 "\b--forever\b output. A separate elapsed time is maintained for "
75 "each file operand. There is no timeout by default. The default "
76 "\atimeout\a unit is seconds. \atimeout\a may be a catenation of 1 "
77 "or more integers, each followed by a 1 character suffix. The suffix "
78 "may be omitted from the last integer, in which case it is "
79 "interpreted as seconds. The supported suffixes are:]:[timeout]{"
80 "[+s?seconds]"
81 "[+m?minutes]"
82 "[+h?hours]"
83 "[+d?days]"
84 "[+w?weeks]"
85 "[+M?months]"
86 "[+y?years]"
87 "[+S?scores]"
88 "}"
89 "[v:verbose?Always ouput filename headers.]"
90
91 "\n"
92 "\n[file ...]\n"
93 "\n"
94
95 "[+EXIT STATUS?]{"
96 "[+0?All files copied successfully.]"
97 "[+>0?One or more files did not copy.]"
98 "}"
99 "[+SEE ALSO?\bcat\b(1), \bhead\b(1), \brev\b(1)]"
100 ;
101
102 #include <cmd.h>
103 #include <ctype.h>
104 #include <ls.h>
105 #include <tv.h>
106 #include <rev.h>
107
108 #define COUNT (1<<0)
109 #define ERROR (1<<1)
110 #define FOLLOW (1<<2)
111 #define HEADERS (1<<3)
112 #define LINES (1<<4)
113 #define LOG (1<<5)
114 #define NEGATIVE (1<<6)
115 #define POSITIVE (1<<7)
116 #define REVERSE (1<<8)
117 #define SILENT (1<<9)
118 #define TIMEOUT (1<<10)
119 #define VERBOSE (1<<11)
120
121 #define NOW (unsigned long)time(NiL)
122
123 #define DEFAULT 10
124
125 #ifdef S_ISSOCK
126 #define FIFO(m) (S_ISFIFO(m)||S_ISSOCK(m))
127 #else
128 #define FIFO(m) S_ISFIFO(m)
129 #endif
130
131 struct Tail_s; typedef struct Tail_s Tail_t;
132
133 struct Tail_s
134 {
135 Tail_t* next;
136 char* name;
137 Sfio_t* sp;
138 Sfoff_t cur;
139 Sfoff_t end;
140 unsigned long expire;
141 long dev;
142 long ino;
143 int fifo;
144 };
145
146 static const char header_fmt[] = "\n==> %s <==\n";
147
148 /*
149 * if file is seekable, position file to tail location and return offset
150 * otherwise, return -1
151 */
152
153 static Sfoff_t
tailpos(register Sfio_t * fp,register Sfoff_t number,int delim)154 tailpos(register Sfio_t* fp, register Sfoff_t number, int delim)
155 {
156 register size_t n;
157 register Sfoff_t offset;
158 register Sfoff_t first;
159 register Sfoff_t last;
160 register char* s;
161 register char* t;
162 struct stat st;
163
164 last = sfsize(fp);
165 if ((first = sfseek(fp, (Sfoff_t)0, SEEK_CUR)) < 0)
166 return last || fstat(sffileno(fp), &st) || st.st_size || FIFO(st.st_mode) ? -1 : 0;
167 if (delim < 0)
168 {
169 if ((offset = last - number) < first)
170 return first;
171 return offset;
172 }
173 for (;;)
174 {
175 if ((offset = last - SF_BUFSIZE) < first)
176 offset = first;
177 sfseek(fp, offset, SEEK_SET);
178 n = last - offset;
179 if (!(s = sfreserve(fp, n, SF_LOCKR)))
180 return -1;
181 t = s + n;
182 while (t > s)
183 if (*--t == delim && number-- <= 0)
184 {
185 sfread(fp, s, 0);
186 return offset + (t - s) + 1;
187 }
188 sfread(fp, s, 0);
189 if (offset == first)
190 break;
191 last = offset;
192 }
193 return first;
194 }
195
196 /*
197 * this code handles tail from a pipe without any size limits
198 */
199
200 static void
pipetail(Sfio_t * infile,Sfio_t * outfile,Sfoff_t number,int delim)201 pipetail(Sfio_t* infile, Sfio_t* outfile, Sfoff_t number, int delim)
202 {
203 register Sfio_t* out;
204 register Sfoff_t n;
205 register Sfoff_t nleft = number;
206 register size_t a = 2 * SF_BUFSIZE;
207 register int fno = 0;
208 Sfoff_t offset[2];
209 Sfio_t* tmp[2];
210
211 if (delim < 0 && a > number)
212 a = number;
213 out = tmp[0] = sftmp(a);
214 tmp[1] = sftmp(a);
215 offset[0] = offset[1] = 0;
216 while ((n = sfmove(infile, out, number, delim)) > 0)
217 {
218 offset[fno] = sftell(out);
219 if ((nleft -= n) <= 0)
220 {
221 out = tmp[fno= !fno];
222 sfseek(out, (Sfoff_t)0, SEEK_SET);
223 nleft = number;
224 }
225 }
226 if (nleft == number)
227 {
228 offset[fno] = 0;
229 fno= !fno;
230 }
231 sfseek(tmp[0], (Sfoff_t)0, SEEK_SET);
232
233 /*
234 * see whether both files are needed
235 */
236
237 if (offset[fno])
238 {
239 sfseek(tmp[1], (Sfoff_t)0, SEEK_SET);
240 if ((n = number - nleft) > 0)
241 sfmove(tmp[!fno], NiL, n, delim);
242 if ((n = offset[!fno] - sftell(tmp[!fno])) > 0)
243 sfmove(tmp[!fno], outfile, n, -1);
244 }
245 else
246 fno = !fno;
247 sfmove(tmp[fno], outfile, offset[fno], -1);
248 sfclose(tmp[0]);
249 sfclose(tmp[1]);
250 }
251
252 /*
253 * (re)initialize a tail stream
254 */
255
256 static int
init(Tail_t * tp,Sfoff_t number,int delim,int flags,const char ** format)257 init(Tail_t* tp, Sfoff_t number, int delim, int flags, const char** format)
258 {
259 Sfoff_t offset;
260 Sfio_t* op;
261 struct stat st;
262
263 tp->fifo = 0;
264 if (tp->sp)
265 {
266 offset = 0;
267 if (tp->sp == sfstdin)
268 tp->sp = 0;
269 }
270 else
271 offset = 1;
272 if (!tp->name || streq(tp->name, "-"))
273 {
274 tp->name = "/dev/stdin";
275 tp->sp = sfstdin;
276 }
277 else if (!(tp->sp = sfopen(tp->sp, tp->name, "r")))
278 {
279 error(ERROR_system(0), "%s: cannot open", tp->name);
280 return -1;
281 }
282 sfset(tp->sp, SF_SHARE, 0);
283 if (offset)
284 {
285 if (number < 0 || !number && (flags & POSITIVE))
286 {
287 sfset(tp->sp, SF_SHARE, !(flags & FOLLOW));
288 if (number < -1)
289 {
290 sfmove(tp->sp, NiL, -number - 1, delim);
291 offset = sfseek(tp->sp, (Sfoff_t)0, SEEK_CUR);
292 }
293 else
294 offset = 0;
295 }
296 else if ((offset = tailpos(tp->sp, number, delim)) >= 0)
297 sfseek(tp->sp, offset, SEEK_SET);
298 else if (fstat(sffileno(tp->sp), &st))
299 {
300 error(ERROR_system(0), "%s: cannot stat", tp->name);
301 goto bad;
302 }
303 else if (!FIFO(st.st_mode))
304 {
305 error(ERROR_SYSTEM|2, "%s: cannot position file to tail", tp->name);
306 goto bad;
307 }
308 else
309 {
310 tp->fifo = 1;
311 if (flags & (HEADERS|VERBOSE))
312 {
313 sfprintf(sfstdout, *format, tp->name);
314 *format = header_fmt;
315 }
316 op = (flags & REVERSE) ? sftmp(4*SF_BUFSIZE) : sfstdout;
317 pipetail(tp->sp ? tp->sp : sfstdin, op, number, delim);
318 if (flags & REVERSE)
319 {
320 sfseek(op, (Sfoff_t)0, SEEK_SET);
321 rev_line(op, sfstdout, (Sfoff_t)0);
322 sfclose(op);
323 }
324 }
325 }
326 tp->cur = tp->end = offset;
327 if (flags & LOG)
328 {
329 if (fstat(sffileno(tp->sp), &st))
330 {
331 error(ERROR_system(0), "%s: cannot stat", tp->name);
332 goto bad;
333 }
334 tp->dev = st.st_dev;
335 tp->ino = st.st_ino;
336 }
337 return 0;
338 bad:
339 if (tp->sp != sfstdin)
340 sfclose(tp->sp);
341 tp->sp = 0;
342 return -1;
343 }
344
345 /*
346 * convert number with validity diagnostics
347 */
348
349 static intmax_t
num(register const char * s,char ** e,int * f,int o)350 num(register const char* s, char** e, int* f, int o)
351 {
352 intmax_t number;
353 char* t;
354 int c;
355
356 *f &= ~(ERROR|NEGATIVE|POSITIVE);
357 if ((c = *s) == '-')
358 {
359 *f |= NEGATIVE;
360 s++;
361 }
362 else if (c == '+')
363 {
364 *f |= POSITIVE;
365 s++;
366 }
367 while (*s == '0' && isdigit(*(s + 1)))
368 s++;
369 errno = 0;
370 number = strtonll(s, &t, NiL, 0);
371 if (t == s)
372 number = DEFAULT;
373 if (o && *t)
374 {
375 number = 0;
376 *f |= ERROR;
377 error(2, "-%c: %s: invalid numeric argument -- unknown suffix", o, s);
378 }
379 else if (errno)
380 {
381 *f |= ERROR;
382 if (o)
383 error(2, "-%c: %s: invalid numeric argument -- out of range", o, s);
384 else
385 error(2, "%s: invalid numeric argument -- out of range", s);
386 }
387 else
388 {
389 *f |= COUNT;
390 if (t > s && isalpha(*(t - 1)))
391 *f &= ~LINES;
392 if (c == '-')
393 number = -number;
394 }
395 if (e)
396 *e = t;
397 return number;
398 }
399
400 int
b_tail(int argc,char ** argv,Shbltin_t * context)401 b_tail(int argc, char** argv, Shbltin_t* context)
402 {
403 register Sfio_t* ip;
404 register int n;
405 register int i;
406 int delim;
407 int flags = HEADERS|LINES;
408 int blocks = 0;
409 char* s;
410 char* t;
411 char* r;
412 char* file;
413 Sfoff_t offset;
414 Sfoff_t number = DEFAULT;
415 unsigned long timeout = 0;
416 struct stat st;
417 const char* format = header_fmt+1;
418 ssize_t z;
419 ssize_t w;
420 Sfio_t* op;
421 register Tail_t* fp;
422 register Tail_t* pp;
423 register Tail_t* hp;
424 Tail_t* files;
425 Tv_t tv;
426
427 cmdinit(argc, argv, context, ERROR_CATALOG, 0);
428 for (;;)
429 {
430 switch (n = optget(argv, usage))
431 {
432 case 0:
433 if (!(flags & FOLLOW) && argv[opt_info.index] && (argv[opt_info.index][0] == '-' || argv[opt_info.index][0] == '+') && !argv[opt_info.index][1])
434 {
435 number = argv[opt_info.index][0] == '-' ? 10 : -10;
436 flags |= LINES;
437 opt_info.index++;
438 continue;
439 }
440 break;
441 case 'b':
442 blocks = 512;
443 flags &= ~LINES;
444 if (opt_info.option[0] == '+')
445 number = -number;
446 continue;
447 case 'c':
448 flags &= ~LINES;
449 if (opt_info.arg == argv[opt_info.index - 1])
450 {
451 strtol(opt_info.arg, &s, 10);
452 if (*s)
453 {
454 opt_info.index--;
455 t = "";
456 goto suffix;
457 }
458 }
459 else if (opt_info.arg && isalpha(*opt_info.arg))
460 {
461 t = opt_info.arg;
462 goto suffix;
463 }
464 /*FALLTHROUGH*/
465 case 'n':
466 flags |= COUNT;
467 if (s = opt_info.arg)
468 number = num(s, &s, &flags, n);
469 else
470 {
471 number = DEFAULT;
472 flags &= ~(ERROR|NEGATIVE|POSITIVE);
473 s = "";
474 }
475 if (n != 'n' && s && isalpha(*s))
476 {
477 t = s;
478 goto suffix;
479 }
480 if (flags & ERROR)
481 continue;
482 if (flags & (NEGATIVE|POSITIVE))
483 number = -number;
484 if (opt_info.option[0]=='+')
485 number = -number;
486 continue;
487 case 'f':
488 flags |= FOLLOW;
489 continue;
490 case 'h':
491 if (opt_info.num)
492 flags |= HEADERS;
493 else
494 flags &= ~HEADERS;
495 continue;
496 case 'l':
497 flags |= LINES;
498 if (opt_info.option[0] == '+')
499 number = -number;
500 continue;
501 case 'L':
502 flags |= LOG;
503 continue;
504 case 'q':
505 flags &= ~HEADERS;
506 continue;
507 case 'r':
508 flags |= REVERSE;
509 continue;
510 case 's':
511 flags |= SILENT;
512 continue;
513 case 't':
514 flags |= TIMEOUT;
515 timeout = strelapsed(opt_info.arg, &s, 1);
516 if (*s)
517 error(ERROR_exit(1), "%s: invalid elapsed time [%s]", opt_info.arg, s);
518 continue;
519 case 'v':
520 flags |= VERBOSE;
521 continue;
522 case ':':
523 /* handle old style arguments */
524 if (!(r = argv[opt_info.index]) || !opt_info.offset)
525 {
526 error(2, "%s", opt_info.arg);
527 break;
528 }
529 s = r + opt_info.offset - 1;
530 if (i = *(s - 1) == '-' || *(s - 1) == '+')
531 s--;
532 if ((number = num(s, &t, &flags, 0)) && i)
533 number = -number;
534 goto compatibility;
535 suffix:
536 r = 0;
537 if (opt_info.option[0] == '+')
538 number = -number;
539 compatibility:
540 for (;;)
541 {
542 switch (*t++)
543 {
544 case 0:
545 if (r)
546 opt_info.offset = t - r - 1;
547 break;
548 case 'c':
549 flags &= ~LINES;
550 continue;
551 case 'f':
552 flags |= FOLLOW;
553 continue;
554 case 'l':
555 flags |= LINES;
556 continue;
557 case 'r':
558 flags |= REVERSE;
559 continue;
560 default:
561 error(2, "%s: invalid suffix", t - 1);
562 if (r)
563 opt_info.offset = strlen(r);
564 break;
565 }
566 break;
567 }
568 continue;
569 case '?':
570 error(ERROR_usage(2), "%s", opt_info.arg);
571 break;
572 }
573 break;
574 }
575 argv += opt_info.index;
576 if (!*argv)
577 {
578 flags &= ~HEADERS;
579 if (fstat(0, &st))
580 error(ERROR_system(0), "/dev/stdin: cannot stat");
581 else if (FIFO(st.st_mode))
582 flags &= ~FOLLOW;
583 }
584 else if (!*(argv + 1))
585 flags &= ~HEADERS;
586 delim = (flags & LINES) ? '\n' : -1;
587 if (blocks)
588 number *= blocks;
589 if (flags & REVERSE)
590 {
591 if (delim < 0)
592 error(2, "--reverse requires line mode");
593 if (!(flags & COUNT))
594 number = -1;
595 flags &= ~FOLLOW;
596 }
597 if ((flags & (FOLLOW|TIMEOUT)) == TIMEOUT)
598 {
599 flags &= ~TIMEOUT;
600 timeout = 0;
601 error(ERROR_warn(0), "--timeout ignored for --noforever");
602 }
603 if ((flags & (LOG|TIMEOUT)) == LOG)
604 {
605 flags &= ~LOG;
606 error(ERROR_warn(0), "--log ignored for --notimeout");
607 }
608 if (error_info.errors)
609 error(ERROR_usage(2), "%s", optusage(NiL));
610 if (flags & FOLLOW)
611 {
612 if (!(fp = (Tail_t*)stakalloc(argc * sizeof(Tail_t))))
613 error(ERROR_system(1), "out of space");
614 files = 0;
615 s = *argv;
616 do
617 {
618 fp->name = s;
619 fp->sp = 0;
620 if (!init(fp, number, delim, flags, &format))
621 {
622 fp->expire = timeout ? (NOW + timeout + 1) : 0;
623 if (files)
624 pp->next = fp;
625 else
626 files = fp;
627 pp = fp;
628 fp++;
629 }
630 } while (s && (s = *++argv));
631 if (!files)
632 return error_info.errors != 0;
633 pp->next = 0;
634 hp = 0;
635 n = 1;
636 tv.tv_sec = 1;
637 tv.tv_nsec = 0;
638 while (fp = files)
639 {
640 if (n)
641 n = 0;
642 else if (sh_checksig(context) || tvsleep(&tv, NiL))
643 {
644 error_info.errors++;
645 break;
646 }
647 pp = 0;
648 while (fp)
649 {
650 if (fstat(sffileno(fp->sp), &st))
651 error(ERROR_system(0), "%s: cannot stat", fp->name);
652 else if (fp->fifo || fp->end < st.st_size)
653 {
654 n = 1;
655 if (timeout)
656 fp->expire = NOW + timeout;
657 z = fp->fifo ? SF_UNBOUND : st.st_size - fp->cur;
658 i = 0;
659 if ((s = sfreserve(fp->sp, z, SF_LOCKR)) || (z = sfvalue(fp->sp)) && (s = sfreserve(fp->sp, z, SF_LOCKR)) && (i = 1))
660 {
661 z = sfvalue(fp->sp);
662 for (r = s + z; r > s && *(r - 1) != '\n'; r--);
663 if ((w = r - s) || i && (w = z))
664 {
665 if ((flags & (HEADERS|VERBOSE)) && hp != fp)
666 {
667 hp = fp;
668 sfprintf(sfstdout, format, fp->name);
669 format = header_fmt;
670 }
671 fp->cur += w;
672 sfwrite(sfstdout, s, w);
673 }
674 else
675 w = 0;
676 sfread(fp->sp, s, w);
677 fp->end += w;
678 }
679 goto next;
680 }
681 else if (!timeout || fp->expire > NOW)
682 goto next;
683 else
684 {
685 if (flags & LOG)
686 {
687 i = 3;
688 while (--i && stat(fp->name, &st))
689 if (sh_checksig(context) || tvsleep(&tv, NiL))
690 {
691 error_info.errors++;
692 goto done;
693 }
694 if (i && (fp->dev != st.st_dev || fp->ino != st.st_ino) && !init(fp, 0, 0, flags, &format))
695 {
696 if (!(flags & SILENT))
697 error(ERROR_warn(0), "%s: log file change", fp->name);
698 fp->expire = NOW + timeout;
699 goto next;
700 }
701 }
702 if (!(flags & SILENT))
703 error(ERROR_warn(0), "%s: %s timeout", fp->name, fmtelapsed(timeout, 1));
704 }
705 if (fp->sp && fp->sp != sfstdin)
706 sfclose(fp->sp);
707 if (pp)
708 pp = pp->next = fp->next;
709 else
710 files = files->next;
711 fp = fp->next;
712 continue;
713 next:
714 pp = fp;
715 fp = fp->next;
716 }
717 if (sfsync(sfstdout))
718 error(ERROR_system(1), "write error");
719 }
720 done:
721 for (fp = files; fp; fp = fp->next)
722 if (fp->sp && fp->sp != sfstdin)
723 sfclose(fp->sp);
724 }
725 else
726 {
727 if (file = *argv)
728 argv++;
729 do
730 {
731 if (!file || streq(file, "-"))
732 {
733 file = "/dev/stdin";
734 ip = sfstdin;
735 }
736 else if (!(ip = sfopen(NiL, file, "r")))
737 {
738 error(ERROR_system(0), "%s: cannot open", file);
739 continue;
740 }
741 if (flags & (HEADERS|VERBOSE))
742 {
743 sfprintf(sfstdout, format, file);
744 format = header_fmt;
745 }
746 if (number < 0 || !number && (flags & POSITIVE))
747 {
748 sfset(ip, SF_SHARE, 1);
749 if (number < -1)
750 sfmove(ip, NiL, -number - 1, delim);
751 if (flags & REVERSE)
752 rev_line(ip, sfstdout, sfseek(ip, (Sfoff_t)0, SEEK_CUR));
753 else
754 sfmove(ip, sfstdout, SF_UNBOUND, -1);
755 }
756 else
757 {
758 sfset(ip, SF_SHARE, 0);
759 if ((offset = tailpos(ip, number, delim)) >= 0)
760 {
761 if (flags & REVERSE)
762 rev_line(ip, sfstdout, offset);
763 else
764 {
765 sfseek(ip, offset, SEEK_SET);
766 sfmove(ip, sfstdout, SF_UNBOUND, -1);
767 }
768 }
769 else
770 {
771 op = (flags & REVERSE) ? sftmp(4*SF_BUFSIZE) : sfstdout;
772 pipetail(ip, op, number, delim);
773 if (flags & REVERSE)
774 {
775 sfseek(op, (Sfoff_t)0, SEEK_SET);
776 rev_line(op, sfstdout, (Sfoff_t)0);
777 sfclose(op);
778 }
779 flags = 0;
780 }
781 }
782 if (ip != sfstdin)
783 sfclose(ip);
784 } while ((file = *argv++) && !sh_checksig(context));
785 }
786 return error_info.errors != 0;
787 }
788