Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/uu/pr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ path = "src/pr.rs"

[dependencies]
clap = { workspace = true }
uucore = { workspace = true, features = ["entries"] }
uucore = { workspace = true, features = ["entries", "time"] }
itertools = { workspace = true }
regex = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
fluent = { workspace = true }

Expand Down
2 changes: 2 additions & 0 deletions src/uu/pr/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pr-help-pages = Begin and stop printing with page FIRST_PAGE[:LAST_PAGE]
pr-help-header =
Use the string header to replace the file name
in the header line.
pr-help-date-format =
Use 'date'-style FORMAT for the header date.
pr-help-double-space =
Produce output that is double spaced. An extra <newline>
character is output following every <newline> found in the input.
Expand Down
2 changes: 2 additions & 0 deletions src/uu/pr/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pr-help-pages = Commencer et arrêter l'impression à la page PREMIÈRE_PAGE[:DE
pr-help-header =
Utiliser la chaîne d'en-tête pour remplacer le nom de fichier
dans la ligne d'en-tête.
pr-help-date-format =
Utiliser le FORMAT de style 'date' pour la date dans la ligne d'en-tête.
pr-help-double-space =
Produire une sortie avec double espacement. Un caractère <saut de ligne>
supplémentaire est affiché après chaque <saut de ligne> trouvé dans l'entrée.
Expand Down
69 changes: 49 additions & 20 deletions src/uu/pr/src/pr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@

// spell-checker:ignore (ToDO) adFfmprt, kmerge

use chrono::{DateTime, Local};
use clap::{Arg, ArgAction, ArgMatches, Command};
use itertools::Itertools;
use regex::Regex;
use std::fs::{File, metadata};
use std::io::{BufRead, BufReader, Lines, Read, Write, stdin, stdout};
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
use std::time::SystemTime;
use thiserror::Error;

use uucore::display::Quotable;
use uucore::error::UResult;
use uucore::format_usage;
use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
use uucore::translate;

const TAB: char = '\t';
Expand All @@ -32,10 +33,10 @@ const DEFAULT_COLUMN_WIDTH: usize = 72;
const DEFAULT_COLUMN_WIDTH_WITH_S_OPTION: usize = 512;
const DEFAULT_COLUMN_SEPARATOR: &char = &TAB;
const FF: u8 = 0x0C_u8;
const DATE_TIME_FORMAT: &str = "%b %d %H:%M %Y";

