1use clap::{Arg, ArgAction, ArgMatches, Command};
10use itertools::Itertools;
11use regex::Regex;
12use std::fs::{File, metadata};
13use std::io::{BufRead, BufReader, Lines, Read, Write, stdin, stdout};
14#[cfg(unix)]
15use std::os::unix::fs::FileTypeExt;
16use std::time::SystemTime;
17use thiserror::Error;
18
19use uucore::display::Quotable;
20use uucore::error::UResult;
21use uucore::format_usage;
22use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
23use uucore::translate;
24
25const TAB: char = '\t';
26const LINES_PER_PAGE: usize = 66;
27const LINES_PER_PAGE_FOR_FORM_FEED: usize = 63;
28const HEADER_LINES_PER_PAGE: usize = 5;
29const TRAILER_LINES_PER_PAGE: usize = 5;
30const FILE_STDIN: &str = "-";
31const READ_BUFFER_SIZE: usize = 1024 * 64;
32const DEFAULT_COLUMN_WIDTH: usize = 72;
33const DEFAULT_COLUMN_WIDTH_WITH_S_OPTION: usize = 512;
34const DEFAULT_COLUMN_SEPARATOR: &char = &TAB;
35const FF: u8 = 0x0C_u8;
36
37mod options {
38 pub const HEADER: &str = "header";
39 pub const DATE_FORMAT: &str = "date-format";
40 pub const DOUBLE_SPACE: &str = "double-space";
41 pub const NUMBER_LINES: &str = "number-lines";
42 pub const FIRST_LINE_NUMBER: &str = "first-line-number";
43 pub const PAGES: &str = "pages";
44 pub const OMIT_HEADER: &str = "omit-header";
45 pub const PAGE_LENGTH: &str = "length";
46 pub const NO_FILE_WARNINGS: &str = "no-file-warnings";
47 pub const FORM_FEED: &str = "form-feed";
48 pub const COLUMN_WIDTH: &str = "width";
49 pub const PAGE_WIDTH: &str = "page-width";
50 pub const ACROSS: &str = "across";
51 pub const COLUMN: &str = "column";
52 pub const COLUMN_CHAR_SEPARATOR: &str = "separator";
53 pub const COLUMN_STRING_SEPARATOR: &str = "sep-string";
54 pub const MERGE: &str = "merge";
55 pub const INDENT: &str = "indent";
56 pub const JOIN_LINES: &str = "join-lines";
57 pub const HELP: &str = "help";
58 pub const FILES: &str = "files";
59}
60
61struct OutputOptions {
62 number: Option<NumberingMode>,
64 header: String,
65 double_space: bool,
66 line_separator: String,
67 content_line_separator: String,
68 last_modified_time: String,
69 start_page: usize,
70 end_page: Option<usize>,
71 display_header_and_trailer: bool,
72 content_lines_per_page: usize,
73 page_separator_char: String,
74 column_mode_options: Option<ColumnModeOptions>,
75 merge_files_print: Option<usize>,
76 offset_spaces: String,
77 form_feed_used: bool,
78 join_lines: bool,
79 col_sep_for_printing: String,
80 line_width: Option<usize>,
81}
82
83struct FileLine {
84 file_id: usize,
85 line_number: usize,
86 page_number: usize,
87 group_key: usize,
88 line_content: Result<String, std::io::Error>,
89 form_feeds_after: usize,
90}
91
92struct ColumnModeOptions {
93 width: usize,
94 columns: usize,
95 column_separator: String,
96 across_mode: bool,
97}
98
99struct NumberingMode {
101 width: usize,
102 separator: String,
103 first_number: usize,
104}
105
106impl Default for NumberingMode {
107 fn default() -> Self {
108 Self {
109 width: 5,
110 separator: TAB.to_string(),
111 first_number: 1,
112 }
113 }
114}
115
116impl Default for FileLine {
117 fn default() -> Self {
118 Self {
119 file_id: 0,
120 line_number: 0,
121 page_number: 0,
122 group_key: 0,
123 line_content: Ok(String::new()),
124 form_feeds_after: 0,
125 }
126 }
127}
128
129impl From<std::io::Error> for PrError {
130 fn from(err: std::io::Error) -> Self {
131 Self::EncounteredErrors {
132 msg: err.to_string(),
133 }
134 }
135}
136
137#[derive(Debug, Error)]
138enum PrError {
139 #[error("{}", translate!("pr-error-reading-input", "file" => file.clone()))]
140 Input {
141 #[source]
142 source: std::io::Error,
143 file: String,
144 },
145 #[error("{}", translate!("pr-error-unknown-filetype", "file" => file.clone()))]
146 UnknownFiletype { file: String },
147 #[error("pr: {msg}")]
148 EncounteredErrors { msg: String },
149 #[error("{}", translate!("pr-error-is-directory", "file" => file.clone()))]
150 IsDirectory { file: String },
151 #[cfg(not(windows))]
152 #[error("{}", translate!("pr-error-socket-not-supported", "file" => file.clone()))]
153 IsSocket { file: String },
154 #[error("{}", translate!("pr-error-no-such-file", "file" => file.clone()))]
155 NotExists { file: String },
156}
157
158pub fn uu_app() -> Command {
159 Command::new(uucore::util_name())
160 .version(uucore::crate_version!())
161 .help_template(uucore::localized_help_template(uucore::util_name()))
162 .about(translate!("pr-about"))
163 .after_help(translate!("pr-after-help"))
164 .override_usage(format_usage(&translate!("pr-usage")))
165 .infer_long_args(true)
166 .args_override_self(true)
167 .disable_help_flag(true)
168 .arg(
169 Arg::new(options::PAGES)
170 .long(options::PAGES)
171 .help(translate!("pr-help-pages"))
172 .value_name("FIRST_PAGE[:LAST_PAGE]"),
173 )
174 .arg(
175 Arg::new(options::HEADER)
176 .short('h')
177 .long(options::HEADER)
178 .help(translate!("pr-help-header"))
179 .value_name("STRING"),
180 )
181 .arg(
182 Arg::new(options::DATE_FORMAT)
183 .short('D')
184 .long(options::DATE_FORMAT)
185 .value_name("FORMAT")
186 .help(translate!("pr-help-date-format")),
187 )
188 .arg(
189 Arg::new(options::DOUBLE_SPACE)
190 .short('d')
191 .long(options::DOUBLE_SPACE)
192 .help(translate!("pr-help-double-space"))
193 .action(ArgAction::SetTrue),
194 )
195 .arg(
196 Arg::new(options::NUMBER_LINES)
197 .short('n')
198 .long(options::NUMBER_LINES)
199 .help(translate!("pr-help-number-lines"))
200 .allow_hyphen_values(true)
201 .value_name("[char][width]"),
202 )
203 .arg(
204 Arg::new(options::FIRST_LINE_NUMBER)
205 .short('N')
206 .long(options::FIRST_LINE_NUMBER)
207 .help(translate!("pr-help-first-line-number"))
208 .value_name("NUMBER"),
209 )
210 .arg(
211 Arg::new(options::OMIT_HEADER)
212 .short('t')
213 .long(options::OMIT_HEADER)
214 .help(translate!("pr-help-omit-header"))
215 .action(ArgAction::SetTrue),
216 )
217 .arg(
218 Arg::new(options::PAGE_LENGTH)
219 .short('l')
220 .long(options::PAGE_LENGTH)
221 .help(translate!("pr-help-page-length"))
222 .value_name("PAGE_LENGTH"),
223 )
224 .arg(
225 Arg::new(options::NO_FILE_WARNINGS)
226 .short('r')
227 .long(options::NO_FILE_WARNINGS)
228 .help(translate!("pr-help-no-file-warnings"))
229 .action(ArgAction::SetTrue),
230 )
231 .arg(
232 Arg::new(options::FORM_FEED)
233 .short('F')
234 .short_alias('f')
235 .long(options::FORM_FEED)
236 .help(translate!("pr-help-form-feed"))
237 .action(ArgAction::SetTrue),
238 )
239 .arg(
240 Arg::new(options::COLUMN_WIDTH)
241 .short('w')
242 .long(options::COLUMN_WIDTH)
243 .help(translate!("pr-help-column-width"))
244 .value_name("width"),
245 )
246 .arg(
247 Arg::new(options::PAGE_WIDTH)
248 .short('W')
249 .long(options::PAGE_WIDTH)
250 .help(translate!("pr-help-page-width"))
251 .value_name("width"),
252 )
253 .arg(
254 Arg::new(options::ACROSS)
255 .short('a')
256 .long(options::ACROSS)
257 .help(translate!("pr-help-across"))
258 .action(ArgAction::SetTrue),
259 )
260 .arg(
261 Arg::new(options::COLUMN)
262 .long(options::COLUMN)
263 .help(translate!("pr-help-column"))
264 .value_name("column"),
265 )
266 .arg(
267 Arg::new(options::COLUMN_CHAR_SEPARATOR)
268 .short('s')
269 .long(options::COLUMN_CHAR_SEPARATOR)
270 .help(translate!("pr-help-column-char-separator"))
271 .value_name("char"),
272 )
273 .arg(
274 Arg::new(options::COLUMN_STRING_SEPARATOR)
275 .short('S')
276 .long(options::COLUMN_STRING_SEPARATOR)
277 .help(translate!("pr-help-column-string-separator"))
278 .value_name("string"),
279 )
280 .arg(
281 Arg::new(options::MERGE)
282 .short('m')
283 .long(options::MERGE)
284 .help(translate!("pr-help-merge"))
285 .action(ArgAction::SetTrue),
286 )
287 .arg(
288 Arg::new(options::INDENT)
289 .short('o')
290 .long(options::INDENT)
291 .help(translate!("pr-help-indent"))
292 .value_name("margin"),
293 )
294 .arg(
295 Arg::new(options::JOIN_LINES)
296 .short('J')
297 .help(translate!("pr-help-join-lines"))
298 .action(ArgAction::SetTrue),
299 )
300 .arg(
301 Arg::new(options::HELP)
302 .long(options::HELP)
303 .help(translate!("pr-help-help"))
304 .action(ArgAction::Help),
305 )
306 .arg(
307 Arg::new(options::FILES)
308 .action(ArgAction::Append)
309 .value_hint(clap::ValueHint::FilePath),
310 )
311}
312
313#[uucore::main]
314pub fn uumain(args: impl uucore::Args) -> UResult<()> {
315 let args = args.collect_ignore();
316
317 let opt_args = recreate_arguments(&args);
318
319 let command = uu_app();
320 let matches = uucore::clap_localization::handle_clap_result(command, opt_args)?;
321
322 let mut files = matches
323 .get_many::<String>(options::FILES)
324 .map(|v| v.map(|s| s.as_str()).collect::<Vec<_>>())
325 .unwrap_or_default()
326 .clone();
327 if files.is_empty() {
328 files.insert(0, FILE_STDIN);
329 }
330
331 let file_groups: Vec<_> = if matches.get_flag(options::MERGE) {
332 vec![files]
333 } else {
334 files.into_iter().map(|i| vec![i]).collect()
335 };
336
337 for file_group in file_groups {
338 let result_options = build_options(&matches, &file_group, &args.join(" "));
339 let options = match result_options {
340 Ok(options) => options,
341 Err(err) => {
342 print_error(&matches, &err);
343 return Err(1.into());
344 }
345 };
346
347 let cmd_result = if let Ok(group) = file_group.iter().exactly_one() {
348 pr(group, &options)
349 } else {
350 mpr(&file_group, &options)
351 };
352
353 let status = match cmd_result {
354 Err(error) => {
355 print_error(&matches, &error);
356 1
357 }
358 _ => 0,
359 };
360 if status != 0 {
361 return Err(status.into());
362 }
363 }
364 Ok(())
365}
366
367fn recreate_arguments(args: &[String]) -> Vec<String> {
372 let column_page_option = Regex::new(r"^[-+]\d+.*").unwrap();
373 let num_regex = Regex::new(r"^[^-]\d*$").unwrap();
374 let n_regex = Regex::new(r"^-n\s*$").unwrap();
375 let mut arguments = args.to_owned();
376 let num_option = args.iter().find_position(|x| n_regex.is_match(x.trim()));
377 if let Some((pos, _value)) = num_option {
378 if let Some(num_val_opt) = args.get(pos + 1) {
379 if !num_regex.is_match(num_val_opt) {
380 let could_be_file = arguments.remove(pos + 1);
381 arguments.insert(pos + 1, format!("{}", NumberingMode::default().width));
382 arguments.insert(pos + 2, could_be_file);
383 }
384 }
385 }
386
387 arguments
388 .into_iter()
389 .filter(|i| !column_page_option.is_match(i))
390 .collect()
391}
392
393fn print_error(matches: &ArgMatches, err: &PrError) {
394 if !matches.get_flag(options::NO_FILE_WARNINGS) {
395 eprintln!("{err}");
396 }
397}
398
399fn parse_usize(matches: &ArgMatches, opt: &str) -> Option<Result<usize, PrError>> {
400 let from_parse_error_to_pr_error = |value_to_parse: (String, String)| {
401 let i = value_to_parse.0;
402 let option = value_to_parse.1;
403 i.parse().map_err(|_e| PrError::EncounteredErrors {
404 msg: format!("invalid {option} argument {}", i.quote()),
405 })
406 };
407 matches
408 .get_one::<String>(opt)
409 .map(|i| (i.to_string(), format!("-{opt}")))
410 .map(from_parse_error_to_pr_error)
411}
412
413fn get_date_format(matches: &ArgMatches) -> String {
414 match matches.get_one::<String>(options::DATE_FORMAT) {
415 Some(format) => format,
416 None => {
417 if std::env::var("POSIXLY_CORRECT").is_ok()
419 && (std::env::var("LC_TIME").unwrap_or_default() == "POSIX"
421 || std::env::var("LC_ALL").unwrap_or_default() == "POSIX")
422 {
423 "%b %e %H:%M %Y"
424 } else {
425 format::LONG_ISO
426 }
427 }
428 }
429 .to_string()
430}
431
432#[allow(clippy::cognitive_complexity)]
433fn build_options(
434 matches: &ArgMatches,
435 paths: &[&str],
436 free_args: &str,
437) -> Result<OutputOptions, PrError> {
438 let form_feed_used = matches.get_flag(options::FORM_FEED);
439
440 let is_merge_mode = matches.get_flag(options::MERGE);
441
442 if is_merge_mode && matches.contains_id(options::COLUMN) {
443 return Err(PrError::EncounteredErrors {
444 msg: translate!("pr-error-column-merge-conflict"),
445 });
446 }
447
448 if is_merge_mode && matches.get_flag(options::ACROSS) {
449 return Err(PrError::EncounteredErrors {
450 msg: translate!("pr-error-across-merge-conflict"),
451 });
452 }
453
454 let merge_files_print = if matches.get_flag(options::MERGE) {
455 Some(paths.len())
456 } else {
457 None
458 };
459
460 let header = matches
461 .get_one::<String>(options::HEADER)
462 .map_or(
463 if is_merge_mode || paths[0] == FILE_STDIN {
464 ""
465 } else {
466 paths[0]
467 },
468 |s| s.as_str(),
469 )
470 .to_string();
471
472 let default_first_number = NumberingMode::default().first_number;
473 let first_number =
474 parse_usize(matches, options::FIRST_LINE_NUMBER).unwrap_or(Ok(default_first_number))?;
475
476 let number = matches
477 .get_one::<String>(options::NUMBER_LINES)
478 .map(|i| {
479 let parse_result = i.parse::<usize>();
480
481 let separator = if parse_result.is_err() {
482 i[0..1].to_string()
483 } else {
484 NumberingMode::default().separator
485 };
486
487 let width = match parse_result {
488 Ok(res) => res,
489 Err(_) => i[1..]
490 .parse::<usize>()
491 .unwrap_or(NumberingMode::default().width),
492 };
493
494 NumberingMode {
495 width,
496 separator,
497 first_number,
498 }
499 })
500 .or_else(|| {
501 if matches.contains_id(options::NUMBER_LINES) {
502 Some(NumberingMode::default())
503 } else {
504 None
505 }
506 });
507
508 let double_space = matches.get_flag(options::DOUBLE_SPACE);
509
510 let content_line_separator = if double_space {
511 "\n".repeat(2)
512 } else {
513 "\n".to_string()
514 };
515
516 let line_separator = "\n".to_string();
517
518 let last_modified_time = {
519 let time = if is_merge_mode || paths[0].eq(FILE_STDIN) {
520 Some(SystemTime::now())
521 } else {
522 metadata(paths.first().unwrap())
523 .ok()
524 .and_then(|i| i.modified().ok())
525 };
526 time.and_then(|time| {
527 let mut v = Vec::new();
528 format_system_time(
529 &mut v,
530 time,
531 &get_date_format(matches),
532 FormatSystemTimeFallback::Integer,
533 )
534 .ok()
535 .map(|()| String::from_utf8_lossy(&v).to_string())
536 })
537 .unwrap_or_default()
538 };
539
540 let page_plus_re = Regex::new(r"\s*\+(\d+:*\d*)\s*").unwrap();
542 let res = page_plus_re.captures(free_args).map(|i| {
543 let unparsed_num = i.get(1).unwrap().as_str().trim();
544 let x: Vec<_> = unparsed_num.split(':').collect();
545 x[0].to_string()
546 .parse::<usize>()
547 .map_err(|_e| PrError::EncounteredErrors {
548 msg: format!("invalid {} argument {}", "+", unparsed_num.quote()),
549 })
550 });
551 let start_page_in_plus_option = match res {
552 Some(res) => res?,
553 None => 1,
554 };
555
556 let res = page_plus_re
557 .captures(free_args)
558 .map(|i| i.get(1).unwrap().as_str().trim())
559 .filter(|i| i.contains(':'))
560 .map(|unparsed_num| {
561 let x: Vec<_> = unparsed_num.split(':').collect();
562 x[1].to_string()
563 .parse::<usize>()
564 .map_err(|_e| PrError::EncounteredErrors {
565 msg: format!("invalid {} argument {}", "+", unparsed_num.quote()),
566 })
567 });
568 let end_page_in_plus_option = match res {
569 Some(res) => Some(res?),
570 None => None,
571 };
572
573 let invalid_pages_map = |i: String| {
574 let unparsed_value = matches.get_one::<String>(options::PAGES).unwrap();
575 i.parse::<usize>().map_err(|_e| PrError::EncounteredErrors {
576 msg: format!("invalid --pages argument {}", unparsed_value.quote()),
577 })
578 };
579
580 let res = matches
581 .get_one::<String>(options::PAGES)
582 .map(|i| {
583 let x: Vec<_> = i.split(':').collect();
584 x[0].to_string()
585 })
586 .map(invalid_pages_map);
587 let start_page = match res {
588 Some(res) => res?,
589 None => start_page_in_plus_option,
590 };
591
592 let res = matches
593 .get_one::<String>(options::PAGES)
594 .filter(|i| i.contains(':'))
595 .map(|i| {
596 let x: Vec<_> = i.split(':').collect();
597 x[1].to_string()
598 })
599 .map(invalid_pages_map);
600 let end_page = match res {
601 Some(res) => Some(res?),
602 None => end_page_in_plus_option,
603 };
604
605 if let Some(end_page) = end_page {
606 if start_page > end_page {
607 return Err(PrError::EncounteredErrors {
608 msg: translate!("pr-error-invalid-pages-range", "start" => start_page, "end" => end_page),
609 });
610 }
611 }
612
613 let default_lines_per_page = if form_feed_used {
614 LINES_PER_PAGE_FOR_FORM_FEED
615 } else {
616 LINES_PER_PAGE
617 };
618
619 let page_length =
620 parse_usize(matches, options::PAGE_LENGTH).unwrap_or(Ok(default_lines_per_page))?;
621
622 let page_length_le_ht = page_length < (HEADER_LINES_PER_PAGE + TRAILER_LINES_PER_PAGE);
623
624 let display_header_and_trailer = !page_length_le_ht && !matches.get_flag(options::OMIT_HEADER);
625
626 let content_lines_per_page = if page_length_le_ht {
627 page_length
628 } else {
629 page_length - (HEADER_LINES_PER_PAGE + TRAILER_LINES_PER_PAGE)
630 };
631
632 let page_separator_char = if matches.get_flag(options::FORM_FEED) {
633 let bytes = vec![FF];
634 String::from_utf8(bytes).unwrap()
635 } else {
636 "\n".to_string()
637 };
638
639 let across_mode = matches.get_flag(options::ACROSS);
640
641 let column_separator = match matches.get_one::<String>(options::COLUMN_STRING_SEPARATOR) {
642 Some(x) => Some(x),
643 None => matches.get_one::<String>(options::COLUMN_CHAR_SEPARATOR),
644 }
645 .map_or_else(|| DEFAULT_COLUMN_SEPARATOR.to_string(), ToString::to_string);
646
647 let default_column_width = if matches.contains_id(options::COLUMN_WIDTH)
648 && matches.contains_id(options::COLUMN_CHAR_SEPARATOR)
649 {
650 DEFAULT_COLUMN_WIDTH_WITH_S_OPTION
651 } else {
652 DEFAULT_COLUMN_WIDTH
653 };
654
655 let column_width =
656 parse_usize(matches, options::COLUMN_WIDTH).unwrap_or(Ok(default_column_width))?;
657
658 let page_width = if matches.get_flag(options::JOIN_LINES) {
659 None
660 } else {
661 match parse_usize(matches, options::PAGE_WIDTH) {
662 Some(res) => Some(res?),
663 None => None,
664 }
665 };
666
667 let re_col = Regex::new(r"\s*-(\d+)\s*").unwrap();
668
669 let res = re_col.captures(free_args).map(|i| {
670 let unparsed_num = i.get(1).unwrap().as_str().trim();
671 unparsed_num
672 .parse::<usize>()
673 .map_err(|_e| PrError::EncounteredErrors {
674 msg: format!("invalid {} argument {}", "-", unparsed_num.quote()),
675 })
676 });
677 let start_column_option = match res {
678 Some(res) => Some(res?),
679 None => None,
680 };
681
682 let column_option_value = match parse_usize(matches, options::COLUMN) {
685 Some(res) => Some(res?),
686 None => start_column_option,
687 };
688
689 let column_mode_options = column_option_value.map(|columns| ColumnModeOptions {
690 columns,
691 width: column_width,
692 column_separator,
693 across_mode,
694 });
695
696 let offset_spaces = " ".repeat(parse_usize(matches, options::INDENT).unwrap_or(Ok(0))?);
697 let join_lines = matches.get_flag(options::JOIN_LINES);
698
699 let col_sep_for_printing = column_mode_options.as_ref().map_or_else(
700 || {
701 merge_files_print
702 .map(|_k| DEFAULT_COLUMN_SEPARATOR.to_string())
703 .unwrap_or_default()
704 },
705 |i| i.column_separator.clone(),
706 );
707
708 let columns_to_print =
709 merge_files_print.unwrap_or_else(|| column_mode_options.as_ref().map_or(1, |i| i.columns));
710
711 let line_width = if join_lines {
712 None
713 } else if columns_to_print > 1 {
714 Some(
715 column_mode_options
716 .as_ref()
717 .map_or(DEFAULT_COLUMN_WIDTH, |i| i.width),
718 )
719 } else {
720 page_width
721 };
722
723 Ok(OutputOptions {
724 number,
725 header,
726 double_space,
727 line_separator,
728 content_line_separator,
729 last_modified_time,
730 start_page,
731 end_page,
732 display_header_and_trailer,
733 content_lines_per_page,
734 page_separator_char,
735 column_mode_options,
736 merge_files_print,
737 offset_spaces,
738 form_feed_used,
739 join_lines,
740 col_sep_for_printing,
741 line_width,
742 })
743}
744
745fn open(path: &str) -> Result<Box<dyn Read>, PrError> {
746 if path == FILE_STDIN {
747 let stdin = stdin();
748 return Ok(Box::new(stdin) as Box<dyn Read>);
749 }
750
751 metadata(path).map_or_else(
752 |_| {
753 Err(PrError::NotExists {
754 file: path.to_string(),
755 })
756 },
757 |i| {
758 let path_string = path.to_string();
759 match i.file_type() {
760 #[cfg(unix)]
761 ft if ft.is_block_device() => Err(PrError::UnknownFiletype { file: path_string }),
762 #[cfg(unix)]
763 ft if ft.is_char_device() => Err(PrError::UnknownFiletype { file: path_string }),
764 #[cfg(unix)]
765 ft if ft.is_fifo() => Err(PrError::UnknownFiletype { file: path_string }),
766 #[cfg(unix)]
767 ft if ft.is_socket() => Err(PrError::IsSocket { file: path_string }),
768 ft if ft.is_dir() => Err(PrError::IsDirectory { file: path_string }),
769 ft if ft.is_file() || ft.is_symlink() => {
770 Ok(Box::new(File::open(path).map_err(|e| PrError::Input {
771 source: e,
772 file: path.to_string(),
773 })?) as Box<dyn Read>)
774 }
775 _ => Err(PrError::UnknownFiletype { file: path_string }),
776 }
777 },
778 )
779}
780
781fn split_lines_if_form_feed(file_content: Result<String, std::io::Error>) -> Vec<FileLine> {
782 file_content.map_or_else(
783 |e| {
784 vec![FileLine {
785 line_content: Err(e),
786 ..FileLine::default()
787 }]
788 },
789 |content| {
790 let mut lines = Vec::new();
791 let mut f_occurred = 0;
792 let mut chunk = Vec::new();
793 for byte in content.as_bytes() {
794 if byte == &FF {
795 f_occurred += 1;
796 } else {
797 if f_occurred != 0 {
798 lines.push(FileLine {
800 line_content: Ok(String::from_utf8(chunk.clone()).unwrap()),
801 form_feeds_after: f_occurred,
802 ..FileLine::default()
803 });
804 chunk.clear();
805 }
806 chunk.push(*byte);
807 f_occurred = 0;
808 }
809 }
810
811 lines.push(FileLine {
812 line_content: Ok(String::from_utf8(chunk).unwrap()),
813 form_feeds_after: f_occurred,
814 ..FileLine::default()
815 });
816
817 lines
818 },
819 )
820}
821
822fn pr(path: &str, options: &OutputOptions) -> Result<i32, PrError> {
823 let lines = BufReader::with_capacity(READ_BUFFER_SIZE, open(path)?).lines();
824
825 let pages = read_stream_and_create_pages(options, lines, 0);
826
827 for page_with_page_number in pages {
828 let page_number = page_with_page_number.0 + 1;
829 let page = page_with_page_number.1;
830 print_page(&page, options, page_number)?;
831 }
832
833 Ok(0)
834}
835
836fn read_stream_and_create_pages(
837 options: &OutputOptions,
838 lines: Lines<BufReader<Box<dyn Read>>>,
839 file_id: usize,
840) -> Box<dyn Iterator<Item = (usize, Vec<FileLine>)>> {
841 let start_page = options.start_page;
842 let start_line_number = get_start_line_number(options);
843 let last_page = options.end_page;
844 let lines_needed_per_page = lines_to_read_for_page(options);
845
846 Box::new(
847 lines
848 .flat_map(split_lines_if_form_feed)
849 .enumerate()
850 .map(move |(i, line)| FileLine {
851 line_number: i + start_line_number,
852 file_id,
853 ..line
854 }) .batching(move |it| {
856 let mut first_page = Vec::new();
857 let mut page_with_lines = Vec::new();
858 for line in it {
859 let form_feeds_after = line.form_feeds_after;
860 first_page.push(line);
861
862 if form_feeds_after > 1 {
863 page_with_lines.push(first_page);
865 for _i in 1..form_feeds_after {
866 page_with_lines.push(vec![]);
867 }
868 return Some(page_with_lines);
869 }
870
871 if first_page.len() == lines_needed_per_page || form_feeds_after == 1 {
872 break;
873 }
874 }
875
876 if first_page.is_empty() {
877 return None;
878 }
879 page_with_lines.push(first_page);
880 Some(page_with_lines)
881 }) .flatten() .enumerate() .skip_while(move |(x, _)| {
885 let current_page = x + 1;
887 current_page < start_page
888 })
889 .take_while(move |(x, _)| {
890 let current_page = x + 1;
892
893 current_page >= start_page
894 && last_page.is_none_or(|last_page| current_page <= last_page)
895 }),
896 )
897}
898
899fn mpr(paths: &[&str], options: &OutputOptions) -> Result<i32, PrError> {
900 let n_files = paths.len();
901
902 for path in paths {
904 open(path)?;
905 }
906
907 let file_line_groups = paths
908 .iter()
909 .enumerate()
910 .map(|(i, path)| {
911 let lines = BufReader::with_capacity(READ_BUFFER_SIZE, open(path).unwrap()).lines();
912
913 read_stream_and_create_pages(options, lines, i).flat_map(move |(x, line)| {
914 let file_line = line;
915 let page_number = x + 1;
916 file_line
917 .into_iter()
918 .map(|fl| FileLine {
919 page_number,
920 group_key: page_number * n_files + fl.file_id,
921 ..fl
922 })
923 .collect::<Vec<_>>()
924 })
925 })
926 .kmerge_by(|a, b| {
927 if a.group_key == b.group_key {
928 a.line_number < b.line_number
929 } else {
930 a.group_key < b.group_key
931 }
932 })
933 .chunk_by(|file_line| file_line.group_key);
934
935 let start_page = options.start_page;
936 let mut lines = Vec::new();
937 let mut page_counter = start_page;
938
939 for (_key, file_line_group) in &file_line_groups {
940 for file_line in file_line_group {
941 if let Err(e) = file_line.line_content {
942 return Err(e.into());
943 }
944 let new_page_number = file_line.page_number;
945 if page_counter != new_page_number {
946 print_page(&lines, options, page_counter)?;
947 lines = Vec::new();
948 page_counter = new_page_number;
949 }
950 lines.push(file_line);
951 }
952 }
953
954 print_page(&lines, options, page_counter)?;
955
956 Ok(0)
957}
958
959fn print_page(
960 lines: &[FileLine],
961 options: &OutputOptions,
962 page: usize,
963) -> Result<usize, std::io::Error> {
964 let line_separator = options.line_separator.as_bytes();
965 let page_separator = options.page_separator_char.as_bytes();
966
967 let header = header_content(options, page);
968 let trailer_content = trailer_content(options);
969
970 let out = stdout();
971 let mut out = out.lock();
972
973 for x in header {
974 out.write_all(x.as_bytes())?;
975 out.write_all(line_separator)?;
976 }
977
978 let lines_written = write_columns(lines, options, &mut out)?;
979
980 for (index, x) in trailer_content.iter().enumerate() {
981 out.write_all(x.as_bytes())?;
982 if index + 1 != trailer_content.len() {
983 out.write_all(line_separator)?;
984 }
985 }
986 out.write_all(page_separator)?;
987 out.flush()?;
988 Ok(lines_written)
989}
990
991#[allow(clippy::cognitive_complexity)]
992fn write_columns(
993 lines: &[FileLine],
994 options: &OutputOptions,
995 out: &mut impl Write,
996) -> Result<usize, std::io::Error> {
997 let line_separator = options.content_line_separator.as_bytes();
998
999 let content_lines_per_page = if options.double_space {
1000 options.content_lines_per_page / 2
1001 } else {
1002 options.content_lines_per_page
1003 };
1004
1005 let columns = options
1006 .merge_files_print
1007 .unwrap_or_else(|| get_columns(options));
1008 let line_width = options.line_width;
1009 let mut lines_printed = 0;
1010 let feed_line_present = options.form_feed_used;
1011 let mut not_found_break = false;
1012
1013 let across_mode = options
1014 .column_mode_options
1015 .as_ref()
1016 .is_some_and(|i| i.across_mode);
1017
1018 let mut filled_lines = Vec::new();
1019 if options.merge_files_print.is_some() {
1020 let mut offset = 0;
1021 for col in 0..columns {
1022 let mut inserted = 0;
1023 for line in &lines[offset..] {
1024 if line.file_id != col {
1025 break;
1026 }
1027 filled_lines.push(Some(line));
1028 inserted += 1;
1029 }
1030 offset += inserted;
1031
1032 for _i in inserted..content_lines_per_page {
1033 filled_lines.push(None);
1034 }
1035 }
1036 }
1037
1038 let table: Vec<Vec<_>> = (0..content_lines_per_page)
1039 .map(move |a| {
1040 (0..columns)
1041 .map(|i| {
1042 if across_mode {
1043 lines.get(a * columns + i)
1044 } else if options.merge_files_print.is_some() {
1045 *filled_lines
1046 .get(content_lines_per_page * i + a)
1047 .unwrap_or(&None)
1048 } else {
1049 lines.get(content_lines_per_page * i + a)
1050 }
1051 })
1052 .collect()
1053 })
1054 .collect();
1055
1056 let blank_line = FileLine::default();
1057 for row in table {
1058 let indexes = row.len();
1059 for (i, cell) in row.iter().enumerate() {
1060 if cell.is_none() && options.merge_files_print.is_some() {
1061 out.write_all(
1062 get_line_for_printing(options, &blank_line, columns, i, line_width, indexes)
1063 .as_bytes(),
1064 )?;
1065 } else if cell.is_none() {
1066 not_found_break = true;
1067 break;
1068 } else if cell.is_some() {
1069 let file_line = cell.unwrap();
1070
1071 out.write_all(
1072 get_line_for_printing(options, file_line, columns, i, line_width, indexes)
1073 .as_bytes(),
1074 )?;
1075 lines_printed += 1;
1076 }
1077 }
1078 if not_found_break && feed_line_present {
1079 break;
1080 }
1081 out.write_all(line_separator)?;
1082 }
1083
1084 Ok(lines_printed)
1085}
1086
1087fn get_line_for_printing(
1088 options: &OutputOptions,
1089 file_line: &FileLine,
1090 columns: usize,
1091 index: usize,
1092 line_width: Option<usize>,
1093 indexes: usize,
1094) -> String {
1095 let blank_line = String::new();
1096 let formatted_line_number = get_formatted_line_number(options, file_line.line_number, index);
1097
1098 let mut complete_line = format!(
1099 "{formatted_line_number}{}",
1100 file_line.line_content.as_ref().unwrap()
1101 );
1102
1103 let offset_spaces = &options.offset_spaces;
1104
1105 let tab_count = complete_line.chars().filter(|i| i == &TAB).count();
1106
1107 let display_length = complete_line.len() + (tab_count * 7);
1108
1109 let sep = if (index + 1) != indexes && !options.join_lines {
1110 &options.col_sep_for_printing
1111 } else {
1112 &blank_line
1113 };
1114
1115 format!(
1116 "{offset_spaces}{}{sep}",
1117 line_width
1118 .map(|i| {
1119 let min_width = (i - (columns - 1)) / columns;
1120 if display_length < min_width {
1121 for _i in 0..(min_width - display_length) {
1122 complete_line.push(' ');
1123 }
1124 }
1125
1126 complete_line.chars().take(min_width).collect()
1127 })
1128 .unwrap_or(complete_line),
1129 )
1130}
1131
1132fn get_formatted_line_number(opts: &OutputOptions, line_number: usize, index: usize) -> String {
1133 let should_show_line_number =
1134 opts.number.is_some() && (opts.merge_files_print.is_none() || index == 0);
1135 if should_show_line_number && line_number != 0 {
1136 let line_str = line_number.to_string();
1137 let num_opt = opts.number.as_ref().unwrap();
1138 let width = num_opt.width;
1139 let separator = &num_opt.separator;
1140 if line_str.len() >= width {
1141 format!("{:>width$}{separator}", &line_str[line_str.len() - width..],)
1142 } else {
1143 format!("{line_str:>width$}{separator}")
1144 }
1145 } else {
1146 String::new()
1147 }
1148}
1149
1150fn header_content(options: &OutputOptions, page: usize) -> Vec<String> {
1153 if options.display_header_and_trailer {
1154 let first_line = format!(
1155 "{} {} {} {page}",
1156 options.last_modified_time,
1157 options.header,
1158 translate!("pr-page")
1159 );
1160 vec![
1161 String::new(),
1162 String::new(),
1163 first_line,
1164 String::new(),
1165 String::new(),
1166 ]
1167 } else {
1168 Vec::new()
1169 }
1170}
1171
1172fn trailer_content(options: &OutputOptions) -> Vec<String> {
1175 if options.display_header_and_trailer && !options.form_feed_used {
1176 vec![
1177 String::new(),
1178 String::new(),
1179 String::new(),
1180 String::new(),
1181 String::new(),
1182 ]
1183 } else {
1184 Vec::new()
1185 }
1186}
1187
1188fn get_start_line_number(opts: &OutputOptions) -> usize {
1192 opts.number.as_ref().map_or(1, |i| i.first_number)
1193}
1194
1195fn lines_to_read_for_page(opts: &OutputOptions) -> usize {
1199 let content_lines_per_page = opts.content_lines_per_page;
1200 let columns = get_columns(opts);
1201 if opts.double_space {
1202 (content_lines_per_page / 2) * columns
1203 } else {
1204 content_lines_per_page * columns
1205 }
1206}
1207
1208fn get_columns(opts: &OutputOptions) -> usize {
1210 opts.column_mode_options.as_ref().map_or(1, |i| i.columns)
1211}