shitbox/corebox/commands/touch/mod.rs
Nathan Fisher d248f7d17b Remove Default constraint on Cmd trait and begin removing
derive(Default) from applets
2023-04-17 23:27:57 -04:00

127 lines
4.9 KiB
Rust

use chrono::DateTime;
use clap::{value_parser, Arg, ArgAction, ArgGroup, Command};
use filetime::*;
use shitbox::Cmd;
use std::{error::Error, fs::File, io, path::PathBuf};
#[derive(Debug)]
pub struct Touch;
impl Cmd for Touch {
fn cli(&self) -> clap::Command {
Command::new("touch")
.about("set file timestamps")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("access")
.help("set the access time of the file")
.short('a')
.long("access")
.action(ArgAction::SetTrue),
Arg::new("modify")
.help("set the modification time of the file")
.short('m')
.long("modification")
.conflicts_with("access")
.action(ArgAction::SetTrue),
Arg::new("create")
.help("Don't create file if it doesn't exist, not affecting exit status.")
.short('c')
.long("nocreate")
.action(ArgAction::SetFalse),
Arg::new("datetime")
.help("Set the time of the format YYYY-MM-DDThh:mm:SS[Z] used for [-am].")
.short('d')
.long("datetime")
.value_name("time")
.num_args(1),
Arg::new("ref")
.help("Set the time used for [-am] to the modification time of ref_file.")
.short('r')
.long("ref")
.value_name("file")
.num_args(1),
Arg::new("epoch")
.help(
"Set the time used for [-am] given as the number of seconds \
since the Unix epoch 1970-01-01T00:00:00Z.",
)
.short('T')
.long("epoch")
.value_name("time")
.value_parser(value_parser!(i64))
.num_args(1),
Arg::new("time")
.help("Set the time of the format [[CC]YY]MMDDhhmm[.SS] used for [-am].")
.short('t')
.long("time")
.value_name("time")
.num_args(1),
Arg::new("file").num_args(1..).required(true),
])
.group(
ArgGroup::new("timespec")
.args(["datetime", "ref", "epoch", "time"])
.multiple(false),
)
}
fn run(&self, matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let ft = if let Some(time) = matches.get_one::<String>("datetime") {
FileTime::from_unix_time(parse_datetime(time)?, 0)
} else if let Some(time) = matches.get_one::<String>("time") {
FileTime::from_unix_time(parse_time(time)?, 0)
} else if let Some(ep) = matches.get_one::<i64>("epoch") {
FileTime::from_unix_time(*ep, 0)
} else if let Some(file_ref) = matches.get_one::<String>("ref") {
let meta = File::open(file_ref)?.metadata()?;
FileTime::from_last_modification_time(&meta)
} else {
FileTime::now()
};
if let Some(files) = matches.get_many::<String>("file") {
for f in files {
let path = PathBuf::from(f);
if !path.exists() && matches.get_flag("create") {
File::create(&path)?;
}
if matches.get_flag("access") {
set_file_atime(&path, ft)?;
} else if matches.get_flag("modify") {
set_file_mtime(&path, ft)?;
} else {
set_file_times(&path, ft, ft)?;
}
}
}
Ok(())
}
fn path(&self) -> Option<shitbox::Path> {
Some(shitbox::Path::UsrBin)
}
}
fn parse_datetime(time: &str) -> Result<i64, Box<dyn Error>> {
match time.len() {
19 => Ok(DateTime::parse_from_str(time, "%Y-%m-%dT%H:%M:%S")?.timestamp()),
25 => Ok(DateTime::parse_from_rfc3339(time)?.timestamp()),
_ => Err(io::Error::new(io::ErrorKind::Other, "invalid datetime string").into()),
}
}
fn parse_time(time: &str) -> Result<i64, Box<dyn Error>> {
let dt = match time.len() {
8 => DateTime::parse_from_str(time, "%m%d%H%M")?,
10 => DateTime::parse_from_str(time, "%y%m%d%H%M")?,
11 => DateTime::parse_from_str(time, "%m%d%H%M.%S")?,
12 => DateTime::parse_from_str(time, "%Y%m%d%H%M")?,
13 => DateTime::parse_from_str(time, "%y%m%d%H%M.%S")?,
15 => DateTime::parse_from_str(time, "%Y%m%d%H%M.%S")?,
_ => return Err(io::Error::new(io::ErrorKind::Other, "invalid datetime string").into()),
};
Ok(dt.timestamp())
}