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, Default)] 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> { let ft = if let Some(time) = matches.get_one::("datetime") { FileTime::from_unix_time(parse_datetime(time)?, 0) } else if let Some(time) = matches.get_one::("time") { FileTime::from_unix_time(parse_time(time)?, 0) } else if let Some(ep) = matches.get_one::("epoch") { FileTime::from_unix_time(*ep, 0) } else if let Some(file_ref) = matches.get_one::("ref") { let meta = File::open(file_ref)?.metadata()?; FileTime::from_last_modification_time(&meta) } else { FileTime::now() }; if let Some(files) = matches.get_many::("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 { Some(shitbox::Path::UsrBin) } } fn parse_datetime(time: &str) -> Result> { 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> { 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()) }