1pub 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 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 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 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 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#[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 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 #[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 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 let mut stdin_offset = 0;
292 if cfg!(unix) {
293 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 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
333fn forwards_thru_file(
381 reader: &mut impl Read,
382 num_delimiters: u64,
383 delimiter: u8,
384) -> io::Result<usize> {
385 if num_delimiters == 0 {
387 return Ok(0);
388 }
389 let mut buf = [0; 32 * 1024];
391 let mut total = 0;
392 let mut count = 0;
393 loop {
397 match reader.read(&mut buf) {
398 Ok(0) => return Ok(total),
401 Ok(n) => {
402 for offset in memchr_iter(delimiter, &buf[..n]) {
404 count += 1;
405 if count == num_delimiters {
406 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
418fn 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 let mut counter = 0;
429 let mut first_slice = true;
430 for slice in ReverseChunks::new(file) {
431 let mut iter = memrchr_iter(delimiter, &slice);
433
434 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 i in iter {
449 counter += 1;
450 if counter >= num_delimiters {
451 assert_eq!(counter, num_delimiters);
453 file.seek(SeekFrom::Current((i + 1) as i64)).unwrap();
458 return;
459 }
460 }
461 }
462}
463
464fn bounded_tail(file: &mut File, settings: &Settings) {
470 debug_assert!(!settings.presume_input_pipe);
471 let mut limit = None;
472
473 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 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 #[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 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 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}