445 lines
9 KiB
C
445 lines
9 KiB
C
/*
|
|
* Copyright (c) 2021 Juhani 'nortti' Krekelä.
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*
|
|
* tail.c
|
|
* Output the trailing part of files.
|
|
*/
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <ctype.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <getopt.h>
|
|
#include <inttypes.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdnoreturn.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#ifdef HEAD
|
|
#define TAIL false
|
|
#else
|
|
#define TAIL true
|
|
#endif
|
|
|
|
static bool byte_mode = false;
|
|
static bool beginning_relative = !TAIL;
|
|
static off_t file_offset = 10;
|
|
static size_t buffer_size = 10;
|
|
static bool verbose = false;
|
|
static bool quiet = false;
|
|
static bool follow = false;
|
|
static bool path_mode = false;
|
|
|
|
static void xputchar(int c)
|
|
{
|
|
if ( putchar(c) < 0 )
|
|
err(1, "stdout");
|
|
}
|
|
|
|
static void xputstr(const char* str)
|
|
{
|
|
if ( fputs(str, stdout) < 0 )
|
|
err(1, "stdout");
|
|
}
|
|
|
|
static int from_beginning(FILE* fp, const char* filename)
|
|
{
|
|
int failure = 0;
|
|
|
|
off_t offset = 0;
|
|
while ( true )
|
|
{
|
|
offset++;
|
|
|
|
if ( !TAIL && file_offset < offset )
|
|
break;
|
|
|
|
char* line = NULL;
|
|
size_t size = 0;
|
|
int c = 0;
|
|
ssize_t length = 0;
|
|
if ( byte_mode )
|
|
c = fgetc(fp);
|
|
else
|
|
length = getline(&line, &size, fp);
|
|
if ( c < 0 || length < 0 )
|
|
{
|
|
if ( !feof(fp) )
|
|
{
|
|
warn("%s", filename);
|
|
failure = 1;
|
|
}
|
|
if ( !byte_mode )
|
|
free(line);
|
|
break;
|
|
}
|
|
|
|
if ( (TAIL && file_offset <= offset) ||
|
|
(!TAIL && offset <= file_offset) )
|
|
{
|
|
if ( byte_mode )
|
|
xputchar(c);
|
|
else
|
|
xputstr(line);
|
|
}
|
|
|
|
if ( !byte_mode )
|
|
free(line);
|
|
}
|
|
|
|
return failure;
|
|
}
|
|
|
|
static int from_end(FILE* fp, const char* filename)
|
|
{
|
|
int failure = 0;
|
|
|
|
void* buffer = calloc(buffer_size, byte_mode ? 1 : sizeof(char*));
|
|
if ( !buffer )
|
|
err(1, "calloc");
|
|
char** lines = !byte_mode ? buffer : NULL;
|
|
unsigned char* bytes = byte_mode ? buffer : NULL;
|
|
size_t buffer_used = 0;
|
|
|
|
size_t offset = 0;
|
|
while ( true )
|
|
{
|
|
offset++;
|
|
|
|
char* line = NULL;
|
|
size_t size = 0;
|
|
int c = 0;
|
|
ssize_t length = 0;
|
|
if ( byte_mode )
|
|
c = fgetc(fp);
|
|
else
|
|
length = getline(&line, &size, fp);
|
|
if ( c < 0 || length < 0 )
|
|
{
|
|
if ( !feof(fp) )
|
|
{
|
|
warn("%s", filename);
|
|
failure = 1;
|
|
}
|
|
free(line);
|
|
break;
|
|
}
|
|
|
|
if ( TAIL )
|
|
{
|
|
if ( buffer_size == 0 )
|
|
{
|
|
free(line);
|
|
break;
|
|
}
|
|
|
|
size_t index = offset % buffer_size;
|
|
if ( buffer_used < buffer_size )
|
|
buffer_used++;
|
|
else if ( !byte_mode )
|
|
free(lines[index]);
|
|
|
|
if ( byte_mode )
|
|
bytes[index] = c;
|
|
else
|
|
lines[index] = line;
|
|
}
|
|
else
|
|
{
|
|
if ( buffer_size == 0 )
|
|
{
|
|
if ( byte_mode )
|
|
xputchar(c);
|
|
else
|
|
xputstr(line);
|
|
free(line);
|
|
continue;
|
|
}
|
|
|
|
size_t index = offset % buffer_size;
|
|
if ( buffer_used < buffer_size )
|
|
buffer_used++;
|
|
else
|
|
{
|
|
if ( byte_mode )
|
|
xputchar(bytes[index]);
|
|
else
|
|
{
|
|
xputstr(lines[index]);
|
|
free(lines[index]);
|
|
}
|
|
}
|
|
|
|
if ( byte_mode )
|
|
bytes[index] = c;
|
|
else
|
|
lines[index] = line;
|
|
}
|
|
}
|
|
|
|
if ( TAIL )
|
|
{
|
|
for ( size_t i = 0; i < buffer_used; i++ )
|
|
{
|
|
size_t index = (offset - buffer_used + i) % buffer_size;
|
|
if ( byte_mode )
|
|
xputchar(bytes[index]);
|
|
else
|
|
xputstr(lines[index]);
|
|
}
|
|
}
|
|
|
|
if ( !byte_mode )
|
|
{
|
|
for ( size_t i = 0; i < buffer_used; i++ )
|
|
{
|
|
size_t index = (offset - buffer_used + i) % buffer_size;
|
|
free(lines[index]);
|
|
}
|
|
}
|
|
free(buffer);
|
|
|
|
return failure;
|
|
}
|
|
|
|
static int process_file(FILE* fp, const char* filename)
|
|
{
|
|
int failure = beginning_relative ? from_beginning(fp, filename) :
|
|
from_end(fp, filename);
|
|
|
|
if ( fflush(stdout) != 0 )
|
|
err(1, "fflush");
|
|
|
|
return failure;
|
|
}
|
|
|
|
noreturn static void follow_file(FILE* fp, const char* filename)
|
|
{
|
|
off_t previous_size = ftello(fp);
|
|
if ( previous_size == -1 && errno != ESPIPE )
|
|
warn("ftello: %s", filename);
|
|
int reopen_errno = 0;
|
|
while ( true )
|
|
{
|
|
// Currently we have nothing like kqueue or inotify, so keep polling.
|
|
// This is used also by e.g. 2.9BSD tail(1) and GNU tail(1) as fallback.
|
|
sleep(1);
|
|
|
|
struct stat st;
|
|
if ( fstat(fileno(fp), &st) < 0 )
|
|
err(1, "fstat");
|
|
|
|
if ( st.st_size < previous_size )
|
|
{
|
|
warnx("File truncated: %s", filename);
|
|
if ( fseeko(fp, 0, SEEK_END) < 0 )
|
|
warn("fseeko: %s", filename);
|
|
}
|
|
previous_size = st.st_size;
|
|
|
|
bool reopen_file = false;
|
|
struct stat path_st;
|
|
if ( fp != stdin && path_mode && stat(filename, &path_st) == 0 &&
|
|
(st.st_dev != path_st.st_dev || st.st_ino != path_st.st_ino) )
|
|
{
|
|
reopen_file = true;
|
|
previous_size = path_st.st_size;
|
|
}
|
|
|
|
clearerr(fp);
|
|
while ( true )
|
|
{
|
|
int c = fgetc(fp);
|
|
if ( c < 0 && feof(fp) )
|
|
break;
|
|
else if ( c < 0 )
|
|
err(1, "%s", filename);
|
|
xputchar(c);
|
|
}
|
|
|
|
if ( fflush(stdout) != 0 )
|
|
err(1, "stdout");
|
|
|
|
// Reopen file only after draining the old one, in case there was any
|
|
// change of contents before the file was replaced.
|
|
// Additionally, keep the old file open as long as we can't succesfully
|
|
// open the new one.
|
|
if ( reopen_file )
|
|
{
|
|
FILE* new_fp = fopen(filename, "r");
|
|
if ( new_fp )
|
|
{
|
|
fclose(fp);
|
|
fp = new_fp;
|
|
reopen_errno = 0;
|
|
warnx("reopened: %s", filename);
|
|
}
|
|
else if ( errno != reopen_errno )
|
|
{
|
|
warn("reopening: %s", filename);
|
|
reopen_errno = errno;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void parse_number(const char* num_string)
|
|
{
|
|
// Handle explicit marking of whether it's relative to beginning or end.
|
|
const char* str = num_string;
|
|
|
|
if ( *str == '+' )
|
|
{
|
|
beginning_relative = true;
|
|
str++;
|
|
}
|
|
else if ( *str == '-' )
|
|
{
|
|
beginning_relative = false;
|
|
str++;
|
|
}
|
|
|
|
if ( *str == '+' || *str == '-' )
|
|
errx(1, "Invalid number of %s: %s",
|
|
byte_mode ? "bytes" : "lines", num_string);
|
|
|
|
char* end;
|
|
errno = 0;
|
|
intmax_t parsed = strtoimax(str, &end, 10);
|
|
if ( errno == ERANGE ||
|
|
(beginning_relative && (off_t) parsed != parsed) ||
|
|
(!beginning_relative && (size_t) parsed != (uintmax_t) parsed) )
|
|
errx(1, "Number of %s too large",
|
|
byte_mode ? "bytes" : "lines");
|
|
if ( !*str || *end || errno )
|
|
errx(1, "Invalid number of %s: %s",
|
|
byte_mode ? "bytes" : "lines", num_string);
|
|
|
|
if ( beginning_relative )
|
|
file_offset = parsed;
|
|
else
|
|
buffer_size = parsed;
|
|
}
|
|
|
|
int main(int argc, char* argv[])
|
|
{
|
|
const struct option longopts[] =
|
|
{
|
|
{"bytes", required_argument, NULL, 'c'},
|
|
{"lines", required_argument, NULL, 'n'},
|
|
{"follow", no_argument, NULL, 'f'},
|
|
{"quiet", no_argument, NULL, 'q'},
|
|
{"verbose", no_argument, NULL, 'v'},
|
|
{0, 0, 0, 0}
|
|
};
|
|
#ifndef HEAD
|
|
const char* opts = "c:fFn:qv";
|
|
#else
|
|
const char* opts = "c:n:qv";
|
|
#endif
|
|
while ( true )
|
|
{
|
|
// Handle the historical but still widely used -num option format.
|
|
if ( optind < argc && argv[optind][0] == '-' &&
|
|
isdigit((unsigned char) argv[optind][1]) )
|
|
{
|
|
parse_number(&argv[optind][1]);
|
|
optind++;
|
|
continue;
|
|
}
|
|
|
|
int opt = getopt_long(argc, argv, opts, longopts, NULL);
|
|
if ( opt == -1 )
|
|
break;
|
|
switch ( opt )
|
|
{
|
|
case 'c':
|
|
case 'n':
|
|
byte_mode = opt == 'c';
|
|
parse_number(optarg);
|
|
break;
|
|
case 'f': follow = true; path_mode = false; break;
|
|
case 'F': follow = true; path_mode = true; break;
|
|
case 'q': quiet = true; verbose = false; break;
|
|
case 'v': verbose = true; quiet = false; break;
|
|
default: return 1;
|
|
}
|
|
}
|
|
|
|
if ( follow && 1 < argc - optind )
|
|
errx(1, "-%c cannot be used with multiple files",
|
|
path_mode ? 'F' : 'f');
|
|
|
|
int failure = 0;
|
|
|
|
if ( argc - optind < 1 )
|
|
{
|
|
if ( verbose )
|
|
{
|
|
if ( printf("==> standard input <==\n") < 0 )
|
|
err(1, "stdout");
|
|
}
|
|
|
|
failure |= process_file(stdin, "standard input");
|
|
|
|
// -f is ignored if stdin is a fifo.
|
|
struct stat st;
|
|
if ( fstat(0, &st) < 0 )
|
|
err(1, "fstat");
|
|
if ( follow && !S_ISFIFO(st.st_mode) )
|
|
follow_file(stdin, "standard input");
|
|
}
|
|
else
|
|
{
|
|
for ( int i = optind; i < argc; i++ )
|
|
{
|
|
bool is_stdin = !strcmp(argv[i], "-");
|
|
const char* filename = is_stdin ? "standard input" : argv[i];
|
|
if ( verbose || (!quiet && 1 < argc - optind) )
|
|
{
|
|
if ( printf("%s==> %s <==\n", i == optind ? "" : "\n",
|
|
filename) < 0 )
|
|
err(1, "stdout");
|
|
}
|
|
|
|
FILE* fp = is_stdin ? stdin : fopen(argv[i], "r");
|
|
if ( !fp )
|
|
{
|
|
warn("%s", argv[i]);
|
|
failure = 1;
|
|
continue;
|
|
}
|
|
|
|
failure |= process_file(fp, filename);
|
|
|
|
struct stat st;
|
|
if ( fstat(fileno(fp), &st) < 0 )
|
|
err(1, "fstat");
|
|
if ( follow && (!is_stdin || !S_ISFIFO(st.st_mode)) )
|
|
follow_file(fp, filename);
|
|
|
|
if ( !is_stdin )
|
|
fclose(fp);
|
|
}
|
|
}
|
|
|
|
return failure;
|
|
}
|