Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 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| {
let mut v = Vec::new();
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
97 changes: 93 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,91 @@ 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.
new_ucmd!()
.args(&[test_file_path, "-D", "Hello!"])
.succeeds()
.stdout_is_templated_fixture(
expected_test_file_path,
&[("{last_modified_time}", "Hello!")],
);

// Long option also works
new_ucmd!()
.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
new_ucmd!()
.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