mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
touch: support obsolete POSIX timestamp argument
Support obsolete form of timestamp argument for old POSIX versions. In
summary, when older versions of POSIX are used and the first positional
argument looks like a date and time, then treat it as a timestamp
instead of as a filename. For example, before this commit
_POSIX2_VERSION=199209
POSIXLY_CORRECT=1
touch 01010000 11111111
would create two files, `01010000` and `11111111`. After this commit,
the first argument is interpreted as a date and time (in this case,
midnight on January 1 of the current year) and that date and time are
set on the file named `11111111`.
Fixes #7180.
This commit is contained in:
+69
-14
@@ -135,12 +135,57 @@ fn filetime_to_datetime(ft: &FileTime) -> Option<DateTime<Local>> {
|
||||
Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into())
|
||||
}
|
||||
|
||||
/// Whether all characters in the string are digits.
|
||||
fn all_digits(s: &str) -> bool {
|
||||
s.as_bytes().iter().all(u8::is_ascii_digit)
|
||||
}
|
||||
|
||||
/// Convert a two-digit year string to the corresponding number.
|
||||
fn get_year(s: &str) -> u8 {
|
||||
// Pre-condition: s.len() >= 2
|
||||
let bytes = s.as_bytes();
|
||||
let y1 = bytes[0] - b'0';
|
||||
let y2 = bytes[1] - b'0';
|
||||
10 * y1 + y2
|
||||
}
|
||||
|
||||
/// Whether the first filename should be interpreted as a timestamp.
|
||||
fn is_first_filename_timestamp(
|
||||
reference: Option<&OsString>,
|
||||
date: Option<&str>,
|
||||
timestamp: Option<&String>,
|
||||
files: &[&String],
|
||||
) -> bool {
|
||||
match std::env::var("_POSIX2_VERSION") {
|
||||
Ok(s) if s == "199209" => {
|
||||
if timestamp.is_none() && reference.is_none() && date.is_none() {
|
||||
if files.len() >= 2 {
|
||||
let s = files[0];
|
||||
if s.len() == 8 && all_digits(s) {
|
||||
true
|
||||
} else if s.len() == 10 && all_digits(s) {
|
||||
let year = get_year(s);
|
||||
(69..=99).contains(&year)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[uucore::main]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let matches = uu_app().try_get_matches_from(args)?;
|
||||
|
||||
let files: Vec<InputFile> = matches
|
||||
.get_many::<OsString>(ARG_FILES)
|
||||
let mut filenames: Vec<&String> = matches
|
||||
.get_many::<String>(ARG_FILES)
|
||||
.ok_or_else(|| {
|
||||
USimpleError::new(
|
||||
1,
|
||||
@@ -150,19 +195,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
),
|
||||
)
|
||||
})?
|
||||
.map(|filename| {
|
||||
if filename == "-" {
|
||||
InputFile::Stdout
|
||||
} else {
|
||||
InputFile::Path(PathBuf::from(filename))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let no_deref = matches.get_flag(options::NO_DEREF);
|
||||
|
||||
let reference = matches.get_one::<OsString>(options::sources::REFERENCE);
|
||||
let timestamp = matches.get_one::<String>(options::sources::TIMESTAMP);
|
||||
let date = matches
|
||||
.get_one::<String>(options::sources::DATE)
|
||||
.map(|date| date.to_owned());
|
||||
|
||||
let mut timestamp = matches.get_one::<String>(options::sources::TIMESTAMP);
|
||||
|
||||
if is_first_filename_timestamp(reference, date.as_deref(), timestamp, &filenames) {
|
||||
let head = filenames[0];
|
||||
let tail = &filenames[1..];
|
||||
timestamp = Some(head);
|
||||
filenames = tail.to_vec();
|
||||
}
|
||||
|
||||
let source = if let Some(reference) = reference {
|
||||
Source::Reference(PathBuf::from(reference))
|
||||
@@ -172,9 +221,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
Source::Now
|
||||
};
|
||||
|
||||
let date = matches
|
||||
.get_one::<String>(options::sources::DATE)
|
||||
.map(|date| date.to_owned());
|
||||
let files: Vec<InputFile> = filenames
|
||||
.into_iter()
|
||||
.map(|filename| {
|
||||
if filename == "-" {
|
||||
InputFile::Stdout
|
||||
} else {
|
||||
InputFile::Path(PathBuf::from(filename))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let opts = Options {
|
||||
no_create: matches.get_flag(options::NO_CREATE),
|
||||
@@ -275,7 +331,6 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(ARG_FILES)
|
||||
.action(ArgAction::Append)
|
||||
.num_args(1..)
|
||||
.value_parser(ValueParser::os_string())
|
||||
.value_hint(clap::ValueHint::AnyPath),
|
||||
)
|
||||
.group(
|
||||
|
||||
@@ -917,3 +917,27 @@ fn test_touch_reference_symlink_with_no_deref() {
|
||||
// Times should be taken from the symlink, not the destination
|
||||
assert_eq!((time, time), get_symlink_times(&at, arg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_obsolete_posix_format() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
ucmd.env("_POSIX2_VERSION", "199209")
|
||||
.env("POSIXLY_CORRECT", "1")
|
||||
.args(&["01010000", "11111111"])
|
||||
.succeeds()
|
||||
.no_output();
|
||||
assert!(at.file_exists("11111111"));
|
||||
assert!(!at.file_exists("01010000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_obsolete_posix_format_with_year() {
|
||||
let (at, mut ucmd) = at_and_ucmd!();
|
||||
ucmd.env("_POSIX2_VERSION", "199209")
|
||||
.env("POSIXLY_CORRECT", "1")
|
||||
.args(&["9001010000", "11111111"])
|
||||
.succeeds()
|
||||
.no_output();
|
||||
assert!(at.file_exists("11111111"));
|
||||
assert!(!at.file_exists("01010000"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user