mod options {
pub const HEADER: &str = "header";
pub const DATE_FORMAT: &str = "date-format";
pub const DOUBLE_SPACE: &str = "double-space";
pub const NUMBER_LINES: &str = "number-lines";
pub const FIRST_LINE_NUMBER: &str = "first-line-number";
Expand Down Expand Up @@ -176,6 +177,13 @@ pub fn uu_app() -> Command {
.help(translate!("pr-help-header"))
.value_name("STRING"),
)
.arg(
Arg::new(options::DATE_FORMAT)
.short('D')
.long(options::DATE_FORMAT)
.value_name("FORMAT")
.help(translate!("pr-help-date-format")),
)
.arg(
Arg::new(options::DOUBLE_SPACE)
.short('d')
Expand Down Expand Up @@ -401,6 +409,25 @@ fn parse_usize(matches: &ArgMatches, opt: &str) -> Option<Result<usize, PrError>
.map(from_parse_error_to_pr_error)
}

fn get_date_format(matches: &ArgMatches) -> String {
match matches.get_one::<String>(options::DATE_FORMAT) {
Some(format) => format,
None => {
// Replicate behavior from GNU manual.
if std::env::var("POSIXLY_CORRECT").is_ok()
// TODO: This needs to be moved to uucore and handled by icu?
&& (std::env::var("LC_TIME").unwrap_or_default() == "POSIX"
|| std::env::var("LC_ALL").unwrap_or_default() == "POSIX")
{
"%b %e %H:%M %Y"
} else {
format::LONG_ISO
}
}
}
.to_string()
}

#[allow(clippy::cognitive_complexity)]
fn build_options(
matches: &ArgMatches,
Expand Down Expand Up @@ -487,11 +514,26 @@ fn build_options(

let line_separator = "\n".to_string();

let last_modified_time = if is_merge_mode || paths[0].eq(FILE_STDIN) {
let date_time = Local::now();
date_time.format(DATE_TIME_FORMAT).to_string()
} else {
file_last_modified_time(paths.first().unwrap())
let last_modified_time = {
let mut v = Vec::new();
let time = if is_merge_mode || paths[0].eq(FILE_STDIN) {
Some(SystemTime::now())
} else {
metadata(paths.first().unwrap())
.ok()
.and_then(|i| i.modified().ok())
};
time.and_then(|time| {
format_system_time(
&mut v,
time,
&get_date_format(matches),
FormatSystemTimeFallback::Integer,
)
.ok()
})
.map(|()| String::from_utf8_lossy(&v).to_string())
.unwrap_or_default()
};

// +page option is less priority than --pages
Expand Down Expand Up @@ -1126,19 +1168,6 @@ fn header_content(options: &OutputOptions, page: usize) -> Vec<String> {
}
}

fn file_last_modified_time(path: &str) -> String {
metadata(path)
.map(|i| {
i.modified()
.map(|x| {
let date_time: DateTime<Local> = x.into();
date_time.format(DATE_TIME_FORMAT).to_string()
})
.unwrap_or_default()
})
.unwrap_or_default()
}

/// Returns five empty lines as trailer content if displaying trailer
/// is not disabled by using `NO_HEADER_TRAILER_OPTION`option.
fn trailer_content(options: &OutputOptions) -> Vec<String> {
Expand Down
100 changes: 96 additions & 4 deletions tests/by-util/test_pr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,33 @@ use std::fs::metadata;
use uutests::new_ucmd;
use uutests::util::UCommand;

const DATE_TIME_FORMAT: &str = "%b %d %H:%M %Y";
const DATE_TIME_FORMAT_DEFAULT: &str = "%Y-%m-%d %H:%M";

fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
fn file_last_modified_time_format(ucmd: &UCommand, path: &str, format: &str) -> String {
let tmp_dir_path = ucmd.get_full_fixture_path(path);
let file_metadata = metadata(tmp_dir_path);
file_metadata
.map(|i| {
i.modified()
.map(|x| {
let date_time: DateTime<Utc> = x.into();
date_time.format(DATE_TIME_FORMAT).to_string()
date_time.format(format).to_string()
})
.unwrap_or_default()
})
.unwrap_or_default()
}

fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
file_last_modified_time_format(ucmd, path, DATE_TIME_FORMAT_DEFAULT)
}

fn all_minutes(from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<String> {
let to = to + Duration::try_minutes(1).unwrap();
let mut vec = vec![];
let mut current = from;
while current < to {
vec.push(current.format(DATE_TIME_FORMAT).to_string());
vec.push(current.format(DATE_TIME_FORMAT_DEFAULT).to_string());
current += Duration::try_minutes(1).unwrap();
}
vec
Expand Down Expand Up @@ -398,6 +402,94 @@ fn test_with_offset_space_option() {
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}

#[test]
fn test_with_date_format() {
let test_file_path = "test_one_page.log";
let expected_test_file_path = "test_one_page.log.expected";
let mut scenario = new_ucmd!();
let value = file_last_modified_time_format(&scenario, test_file_path, "%Y__%s");
scenario
.args(&[test_file_path, "-D", "%Y__%s"])
.succeeds()
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);

// "Format" doesn't need to contain any replaceable token.
let mut scenario = new_ucmd!();
scenario
.args(&[test_file_path, "-D", "Hello!"])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
&[("{last_modified_time}", "Hello!")],
);

// Long option also works
let mut scenario = new_ucmd!();
scenario
.args(&[test_file_path, "--date-format=Hello!"])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
&[("{last_modified_time}", "Hello!")],
);

// Option takes precedence over environment variables
let mut scenario = new_ucmd!();
scenario
.env("POSIXLY_CORRECT", "1")
.env("LC_TIME", "POSIX")
.args(&[test_file_path, "-D", "Hello!"])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
&[("{last_modified_time}", "Hello!")],
);
}

#[test]
fn test_with_date_format_env() {
const POSIXLY_FORMAT: &str = "%b %e %H:%M %Y";

// POSIXLY_CORRECT + LC_ALL/TIME=POSIX uses "%b %e %H:%M %Y" date format
let test_file_path = "test_one_page.log";
let expected_test_file_path = "test_one_page.log.expected";
let mut scenario = new_ucmd!();
let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT);
scenario
.env("POSIXLY_CORRECT", "1")
.env("LC_ALL", "POSIX")
.args(&[test_file_path])
.succeeds()
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);

let mut scenario = new_ucmd!();
let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT);
scenario
.env("POSIXLY_CORRECT", "1")
.env("LC_TIME", "POSIX")
.args(&[test_file_path])
.succeeds()
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);

// But not if POSIXLY_CORRECT/LC_ALL is something else.
let mut scenario = new_ucmd!();
let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT);
scenario
.env("LC_TIME", "POSIX")
.args(&[test_file_path])
.succeeds()
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);

let mut scenario = new_ucmd!();
let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT);
scenario
.env("POSIXLY_CORRECT", "1")
.env("LC_TIME", "C")
.args(&[test_file_path])
.succeeds()
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
}

#[test]
fn test_with_pr_core_utils_tests() {
let test_cases = vec![
Expand Down
Loading