Skip to main content

uu_tail/
tail.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (ToDO) seekable seek'd tail'ing ringbuffer ringbuf unwatch
7// spell-checker:ignore (ToDO) Uncategorized filehandle Signum memrchr
8// spell-checker:ignore (libs) kqueue
9// spell-checker:ignore (acronyms)
10// spell-checker:ignore (env/flags)
11// spell-checker:ignore (jargon) tailable untailable stdlib
12// spell-checker:ignore (names)
13// spell-checker:ignore (shell/tools)
14// spell-checker:ignore (misc)
15
16pub mod args;
17pub mod chunks;
18mod follow;
19mod parse;
20mod paths;
21mod platform;
22pub mod text;
23
24pub use args::uu_app;
25use args::{FilterMode, Settings, Signum, parse_args};
26use chunks::ReverseChunks;
27use follow::Observer;
28use memchr::{memchr_iter, memrchr_iter};
29use paths::{FileExtTail, HeaderPrinter, Input, InputKind};
30use same_file::Handle;
31use std::cmp::Ordering;
32use std::fs::File;
33use std::io::{self, BufReader, BufWriter, ErrorKind, Read, Seek, SeekFrom, Write, stdin, stdout};
34use std::path::{Path, PathBuf};
35use uucore::display::Quotable;
36use uucore::error::{FromIo, UResult, USimpleError, set_exit_code};
37use uucore::translate;
38
39use uucore::{show, show_error};
40
41#[uucore::main]
42pub fn uumain(args: impl uucore::Args) -> UResult<()> {
43    let settings = parse_args(args)?;
44
45    settings.check_warnings();
46
47    match settings.verify() {
48        args::VerificationResult::CannotFollowStdinByName => {
49            return Err(USimpleError::new(
50                1,
51                translate!("tail-error-cannot-follow-stdin-by-name", "stdin" => text::DASH.quote()),
52            ));
53        }
54        // Exit early if we do not output anything. Note, that this may break a pipe
55        // when tail is on the receiving side.
56        args::VerificationResult::NoOutput => return Ok(()),
57        args::VerificationResult::Ok => {}
58    }
59
60    uu_tail(&settings)
61}
62
63fn uu_tail(settings: &Settings) -> UResult<()> {
64    let mut printer = HeaderPrinter::new(settings.verbose, true);
65    let mut observer = Observer::from(settings);
66
67    observer.start(settings)?;
68
69    // Print debug info about the follow implementation being used
70    if settings.debug && settings.follow.is_some() {
71        if observer.use_polling {
72            show_error!("{}", translate!("tail-debug-using-polling-mode"));
73        } else {
74            show_error!("{}", translate!("tail-debug-using-notification-mode"));
75        }
76    }
77
78    // Do an initial tail print of each path's content.
79    // Add `path` and `reader` to `files` map if `--follow` is selected.
80    for input in &settings.inputs.clone() {
81        match input.kind() {
82            InputKind::Stdin => {
83                tail_stdin(settings, &mut printer, input, &mut observer)?;
84            }
85            InputKind::File(path) if cfg!(unix) && path == &PathBuf::from(text::DEV_STDIN) => {
86                tail_stdin(settings, &mut printer, input, &mut observer)?;
87            }
88            InputKind::File(path) => {
89                tail_file(settings, &mut printer, input, path, &mut observer, 0)?;
90            }
91        }
92    }
93
94    if settings.follow.is_some() {
95        /*
96        POSIX specification regarding tail -f
97        If the input file is a regular file or if the file operand specifies a FIFO, do not
98        terminate after the last line of the input file has been copied, but read and copy
99        further bytes from the input file when they become available. If no file operand is
100        specified and standard input is a pipe or FIFO, the -f option shall be ignored. If
101        the input file is not a FIFO, pipe, or regular file, it is unspecified whether or
102        not the -f option shall be ignored.
103        */
104        if !settings.has_only_stdin() || settings.pid != 0 {
105            follow::follow(observer, settings)?;
106        }
107    }
108
109    Ok(())
110}
111
112fn tail_file(
113    settings: &Settings,
114    header_printer: &mut HeaderPrinter,
115    input: &Input,
116    path: &Path,
117    observer: &mut Observer,
118    offset: u64,
119) -> UResult<()> {
120    let md = path.metadata();
121    if let Err(ref e) = md {
122        if e.kind() == ErrorKind::NotFound {
123            set_exit_code(1);
124            show_error!(
125                "{}",
126                translate!(
127                    "tail-error-cannot-open-no-such-file",
128                    "file" => input.display_name.clone(),
129                    "error" => translate!("tail-no-such-file-or-directory")
130                )
131            );
132            observer.add_bad_path(path, input.display_name.as_str(), false)?;
133            return Ok(());
134        }
135    }
136
137    if path.is_dir() {
138        set_exit_code(1);
139
140        header_printer.print_input(input);
141        let err_msg = translate!("tail-is-a-directory");
142
143        show_error!(
144            "{}",
145            translate!("tail-error-reading-file", "file" => input.display_name.clone(), "error" => err_msg)
146        );
147        if settings.follow.is_some() {
148            let msg = if settings.retry {
149                ""
150            } else {
151                &translate!("tail-giving-up-on-this-name")
152            };
153            show_error!(
154                "{}",
155                translate!("tail-error-cannot-follow-file-type", "file" => input.display_name.clone(), "msg" => msg)
156            );
157        }
158        if !observer.follow_name_retry() {
159            return Ok(());
160        }
161        observer.add_bad_path(path, input.display_name.as_str(), false)?;
162    } else {
163        #[cfg(unix)]
164        let open_result = open_file(path, settings.pid != 0);
165        #[cfg(not(unix))]
166        let open_result = File::open(path);
167
168        match open_result {
169            Ok(mut file) => {
170                let st = file.metadata()?;
171                let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st);
172                header_printer.print_input(input);
173                let mut reader;
174                if !settings.presume_input_pipe
175                    && file.is_seekable(if input.is_stdin() { offset } else { 0 })
176                    && (!st.is_file() || st.len() > blksize_limit)
177                {
178                    bounded_tail(&mut file, settings);
179                    reader = BufReader::new(file);
180                } else {
181                    reader = BufReader::new(file);
182                    unbounded_tail(&mut reader, settings)?;
183                }
184                if input.is_tailable() {
185                    observer.add_path(
186                        path,
187                        input.display_name.as_str(),
188                        Some(Box::new(reader)),
189                        true,
190                    )?;
191                } else {
192                    observer.add_bad_path(path, input.display_name.as_str(), false)?;
193                }
194            }
195            Err(e) if e.kind() == ErrorKind::PermissionDenied => {
196                observer.add_bad_path(path, input.display_name.as_str(), false)?;
197                show!(e.map_err_context(|| {
198                    translate!("tail-error-cannot-open-for-reading", "file" => input.display_name.clone())
199                }));
200            }
201            Err(e) => {
202                observer.add_bad_path(path, input.display_name.as_str(), false)?;
203                return Err(e.map_err_context(|| {
204                    translate!("tail-error-cannot-open-for-reading", "file" => input.display_name.clone())
205                }));
206            }
207        }
208    }
209
210    Ok(())
211}
212
213/// Opens a file, using non-blocking mode for FIFOs when `use_nonblock_for_fifo` is true.
214///
215/// When opening a FIFO with `--pid`, we need to use O_NONBLOCK so that:
216/// 1. The open() call doesn't block waiting for a writer
217/// 2. We can periodically check if the monitored process is still alive
218///
219/// After opening, we clear O_NONBLOCK so subsequent reads block normally.
220/// Without `--pid`, FIFOs block on open() until a writer connects (GNU behavior).
221#[cfg(unix)]
222fn open_file(path: &Path, use_nonblock_for_fifo: bool) -> io::Result<File> {
223    use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
224    use std::fs::OpenOptions;
225    use std::os::fd::AsFd;
226    use std::os::unix::fs::{FileTypeExt, OpenOptionsExt};
227
228    let is_fifo = path
229        .metadata()
230        .ok()
231        .is_some_and(|m| m.file_type().is_fifo());
232
233    if is_fifo && use_nonblock_for_fifo {
234        let file = OpenOptions::new()
235            .read(true)
236            .custom_flags(libc::O_NONBLOCK)
237            .open(path)?;
238
239        // Clear O_NONBLOCK so reads block normally
240        let flags = fcntl_getfl(file.as_fd())?;
241        let new_flags = flags & !OFlags::NONBLOCK;
242        fcntl_setfl(file.as_fd(), new_flags)?;
243
244        Ok(file)
245    } else {
246        File::open(path)
247    }
248}
249
250fn tail_stdin(
251    settings: &Settings,
252    header_printer: &mut HeaderPrinter,
253    input: &Input,
254    observer: &mut Observer,
255) -> UResult<()> {
256    // on macOS, resolve() will always return None for stdin,
257    // we need to detect if stdin is a directory ourselves.
258    // fstat-ing certain descriptors under /dev/fd fails with
259    // bad file descriptor or might not catch directory cases
260    // e.g. see the differences between running ls -l /dev/stdin /dev/fd/0
261    // on macOS and Linux.
262    #[cfg(target_os = "macos")]
263    {
264        if let Ok(mut stdin_handle) = Handle::stdin() {
265            if let Ok(meta) = stdin_handle.as_file_mut().metadata() {
266                if meta.file_type().is_dir() {
267                    set_exit_code(1);
268                    show_error!(
269                        "{}",
270                        translate!("tail-error-cannot-open-no-such-file", "file" => input.display_name.clone(), "error" => translate!("tail-no-such-file-or-directory"))
271                    );
272                    return Ok(());
273                }
274            }
275        }
276    }
277
278    // Check if stdin was closed before Rust reopened it as /dev/null
279    if paths::stdin_is_bad_fd() {
280        set_exit_code(1);
281        show_error!(
282            "{}",
283            translate!("tail-error-cannot-fstat", "file" => translate!("tail-stdin-header").quote(), "error" => translate!("tail-bad-fd"))
284        );
285        show_error!("{}", translate!("tail-no-files-remaining"));
286        return Ok(());
287    }
288
289    if let Some(path) = input.resolve() {
290        // fifo
291        let mut stdin_offset = 0;
292        if cfg!(unix) {
293            // Save the current seek position/offset of a stdin redirected file.
294            // This is needed to pass "gnu/tests/tail-2/start-middle.sh"
295            if let Ok(mut stdin_handle) = Handle::stdin() {
296                if let Ok(offset) = stdin_handle.as_file_mut().stream_position() {
297                    stdin_offset = offset;
298                }
299            }
300        }
301        tail_file(
302            settings,
303            header_printer,
304            input,
305            &path,
306            observer,
307            stdin_offset,
308        )?;
309    } else {
310        // pipe
311        header_printer.print_input(input);
312        if paths::stdin_is_bad_fd() {
313            set_exit_code(1);
314            show_error!(
315                "{}",
316                translate!("tail-error-cannot-fstat", "file" => translate!("tail-stdin-header"), "error" => translate!("tail-bad-fd"))
317            );
318            if settings.follow.is_some() {
319                show_error!(
320                    "{}",
321                    translate!("tail-error-reading-file", "file" => translate!("tail-stdin-header"), "error" => translate!("tail-bad-fd"))
322                );
323            }
324        } else {
325            let mut reader = BufReader::new(stdin());
326            unbounded_tail(&mut reader, settings)?;
327        }
328    }
329
330    Ok(())
331}
332
333/// Find the index after the given number of instances of a given byte.
334///
335/// This function reads through a given reader until `num_delimiters`
336/// instances of `delimiter` have been seen, returning the index of
337/// the byte immediately following that delimiter. If there are fewer
338/// than `num_delimiters` instances of `delimiter`, this returns the
339/// total number of bytes read from the `reader` until EOF.
340///
341/// # Errors
342///
343/// This function returns an error if there is an error during reading
344/// from `reader`.
345///
346/// # Examples
347///
348/// Basic usage:
349///
350/// ```rust,ignore
351/// use std::io::Cursor;
352///
353/// let mut reader = Cursor::new("a\nb\nc\nd\ne\n");
354/// let i = forwards_thru_file(&mut reader, 2, b'\n').unwrap();
355/// assert_eq!(i, 4);
356/// ```
357///
358/// If `num_delimiters` is zero, then this function always returns
359/// zero:
360///
361/// ```rust,ignore
362/// use std::io::Cursor;
363///
364/// let mut reader = Cursor::new("a\n");
365/// let i = forwards_thru_file(&mut reader, 0, b'\n').unwrap();
366/// assert_eq!(i, 0);
367/// ```
368///
369/// If there are fewer than `num_delimiters` instances of `delimiter`
370/// in the reader, then this function returns the total number of
371/// bytes read:
372///
373/// ```rust,ignore
374/// use std::io::Cursor;
375///
376/// let mut reader = Cursor::new("a\n");
377/// let i = forwards_thru_file(&mut reader, 2, b'\n').unwrap();
378/// assert_eq!(i, 2);
379/// ```
380fn forwards_thru_file(
381    reader: &mut impl Read,
382    num_delimiters: u64,
383    delimiter: u8,
384) -> io::Result<usize> {
385    // If num_delimiters == 0, always return 0.
386    if num_delimiters == 0 {
387        return Ok(0);
388    }
389    // Use a 32K buffer.
390    let mut buf = [0; 32 * 1024];
391    let mut total = 0;
392    let mut count = 0;
393    // Iterate through the input, using `count` to record the number of times `delimiter`
394    // is seen. Once we find `num_delimiters` instances, return the offset of the byte
395    // immediately following that delimiter.
396    loop {
397        match reader.read(&mut buf) {
398            // Ok(0) => EoF before we found `num_delimiters` instance of `delimiter`.
399            // Return the total number of bytes read in that case.
400            Ok(0) => return Ok(total),
401            Ok(n) => {
402                // Use memchr_iter since it greatly improves search performance.
403                for offset in memchr_iter(delimiter, &buf[..n]) {
404                    count += 1;
405                    if count == num_delimiters {
406                        // Return offset of the byte after the `delimiter` instance.
407                        return Ok(total + offset + 1);
408                    }
409                }
410                total += n;
411            }
412            Err(e) if e.kind() == ErrorKind::Interrupted => (),
413            Err(e) => return Err(e),
414        }
415    }
416}
417
418/// Iterate over bytes in the file, in reverse, until we find the
419/// `num_delimiters` instance of `delimiter`. The `file` is left seek'd to the
420/// position just after that delimiter.
421fn backwards_thru_file(file: &mut File, num_delimiters: u64, delimiter: u8) {
422    if num_delimiters == 0 {
423        file.seek(SeekFrom::End(0)).unwrap();
424        return;
425    }
426    // This variable counts the number of delimiters found in the file
427    // so far (reading from the end of the file toward the beginning).
428    let mut counter = 0;
429    let mut first_slice = true;
430    for slice in ReverseChunks::new(file) {
431        // Iterate over each byte in the slice in reverse order.
432        let mut iter = memrchr_iter(delimiter, &slice);
433
434        // Ignore a trailing newline in the last block, if there is one.
435        if first_slice {
436            if let Some(c) = slice.last() {
437                if *c == delimiter {
438                    iter.next();
439                }
440            }
441            first_slice = false;
442        }
443
444        // For each byte, increment the count of the number of
445        // delimiters found. If we have found more than the specified
446        // number of delimiters, terminate the search and seek to the
447        // appropriate location in the file.
448        for i in iter {
449            counter += 1;
450            if counter >= num_delimiters {
451                // We should never over-count - assert that.
452                assert_eq!(counter, num_delimiters);
453                // After each iteration of the outer loop, the
454                // cursor in the file is at the *beginning* of the
455                // block, so seeking forward by `i + 1` bytes puts
456                // us right after the found delimiter.
457                file.seek(SeekFrom::Current((i + 1) as i64)).unwrap();
458                return;
459            }
460        }
461    }
462}
463
464/// When tail'ing a file, we do not need to read the whole file from start to
465/// finish just to find the last n lines or bytes. Instead, we can seek to the
466/// end of the file, and then read the file "backwards" in blocks of size
467/// `BLOCK_SIZE` until we find the location of the first line/byte. This ends up
468/// being a nice performance win for very large files.
469fn bounded_tail(file: &mut File, settings: &Settings) {
470    debug_assert!(!settings.presume_input_pipe);
471    let mut limit = None;
472
473    // Find the position in the file to start printing from.
474    match &settings.mode {
475        FilterMode::Lines(Signum::Negative(count), delimiter) => {
476            backwards_thru_file(file, *count, *delimiter);
477        }
478        FilterMode::Lines(Signum::Positive(count), delimiter) if count > &1 => {
479            let i = forwards_thru_file(file, *count - 1, *delimiter).unwrap();
480            file.seek(SeekFrom::Start(i as u64)).unwrap();
481        }
482        FilterMode::Lines(Signum::MinusZero, _) => {
483            file.seek(SeekFrom::End(0)).unwrap();
484        }
485        FilterMode::Bytes(Signum::Negative(count)) => {
486            if file.seek(SeekFrom::End(-(*count as i64))).is_err() {
487                file.seek(SeekFrom::Start(0)).unwrap();
488            }
489            limit = Some(*count);
490        }
491        FilterMode::Bytes(Signum::Positive(count)) if count > &1 => {
492            // GNU `tail` seems to index bytes and lines starting at 1, not
493            // at 0. It seems to treat `+0` and `+1` as the same thing.
494            file.seek(SeekFrom::Start(*count - 1)).unwrap();
495        }
496        FilterMode::Bytes(Signum::MinusZero) => {
497            file.seek(SeekFrom::End(0)).unwrap();
498        }
499        _ => {}
500    }
501
502    print_target_section(file, limit);
503}
504
505fn unbounded_tail<T: Read>(reader: &mut BufReader<T>, settings: &Settings) -> UResult<()> {
506    let mut writer = BufWriter::new(stdout().lock());
507    match &settings.mode {
508        FilterMode::Lines(Signum::Negative(count), sep) => {
509            let mut chunks = chunks::LinesChunkBuffer::new(*sep, *count);
510            chunks.fill(reader)?;
511            chunks.write(&mut writer)?;
512        }
513        FilterMode::Lines(Signum::PlusZero | Signum::Positive(1), _) => {
514            io::copy(reader, &mut writer)?;
515        }
516        FilterMode::Lines(Signum::Positive(count), sep) => {
517            let mut num_skip = *count - 1;
518            let mut chunk = chunks::LinesChunk::new(*sep);
519            while chunk.fill(reader)?.is_some() {
520                let lines = chunk.get_lines() as u64;
521                if lines < num_skip {
522                    num_skip -= lines;
523                } else {
524                    break;
525                }
526            }
527            if chunk.has_data() {
528                chunk.write_lines(&mut writer, num_skip as usize)?;
529                io::copy(reader, &mut writer)?;
530            }
531        }
532        FilterMode::Bytes(Signum::Negative(count)) => {
533            let mut chunks = chunks::BytesChunkBuffer::new(*count);
534            chunks.fill(reader)?;
535            chunks.print(&mut writer)?;
536        }
537        FilterMode::Lines(Signum::MinusZero, sep) => {
538            let mut chunks = chunks::LinesChunkBuffer::new(*sep, 0);
539            chunks.fill(reader)?;
540            chunks.write(&mut writer)?;
541        }
542        FilterMode::Bytes(Signum::PlusZero | Signum::Positive(1)) => {
543            io::copy(reader, &mut writer)?;
544        }
545        FilterMode::Bytes(Signum::Positive(count)) => {
546            let mut num_skip = *count - 1;
547            let mut chunk = chunks::BytesChunk::new();
548            loop {
549                if let Some(bytes) = chunk.fill(reader)? {
550                    let bytes: u64 = bytes as u64;
551                    match bytes.cmp(&num_skip) {
552                        Ordering::Less => num_skip -= bytes,
553                        Ordering::Equal => {
554                            break;
555                        }
556                        Ordering::Greater => {
557                            writer.write_all(chunk.get_buffer_with(num_skip as usize))?;
558                            break;
559                        }
560                    }
561                } else {
562                    return Ok(());
563                }
564            }
565
566            io::copy(reader, &mut writer)?;
567        }
568        _ => {}
569    }
570    #[cfg(not(target_os = "windows"))]
571    writer.flush()?;
572
573    // SIGPIPE is not available on Windows.
574    #[cfg(target_os = "windows")]
575    writer.flush().inspect_err(|err| {
576        if err.kind() == ErrorKind::BrokenPipe {
577            std::process::exit(13);
578        }
579    })?;
580    Ok(())
581}
582
583fn print_target_section<R>(file: &mut R, limit: Option<u64>)
584where
585    R: Read + ?Sized,
586{
587    // Print the target section of the file.
588    let stdout = stdout();
589    let mut stdout = stdout.lock();
590    if let Some(limit) = limit {
591        let mut reader = file.take(limit);
592        io::copy(&mut reader, &mut stdout).unwrap();
593    } else {
594        io::copy(file, &mut stdout).unwrap();
595    }
596}
597
598#[cfg(test)]
599mod tests {
600
601    use crate::forwards_thru_file;
602    use std::io::Cursor;
603
604    #[test]
605    fn test_forwards_thru_file_zero() {
606        let mut reader = Cursor::new("a\n");
607        let i = forwards_thru_file(&mut reader, 0, b'\n').unwrap();
608        assert_eq!(i, 0);
609    }
610
611    #[test]
612    fn test_forwards_thru_file_basic() {
613        //                   01 23 45 67 89
614        let mut reader = Cursor::new("a\nb\nc\nd\ne\n");
615        let i = forwards_thru_file(&mut reader, 2, b'\n').unwrap();
616        assert_eq!(i, 4);
617    }
618
619    #[test]
620    fn test_forwards_thru_file_past_end() {
621        let mut reader = Cursor::new("x\n");
622        let i = forwards_thru_file(&mut reader, 2, b'\n').unwrap();
623        assert_eq!(i, 2);
624    }
625}