Skip to content

Commit 7003b1d

Browse files
authored
Merge pull request #8418 from drinkcat/pr-time
pr: Add support for -D/--date-format parameter
2 parents b5f453a + 82cb88a commit 7003b1d

File tree

6 files changed

+147
-27
lines changed

6 files changed

+147
-27
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/pr/Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@ path = "src/pr.rs"
1919

2020
[dependencies]
2121
clap = { workspace = true }
22-
uucore = { workspace = true, features = ["entries"] }
22+
uucore = { workspace = true, features = ["entries", "time"] }
2323
itertools = { workspace = true }
2424
regex = { workspace = true }
25-
chrono = { workspace = true }
2625
thiserror = { workspace = true }
2726
fluent = { workspace = true }
2827

src/uu/pr/locales/en-US.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pr-help-pages = Begin and stop printing with page FIRST_PAGE[:LAST_PAGE]
1313
pr-help-header =
1414
Use the string header to replace the file name
1515
in the header line.
16+
pr-help-date-format =
17+
Use 'date'-style FORMAT for the header date.
1618
pr-help-double-space =
1719
Produce output that is double spaced. An extra <newline>
1820
character is output following every <newline> found in the input.

src/uu/pr/locales/fr-FR.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pr-help-pages = Commencer et arrêter l'impression à la page PREMIÈRE_PAGE[:DE
1313
pr-help-header =
1414
Utiliser la chaîne d'en-tête pour remplacer le nom de fichier
1515
dans la ligne d'en-tête.
16+
pr-help-date-format =
17+
Utiliser le FORMAT de style 'date' pour la date dans la ligne d'en-tête.
1618
pr-help-double-space =
1719
Produire une sortie avec double espacement. Un caractère <saut de ligne>
1820
supplémentaire est affiché après chaque <saut de ligne> trouvé dans l'entrée.

src/uu/pr/src/pr.rs

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66

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

9-
use chrono::{DateTime, Local};
109
use clap::{Arg, ArgAction, ArgMatches, Command};
1110
use itertools::Itertools;
1211
use regex::Regex;
1312
use std::fs::{File, metadata};
1413
use std::io::{BufRead, BufReader, Lines, Read, Write, stdin, stdout};
1514
#[cfg(unix)]
1615
use std::os::unix::fs::FileTypeExt;
16+
use std::time::SystemTime;
1717
use thiserror::Error;
1818

1919
use uucore::display::Quotable;
2020
use uucore::error::UResult;
2121
use uucore::format_usage;
22+
use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
2223
use uucore::translate;
2324

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

