1/* tail.c - copy last lines from input to stdout.
2 *
3 * Copyright 2012 Timothy Elliott <tle@holymonkey.com>
4 *
5 * See http://opengroup.org/onlinepubs/9699919799/utilities/tail.html
6
7USE_TAIL(NEWTOY(tail, "?fc-n-[-cn]", TOYFLAG_USR|TOYFLAG_BIN))
8
9config TAIL
10  bool "tail"
11  default y
12  help
13    usage: tail [-n|c NUMBER] [-f] [FILE...]
14
15    Copy last lines from files to stdout. If no files listed, copy from
16    stdin. Filename "-" is a synonym for stdin.
17
18    -n	output the last NUMBER lines (default 10), +X counts from start.
19    -c	output the last NUMBER bytes, +NUMBER counts from start
20    #-f	follow FILE(s), waiting for more data to be appended [TODO]
21
22config TAIL_SEEK
23  bool "tail seek support"
24  default y
25  depends on TAIL
26  help
27    This version uses lseek, which is faster on large files.
28*/
29
30#define FOR_tail
31#include "toys.h"
32
33GLOBALS(
34  long lines;
35  long bytes;
36
37  int file_no;
38)
39
40struct line_list {
41  struct line_list *next, *prev;
42  char *data;
43  int len;
44};
45
46static struct line_list *get_chunk(int fd, int len)
47{
48  struct line_list *line = xmalloc(sizeof(struct line_list)+len);
49
50  memset(line, 0, sizeof(struct line_list));
51  line->data = ((char *)line) + sizeof(struct line_list);
52  line->len = readall(fd, line->data, len);
53
54  if (line->len < 1) {
55    free(line);
56    return 0;
57  }
58
59  return line;
60}
61
62static void dump_chunk(void *ptr)
63{
64  struct line_list *list = ptr;
65
66  xwrite(1, list->data, list->len);
67  free(list);
68}
69
70// Reading through very large files is slow.  Using lseek can speed things
71// up a lot, but isn't applicable to all input (cat | tail).
72// Note: bytes and lines are negative here.
73static int try_lseek(int fd, long bytes, long lines)
74{
75  struct line_list *list = 0, *temp;
76  int flag = 0, chunk = sizeof(toybuf);
77  ssize_t pos = lseek(fd, 0, SEEK_END);
78
79  // If lseek() doesn't work on this stream, return now.
80  if (pos<0) return 0;
81
82  // Seek to the right spot, output data from there.
83  if (bytes) {
84    if (lseek(fd, bytes, SEEK_END)<0) lseek(fd, 0, SEEK_SET);
85    xsendfile(fd, 1);
86    return 1;
87  }
88
89  // Read from end to find enough lines, then output them.
90
91  bytes = pos;
92  while (lines && pos) {
93    int offset;
94
95    // Read in next chunk from end of file
96    if (chunk>pos) chunk = pos;
97    pos -= chunk;
98    if (pos != lseek(fd, pos, SEEK_SET)) {
99      perror_msg("seek failed");
100      break;
101    }
102    if (!(temp = get_chunk(fd, chunk))) break;
103    temp->next = list;
104    list = temp;
105
106    // Count newlines in this chunk.
107    offset = list->len;
108    while (offset--) {
109      // If the last line ends with a newline, that one doesn't count.
110      if (!flag) flag++;
111
112      // Start outputting data right after newline
113      else if (list->data[offset] == '\n' && !++lines) {
114        offset++;
115        list->data += offset;
116        list->len -= offset;
117
118        break;
119      }
120    }
121  }
122
123  // Output stored data
124  llist_traverse(list, dump_chunk);
125
126  // In case of -f
127  lseek(fd, bytes, SEEK_SET);
128  return 1;
129}
130
131// Called for each file listed on command line, and/or stdin
132static void do_tail(int fd, char *name)
133{
134  long bytes = TT.bytes, lines = TT.lines;
135  int linepop = 1;
136
137  if (toys.optc > 1) {
138    if (TT.file_no++) xputc('\n');
139    xprintf("==> %s <==\n", name);
140  }
141
142  // Are we measuring from the end of the file?
143
144  if (bytes<0 || lines<0) {
145    struct line_list *list = 0, *new;
146
147    // The slow codepath is always needed, and can handle all input,
148    // so make lseek support optional.
149    if (CFG_TAIL_SEEK && try_lseek(fd, bytes, lines)) return;
150
151    // Read data until we run out, keep a trailing buffer
152    for (;;) {
153      // Read next page of data, appending to linked list in order
154      if (!(new = get_chunk(fd, sizeof(toybuf)))) break;
155      dlist_add_nomalloc((void *)&list, (void *)new);
156
157      // If tracing bytes, add until we have enough, discarding overflow.
158      if (TT.bytes) {
159        bytes += new->len;
160        if (bytes > 0) {
161          while (list->len <= bytes) {
162            bytes -= list->len;
163            free(dlist_pop(&list));
164          }
165          list->data += bytes;
166          list->len -= bytes;
167          bytes = 0;
168        }
169      } else {
170        int len = new->len, count;
171        char *try = new->data;
172
173        // First character _after_ a newline starts a new line, which
174        // works even if file doesn't end with a newline
175        for (count=0; count<len; count++) {
176          if (linepop) lines++;
177          linepop = try[count] == '\n';
178
179          if (lines > 0) {
180            char c;
181
182            do {
183              c = *list->data;
184              if (!--(list->len)) free(dlist_pop(&list));
185              else list->data++;
186            } while (c != '\n');
187            lines--;
188          }
189        }
190      }
191    }
192
193    // Output/free the buffer.
194    llist_traverse(list, dump_chunk);
195
196  // Measuring from the beginning of the file.
197  } else for (;;) {
198    int len, offset = 0;
199
200    // Error while reading does not exit.  Error writing does.
201    len = read(fd, toybuf, sizeof(toybuf));
202    if (len<1) break;
203    while (bytes > 1 || lines > 1) {
204      bytes--;
205      if (toybuf[offset++] == '\n') lines--;
206      if (offset >= len) break;
207    }
208    if (offset<len) xwrite(1, toybuf+offset, len-offset);
209  }
210
211  // -f support: cache name/descriptor
212}
213
214void tail_main(void)
215{
216  char **args = toys.optargs;
217
218  if (!(toys.optflags&(FLAG_n|FLAG_c))) {
219    char *arg = *args;
220
221    // handle old "-42" style arguments
222    if (arg && *arg == '-' && arg[1]) {
223      TT.lines = atolx(*(args++));
224      toys.optc--;
225    }
226
227    // if nothing specified, default -n to -10
228    TT.lines = -10;
229  }
230
231  loopfiles(args, do_tail);
232
233  // do -f stuff
234}
235