uu_pr/
pr.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
7// spell-checker:ignore (ToDO) adFfmprt, kmerge
8
9use 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    /// Line numbering mode
63    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
99/// Line numbering mode
100struct 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
367/// Returns re-written arguments which are passed to the program.
368/// Removes -column and +page option as getopts cannot parse things like -3 etc
369/// # Arguments
370/// * `args` - Command line arguments
371fn 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            // Replicate behavior from GNU manual.
418            if std::env::var("POSIXLY_CORRECT").is_ok()
419                // TODO: This needs to be moved to uucore and handled by icu?
420                && (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    // +page option is less priority than --pages
541    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    // --column has more priority than -column
683
684    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                        // First time byte occurred in the scan
799                        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            }) // Add line number and file_id
855            .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                        // insert empty pages
864                        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            }) // Create set of pages as form feeds could lead to empty pages
882            .flatten() // Flatten to pages from page sets
883            .enumerate() // Assign page number
884            .skip_while(move |(x, _)| {
885                // Skip the not needed pages
886                let current_page = x + 1;
887                current_page < start_page
888            })
889            .take_while(move |(x, _)| {
890                // Take only the required pages
891                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    // Check if files exists
903    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
1150/// Returns a five line header content if displaying header is not disabled by
1151/// using `NO_HEADER_TRAILER_OPTION` option.
1152fn 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
1172/// Returns five empty lines as trailer content if displaying trailer
1173/// is not disabled by using `NO_HEADER_TRAILER_OPTION`option.
1174fn 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
1188/// Returns starting line number for the file to be printed.
1189/// If -N is specified the first line number changes otherwise
1190/// default is 1.
1191fn get_start_line_number(opts: &OutputOptions) -> usize {
1192    opts.number.as_ref().map_or(1, |i| i.first_number)
1193}
1194
1195/// Returns number of lines to read from input for constructing one page of pr output.
1196/// If double space -d is used lines are halved.
1197/// If columns --columns is used the lines are multiplied by the value.
1198fn 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
1208/// Returns number of columns to output
1209fn get_columns(opts: &OutputOptions) -> usize {
1210    opts.column_mode_options.as_ref().map_or(1, |i| i.columns)
1211}