3737
mod options {
3838
pub const HEADER: &str = "header";
39+
pub const DATE_FORMAT: &str = "date-format";
3940
pub const DOUBLE_SPACE: &str = "double-space";
4041
pub const NUMBER_LINES: &str = "number-lines";
4142
pub const FIRST_LINE_NUMBER: &str = "first-line-number";
@@ -176,6 +177,13 @@ pub fn uu_app() -> Command {
176177
.help(translate!("pr-help-header"))
177178
.value_name("STRING"),
178179
)
180+
.arg(
181+
Arg::new(options::DATE_FORMAT)
182+
.short('D')
183+
.long(options::DATE_FORMAT)
184+
.value_name("FORMAT")
185+
.help(translate!("pr-help-date-format")),
186+
)
179187
.arg(
180188
Arg::new(options::DOUBLE_SPACE)
181189
.short('d')
@@ -401,6 +409,25 @@ fn parse_usize(matches: &ArgMatches, opt: &str) -> Option<Result<usize, PrError>
401409
.map(from_parse_error_to_pr_error)
402410
}
403411

412+
fn get_date_format(matches: &ArgMatches) -> String {
413+
match matches.get_one::<String>(options::DATE_FORMAT) {
414+
Some(format) => format,
415+
None => {
416+
// Replicate behavior from GNU manual.
417+
if std::env::var("POSIXLY_CORRECT").is_ok()
418+
// TODO: This needs to be moved to uucore and handled by icu?
419+
&& (std::env::var("LC_TIME").unwrap_or_default() == "POSIX"
420+
|| std::env::var("LC_ALL").unwrap_or_default() == "POSIX")
421+
{
422+
"%b %e %H:%M %Y"
423+
} else {
424+
format::LONG_ISO
425+
}
426+
}
427+
}
428+
.to_string()
429+
}
430+
404431
#[allow(clippy::cognitive_complexity)]
405432
fn build_options(
406433
matches: &ArgMatches,
@@ -487,11 +514,26 @@ fn build_options(
487514

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

490-
let last_modified_time = if is_merge_mode || paths[0].eq(FILE_STDIN) {
491-
let date_time = Local::now();
492-
date_time.format(DATE_TIME_FORMAT).to_string()
493-
} else {
494-
file_last_modified_time(paths.first().unwrap())
517+
let last_modified_time = {
518+
let time = if is_merge_mode || paths[0].eq(FILE_STDIN) {
519+
Some(SystemTime::now())
520+
} else {
521+
metadata(paths.first().unwrap())
522+
.ok()
523+
.and_then(|i| i.modified().ok())
524+
};
525+
time.and_then(|time| {
526+
let mut v = Vec::new();
527+
format_system_time(
528+
&mut v,
529+
time,
530+
&get_date_format(matches),
531+
FormatSystemTimeFallback::Integer,
532+
)
533+
.ok()
534+
.map(|()| String::from_utf8_lossy(&v).to_string())
535+
})
536+
.unwrap_or_default()
495537
};
496538

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

1129-
fn file_last_modified_time(path: &str) -> String {
1130-
metadata(path)
1131-
.map(|i| {
1132-
i.modified()
1133-
.map(|x| {
1134-
let date_time: DateTime<Local> = x.into();
1135-
date_time.format(DATE_TIME_FORMAT).to_string()
1136-
})
1137-
.unwrap_or_default()
1138-
})
1139-
.unwrap_or_default()
1140-
}
1141-
11421171
/// Returns five empty lines as trailer content if displaying trailer
11431172
/// is not disabled by using `NO_HEADER_TRAILER_OPTION`option.
11441173
fn trailer_content(options: &OutputOptions) -> Vec<String> {

tests/by-util/test_pr.rs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,33 @@ use std::fs::metadata;
99
use uutests::new_ucmd;
1010
use uutests::util::UCommand;
1111

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

14-
fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
14+
fn file_last_modified_time_format(ucmd: &UCommand, path: &str, format: &str) -> String {
1515
let tmp_dir_path = ucmd.get_full_fixture_path(path);
1616
let file_metadata = metadata(tmp_dir_path);
1717
file_metadata
1818
.map(|i| {
1919
i.modified()
2020
.map(|x| {
2121
let date_time: DateTime<Utc> = x.into();
22-
date_time.format(DATE_TIME_FORMAT).to_string()
22+
date_time.format(format).to_string()
2323
})
2424
.unwrap_or_default()
2525
})
2626
.unwrap_or_default()
2727
}
2828

29+
fn file_last_modified_time(ucmd: &UCommand, path: &str) -> String {
30+
file_last_modified_time_format(ucmd, path, DATE_TIME_FORMAT_DEFAULT)
31+
}
32+
2933
fn all_minutes(from: DateTime<Utc>, to: DateTime<Utc>) -> Vec<String> {
3034
let to = to + Duration::try_minutes(1).unwrap();
3135
let mut vec = vec![];
3236
let mut current = from;
3337
while current < to {
34-
vec.push(current.format(DATE_TIME_FORMAT).to_string());
38+
vec.push(current.format(DATE_TIME_FORMAT_DEFAULT).to_string());
3539
current += Duration::try_minutes(1).unwrap();
3640
}
3741
vec
@@ -398,6 +402,91 @@ fn test_with_offset_space_option() {
398402
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
399403
}
400404

405+
#[test]
406+
fn test_with_date_format() {
407+
let test_file_path = "test_one_page.log";
408+
let expected_test_file_path = "test_one_page.log.expected";
409+
let mut scenario = new_ucmd!();
410+
let value = file_last_modified_time_format(&scenario, test_file_path, "%Y__%s");
411+
scenario
412+
.args(&[test_file_path, "-D", "%Y__%s"])
413+
.succeeds()
414+
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
415+
416+
// "Format" doesn't need to contain any replaceable token.
417+
new_ucmd!()
418+
.args(&[test_file_path, "-D", "Hello!"])
419+
.succeeds()
420+
.stdout_is_templated_fixture(
421+
expected_test_file_path,
422+
&[("{last_modified_time}", "Hello!")],
423+
);
424+
425+
// Long option also works
426+
new_ucmd!()
427+
.args(&[test_file_path, "--date-format=Hello!"])
428+
.succeeds()
429+
.stdout_is_templated_fixture(
430+
expected_test_file_path,
431+
&[("{last_modified_time}", "Hello!")],
432+
);
433+
434+
// Option takes precedence over environment variables
435+
new_ucmd!()
436+
.env("POSIXLY_CORRECT", "1")
437+
.env("LC_TIME", "POSIX")
438+
.args(&[test_file_path, "-D", "Hello!"])
439+
.succeeds()
440+
.stdout_is_templated_fixture(
441+
expected_test_file_path,
442+
&[("{last_modified_time}", "Hello!")],
443+
);
444+
}
445+
446+
#[test]
447+
fn test_with_date_format_env() {
448+
const POSIXLY_FORMAT: &str = "%b %e %H:%M %Y";
449+
450+
// POSIXLY_CORRECT + LC_ALL/TIME=POSIX uses "%b %e %H:%M %Y" date format
451+
let test_file_path = "test_one_page.log";
452+
let expected_test_file_path = "test_one_page.log.expected";
453+
let mut scenario = new_ucmd!();
454+
let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT);
455+
scenario
456+
.env("POSIXLY_CORRECT", "1")
457+
.env("LC_ALL", "POSIX")
458+
.args(&[test_file_path])
459+
.succeeds()
460+
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
461+
462+
let mut scenario = new_ucmd!();
463+
let value = file_last_modified_time_format(&scenario, test_file_path, POSIXLY_FORMAT);
464+
scenario
465+
.env("POSIXLY_CORRECT", "1")
466+
.env("LC_TIME", "POSIX")
467+
.args(&[test_file_path])
468+
.succeeds()
469+
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
470+
471+
// But not if POSIXLY_CORRECT/LC_ALL is something else.
472+
let mut scenario = new_ucmd!();
473+
let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT);
474+
scenario
475+
.env("LC_TIME", "POSIX")
476+
.args(&[test_file_path])
477+
.succeeds()
478+
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
479+
480+
let mut scenario = new_ucmd!();
481+
let value = file_last_modified_time_format(&scenario, test_file_path, DATE_TIME_FORMAT_DEFAULT);
482+
scenario
483+
.env("POSIXLY_CORRECT", "1")
484+
.env("LC_TIME", "C")
485+
.args(&[test_file_path])
486+
.succeeds()
487+
.stdout_is_templated_fixture(expected_test_file_path, &[("{last_modified_time}", &value)]);
488+
}
489+
401490
#[test]
402491
fn test_with_pr_core_utils_tests() {
403492
let test_cases = vec![

0 commit comments

Comments
 (0)