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 }