gopkg.in/hugelgupf/u-root.v2@v2.0.0-20180831055005-3f8fdb0ce09d/cmds/tail/tail.go (about)

     1  // Copyright 2012-2017 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  )
    32  
    33  var (
    34  	flagFollow   = flag.Bool("f", false, "follow the end of the file")
    35  	flagNumLines = flag.Int("n", 10, "specify the number of lines to show")
    36  )
    37  
    38  type ReadAtSeeker interface {
    39  	io.ReaderAt
    40  	io.Seeker
    41  }
    42  
    43  // TailConfig is a configuration object for the Tail function
    44  type TailConfig struct {
    45  	// enable follow-mode (-f)
    46  	follow bool
    47  
    48  	// specifies the number of lines to print (-n)
    49  	numLines uint
    50  }
    51  
    52  // getBlockSize returns the number of bytes to read for each ReadAt call. This
    53  // helps minimize the number of syscalls to get the last N lines of the file.
    54  func getBlockSize(numLines uint) int64 {
    55  	// This is currently computed as 81 * N, where N is the requested number of
    56  	// lines, and 81 is a relatively generous estimation of the average line
    57  	// length.
    58  	return 81 * int64(numLines)
    59  }
    60  
    61  // lastNLines finds the n-th-to-last line in `buf`, and returns a new slice
    62  // containing only the last `n` lines. If less lines are found, the input slice
    63  // is returned unmodified.
    64  func lastNLines(buf []byte, n uint) []byte {
    65  	slice := buf
    66  	// `data` contains up to `n` lines of the file
    67  	var data []byte
    68  	if len(slice) != 0 {
    69  		if slice[len(slice)-1] == '\n' {
    70  			// don't consider the last new line for the line count
    71  			slice = slice[:len(slice)-1]
    72  		}
    73  		var (
    74  			foundLines uint
    75  			idx        int
    76  		)
    77  		for {
    78  			if foundLines >= n {
    79  				break
    80  			}
    81  			// find newlines backwards from the end of `slice`
    82  			idx = bytes.LastIndexByte(slice, '\n')
    83  			if idx == -1 {
    84  				// there are less than `n` lines
    85  				break
    86  			}
    87  			foundLines++
    88  			slice = slice[:idx-1]
    89  		}
    90  		if idx == -1 {
    91  			// if there are less than `numLines` lines, use all what we have read
    92  			data = buf
    93  		} else {
    94  			data = buf[idx+1:] // +1 to skip the newline belonging to the previous line
    95  		}
    96  	}
    97  	return data
    98  }
    99  
   100  // readLastLinesBackwards reads the last N lines from the provided file, reading
   101  // backwards from the end of the file. This is more efficient than reading from
   102  // the beginning, but can only be done on seekable files, (e.g. this won't work
   103  // on stdin). For non-seekable files see readLastLinesFromBeginning.
   104  // It returns an error, if any. If no error is encountered, the File object's
   105  // offset is positioned after the last read location.
   106  func readLastLinesBackwards(input ReadAtSeeker, writer io.Writer, numLines uint) error {
   107  	blkSize := getBlockSize(numLines)
   108  	// go to the end of the file
   109  	lastPos, err := input.Seek(0, os.SEEK_END)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	// read block by block backwards until `numLines` lines are found
   114  	readData := make([]byte, 0)
   115  	buf := make([]byte, blkSize)
   116  	pos := lastPos
   117  	var foundLines uint
   118  	// for each block, count how many new lines, until they add up to `numLines`
   119  	for {
   120  		if pos == 0 {
   121  			break
   122  		}
   123  		var thisChunkSize int64
   124  		if pos < blkSize {
   125  			thisChunkSize = pos
   126  		} else {
   127  			thisChunkSize = blkSize
   128  		}
   129  		pos -= thisChunkSize
   130  		n, err := input.ReadAt(buf, pos)
   131  		if err != nil && err != io.EOF {
   132  			return err
   133  		}
   134  		// merge this block to what was read so far
   135  		readData = append(buf[:n], readData...)
   136  		// count how many lines we have so far, and stop reading if we have
   137  		// enough
   138  		foundLines += uint(bytes.Count(buf[:n], []byte{'\n'}))
   139  		if foundLines >= numLines {
   140  			break
   141  		}
   142  	}
   143  	// find the start of the n-th to last line
   144  	data := lastNLines(readData, numLines)
   145  	// write the requested lines to the writer
   146  	if _, err = writer.Write(data); err != nil {
   147  		return err
   148  	}
   149  	// reposition the stream at the end, so the caller can keep reading the file
   150  	// (e.g. when using follow-mode)
   151  	_, err = input.Seek(lastPos, os.SEEK_SET)
   152  	return err
   153  }
   154  
   155  // readLastLinesFromBeginning reads the last N lines from the provided file,
   156  // reading from the beginning of the file and keeping track of the last N lines.
   157  // This is necessary for files that are not seekable (e.g. stdin), but it's less
   158  // efficient. For an efficient alternative that works on seekable files see
   159  // readLastLinesBackwards.
   160  // It returns an error, if any. If no error is encountered, the File object's
   161  // offset is positioned after the last read location.
   162  func readLastLinesFromBeginning(input io.ReadSeeker, writer io.Writer, numLines uint) error {
   163  	blkSize := getBlockSize(numLines)
   164  	// read block by block until EOF and store a reference to the last lines
   165  	buf := make([]byte, blkSize)
   166  	var (
   167  		slice      []byte // will hold the final data, after moving line by line
   168  		foundLines uint
   169  	)
   170  	for {
   171  		n, err := io.ReadFull(input, buf)
   172  		if err != nil {
   173  			if err == io.EOF {
   174  				break
   175  			}
   176  			if err != io.ErrUnexpectedEOF {
   177  				return err
   178  			}
   179  		}
   180  		// look for newlines and keep a slice starting at the n-th to last line
   181  		// (no further than numLines)
   182  		foundLines += uint(bytes.Count(buf[:n], []byte{'\n'}))
   183  		slice = append(slice, buf[:n]...) // this is the slice that points to the wanted lines
   184  		// process the current slice
   185  		slice = lastNLines(slice, numLines)
   186  	}
   187  	if _, err := writer.Write(slice); err != nil {
   188  		return err
   189  	}
   190  	return nil
   191  }
   192  
   193  // Tail reads the last N lines from the input File and writes them to the Writer.
   194  // The TailConfig object allows to specify the precise behaviour.
   195  func Tail(inFile *os.File, writer io.Writer, config TailConfig) error {
   196  	if config.follow {
   197  		return fmt.Errorf("follow-mode not implemented yet")
   198  	}
   199  	if inFile == nil {
   200  		return fmt.Errorf("No input file specified")
   201  	}
   202  	// try reading from the end of the file
   203  	retryFromBeginning := false
   204  	err := readLastLinesBackwards(inFile, writer, config.numLines)
   205  	if err != nil {
   206  		// if it failed because it couldn't seek, mark it for retry reading from
   207  		// the beginning
   208  		if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ESPIPE {
   209  			retryFromBeginning = true
   210  		} else {
   211  			return err
   212  		}
   213  	}
   214  	// if reading backwards failed because the file is not seekable,
   215  	// retry from the beginning
   216  	if retryFromBeginning {
   217  		if err = readLastLinesFromBeginning(inFile, writer, config.numLines); err != nil {
   218  			return err
   219  		}
   220  	}
   221  	return nil
   222  }
   223  
   224  func main() {
   225  	flag.Parse()
   226  
   227  	var (
   228  		inFile *os.File
   229  		writer = os.Stdout
   230  		err    error
   231  	)
   232  	switch nArgs := len(flag.Args()); nArgs {
   233  	case 0:
   234  		inFile = os.Stdin
   235  	case 1:
   236  		inFile, err = os.Open(flag.Args()[0])
   237  		if err != nil {
   238  			log.Fatal(err)
   239  		}
   240  	default:
   241  		// TODO support multiple files
   242  		log.Fatal("tail: can only read one file at a time")
   243  	}
   244  
   245  	if *flagNumLines < 0 {
   246  		log.Fatalf("The number of lines cannot be negative")
   247  	}
   248  	config := TailConfig{follow: *flagFollow, numLines: uint(*flagNumLines)}
   249  	if err := Tail(inFile, writer, config); err != nil {
   250  		log.Fatalf("tail: %v", err)
   251  	}
   252  }