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 }