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
30static const char usage[] =
31"+[-?\n@(#)$Id: tail (AT&T Research) 2012-06-19 $\n]"
32USAGE_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
131struct Tail_s; typedef struct Tail_s Tail_t;
132
133struct 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
146static 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
153static Sfoff_t
154tailpos(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
200static void
201pipetail(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
256static int
257init(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
349static intmax_t
350num(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
400int
401b_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