github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/cmds/core/tail/tail.go (about)

     1  // Copyright 2012-2022 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Tail prints the lasts 10 lines of a file. Can additionally follow the
     6  // the end of the file as it grows.
     7  //
     8  // Synopsis:
     9  //     tail [-f] [-n lines_to_show] [FILE]
    10  //
    11  // Description:
    12  //     If no files are specified, read from stdin.
    13  //
    14  // Options:
    15  //     -f: follow the end of the file as it grows
    16  //     -n: specify the number of lines to show (default: 10)
    17  
    18  // Missing features:
    19  // - follow-mode (i.e. tail -f)
    20  
    21  package main
    22  
    23  import (
    24  	"bytes"
    25  	"flag"
    26  	"fmt"
    27  	"io"
    28  	"log"
    29  	"os"
    30  	"syscall"
    31  	"time"
    32  )
    33  
    34  var (
    35  	flagFollow   = flag.Bool("f", false, "follow the end of the file")
    36  	flagNumLines = flag.Int("n", 10, "specify the number of lines to show")
    37  )
    38  
    39  type readAtSeeker interface {
    40  	io.ReaderAt
    41  	io.Seeker
    42  }
    43  
    44  // tailConfig is a configuration object for the Tail function
    45  type tailConfig struct {
    46  	// enable follow-mode (-f)
    47  	follow bool
    48  
    49  	// specifies the number of lines to print (-n)
    50  	numLines uint
    51  }
    52  
    53  // getBlockSize returns the number of bytes to read for each ReadAt call. This
    54  // helps minimize the number of syscalls to get the last N lines of the file.
    55  func getBlockSize(numLines uint) int64 {
    56  	// This is currently computed as 81 * N, where N is the requested number of
    57  	// lines, and 81 is a relatively generous estimation of the average line
    58  	// length.
    59  	return 81 * int64(numLines)
    60  }
    61  
    62  // lastNLines finds the n-th-to-last line in `buf`, and returns a new slice
    63  // containing only the last `n` lines. If less lines are found, the input slice
    64  // is returned unmodified.
    65  func lastNLines(buf []byte, n uint) []byte {
    66  	slice := buf
    67  	// `data` contains up to `n` lines of the file
    68  	var data []byte
    69  	if len(slice) != 0 {
    70  		if slice[len(slice)-1] == '\n' {
    71  			// don't consider the last new line for the line count
    72  			slice = slice[:len(slice)-1]
    73  		}
    74  		var (
    75  			foundLines uint
    76  			idx        int
    77  		)
    78  		for {
    79  			if foundLines >= n {
    80  				break
    81  			}
    82  			// find newlines backwards from the end of `slice`
    83  			idx = bytes.LastIndexByte(slice, '\n')
    84  			if idx == -1 {
    85  				// there are less than `n` lines
    86  				break
    87  			}
    88  			foundLines++
    89  			if len(slice) > 1 && slice[idx-1] == '\n' {
    90  				slice = slice[:idx]
    91  			} else {
    92  				slice = slice[:idx-1]
    93  			}
    94  		}
    95  		if idx == -1 {
    96  			// if there are less than `numLines` lines, use all what we have read
    97  			data = buf
    98  		} else {
    99  			data = buf[idx+1:] // +1 to skip the newline belonging to the previous line
   100  		}
   101  	}
   102  	return data
   103  }
   104  
   105  // readLastLinesBackwards reads the last N lines from the provided file, reading
   106  // backwards from the end of the file. This is more efficient than reading from
   107  // the beginning, but can only be done on seekable files, (e.g. this won't work
   108  // on stdin). For non-seekable files see readLastLinesFromBeginning.
   109  // It returns an error, if any. If no error is encountered, the File object's
   110  // offset is positioned after the last read location.
   111  func readLastLinesBackwards(input readAtSeeker, writer io.Writer, numLines uint) error {
   112  	blkSize := getBlockSize(numLines)
   113  	// go to the end of the file
   114  	lastPos, err := input.Seek(0, os.SEEK_END)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	// read block by block backwards until `numLines` lines are found
   119  	readData := make([]byte, 0)
   120  	buf := make([]byte, blkSize)
   121  	pos := lastPos
   122  	var foundLines uint
   123  	// for each block, count how many new lines, until they add up to `numLines`
   124  	for {
   125  		if pos == 0 {
   126  			break
   127  		}
   128  		var thisChunkSize int64
   129  		if pos < blkSize {
   130  			thisChunkSize = pos
   131  		} else {
   132  			thisChunkSize = blkSize
   133  		}
   134  		pos -= thisChunkSize
   135  		n, err := input.ReadAt(buf, pos)
   136  		if err != nil && err != io.EOF {
   137  			return err
   138  		}
   139  		// merge this block to what was read so far
   140  		readData = append(buf[:n], readData...)
   141  		// count how many lines we have so far, and stop reading if we have
   142  		// enough
   143  		foundLines += uint(bytes.Count(buf[:n], []byte{'\n'}))
   144  		if foundLines >= numLines {
   145  			break
   146  		}
   147  	}
   148  	// find the start of the n-th to last line
   149  	data := lastNLines(readData, numLines)
   150  	// write the requested lines to the writer
   151  	if _, err = writer.Write(data); err != nil {
   152  		return err
   153  	}
   154  	// reposition the stream at the end, so the caller can keep reading the file
   155  	// (e.g. when using follow-mode)
   156  	_, err = input.Seek(lastPos, io.SeekStart)
   157  	return err
   158  }
   159  
   160  // readLastLinesFromBeginning reads the last N lines from the provided file,
   161  // reading from the beginning of the file and keeping track of the last N lines.
   162  // This is necessary for files that are not seekable (e.g. stdin), but it's less
   163  // efficient. For an efficient alternative that works on seekable files see
   164  // readLastLinesBackwards.
   165  // It returns an error, if any. If no error is encountered, the File object's
   166  // offset is positioned after the last read location.
   167  func readLastLinesFromBeginning(input io.ReadSeeker, writer io.Writer, numLines uint) error {
   168  	blkSize := getBlockSize(numLines)
   169  	// read block by block until EOF and store a reference to the last lines
   170  	buf := make([]byte, blkSize)
   171  	var (
   172  		slice      []byte // will hold the final data, after moving line by line
   173  		foundLines uint
   174  	)
   175  	for {
   176  		n, err := io.ReadFull(input, buf)
   177  		if err != nil {
   178  			if err == io.EOF {
   179  				break
   180  			}
   181  			if err != io.ErrUnexpectedEOF {
   182  				return err
   183  			}
   184  		}
   185  		// look for newlines and keep a slice starting at the n-th to last line
   186  		// (no further than numLines)
   187  		foundLines += uint(bytes.Count(buf[:n], []byte{'\n'}))
   188  		slice = append(slice, buf[:n]...) // this is the slice that points to the wanted lines
   189  		// process the current slice
   190  		slice = lastNLines(slice, numLines)
   191  	}
   192  	if _, err := writer.Write(slice); err != nil {
   193  		return err
   194  	}
   195  	return nil
   196  }
   197  
   198  func isTruncated(file *os.File) (bool, error) {
   199  	// current read position in a file
   200  	currentPos, err := file.Seek(0, io.SeekCurrent)
   201  	if err != nil {
   202  		return false, err
   203  	}
   204  	// file stat to get the size
   205  	fileInfo, err := file.Stat()
   206  	if err != nil {
   207  		return false, err
   208  	}
   209  	return currentPos > fileInfo.Size(), nil
   210  }
   211  
   212  // tail reads the last N lines from the input File and writes them to the Writer.
   213  // The tailConfig object allows to specify the precise behaviour.
   214  func tail(inFile *os.File, writer io.Writer, config tailConfig) error {
   215  	// try reading from the end of the file
   216  	retryFromBeginning := false
   217  	err := readLastLinesBackwards(inFile, writer, config.numLines)
   218  	if err != nil {
   219  		// if it failed because it couldn't seek, mark it for retry reading from
   220  		// the beginning
   221  		if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ESPIPE {
   222  			retryFromBeginning = true
   223  		} else {
   224  			return err
   225  		}
   226  	}
   227  	// if reading backwards failed because the file is not seekable,
   228  	// retry from the beginning
   229  	if retryFromBeginning {
   230  		if err = readLastLinesFromBeginning(inFile, writer, config.numLines); err != nil {
   231  			return err
   232  		}
   233  	}
   234  	if config.follow {
   235  		blkSize := getBlockSize(1)
   236  		// read block by block until EOF and store a reference to the last lines
   237  		buf := make([]byte, blkSize)
   238  		for {
   239  			_, err = inFile.Read(buf)
   240  			if err == io.EOF {
   241  				// without this sleep you would hogg the CPU
   242  				time.Sleep(500 * time.Millisecond)
   243  				// truncated ?
   244  				truncated, errTruncated := isTruncated(inFile)
   245  				if errTruncated != nil {
   246  					break
   247  				}
   248  				if truncated {
   249  					// seek from start
   250  					_, errSeekStart := inFile.Seek(0, io.SeekStart)
   251  					if errSeekStart != nil {
   252  						break
   253  					}
   254  				}
   255  				continue
   256  			}
   257  			break
   258  		}
   259  	}
   260  	return nil
   261  }
   262  
   263  func run(reader *os.File, writer io.Writer, args []string) error {
   264  	var (
   265  		inFile *os.File
   266  		err    error
   267  	)
   268  	switch len(args) {
   269  	case 0:
   270  		inFile = reader
   271  	case 1:
   272  		inFile, err = os.Open(args[0])
   273  		if err != nil {
   274  			return err
   275  		}
   276  	default:
   277  		// TODO support multiple files
   278  		return fmt.Errorf("tail: can only read one file at a time")
   279  	}
   280  
   281  	// TODO: add support for parsing + (from beggining of the file)
   282  	// negative sign is the same as none
   283  	if *flagNumLines < 0 {
   284  		*flagNumLines = -1 * *flagNumLines
   285  	}
   286  	config := tailConfig{follow: *flagFollow, numLines: uint(*flagNumLines)}
   287  	return tail(inFile, writer, config)
   288  }
   289  
   290  func main() {
   291  	flag.Parse()
   292  	if err := run(os.Stdin, os.Stdout, flag.Args()); err != nil {
   293  		log.Fatalf("tail: %v", err)
   294  	}
   295  }