use super::{Cmd, Feedback}; use crate::mode::{Mode, Parser}; use clap::{Arg, ArgAction, Command}; use std::{ error::Error, fs::{self, File, Permissions}, io, os::unix::prelude::{MetadataExt, PermissionsExt}, path::PathBuf, }; use walkdir::{DirEntry, WalkDir}; #[derive(Debug, Default)] pub struct Chmod; impl Cmd for Chmod { fn cli(&self) -> clap::Command { Command::new("chmod") .about("change file mode bits") .author("Nathan Fisher") .version(env!("CARGO_PKG_VERSION")) .args([ Arg::new("verbose") .short('v') .long("verbose") .help("output a diagnostic for every file processed") .action(ArgAction::SetTrue), Arg::new("changes") .short('c') .long("changes") .help("like verbose but report only when a change is made") .conflicts_with("verbose") .action(ArgAction::SetTrue), Arg::new("quiet") .short('f') .long("silent") .visible_alias("quiet") .help("suppress most error messages") .action(ArgAction::SetTrue), Arg::new("recursive") .short('R') .long("recursive") .visible_short_alias('r') .help("change files and directories recursively") .action(ArgAction::SetTrue), Arg::new("mode") .value_name("MODE") .value_delimiter(',') .num_args(1) .required(true), Arg::new("file") .value_name("FILE") .num_args(1..) .required(true), ]) } fn run(&self, matches: Option<&clap::ArgMatches>) -> Result<(), Box> { let Some(matches) = matches else { return Err(io::Error::new(io::ErrorKind::Other, "no input").into()); }; let feedback = Feedback::from_matches(matches); let mode = matches .get_one::("mode") .ok_or(io::Error::new(io::ErrorKind::Other, "no mode given"))?; if let Some(files) = matches.get_many::("file") { for f in files { let mut path = PathBuf::from(f); if path.is_symlink() { path = fs::read_link(&path)?; } let action = Action { path, feedback, mode, }; if let Err(e) = action.apply() { if !matches.get_flag("quiet") { return Err(e.into()); } } if matches.get_flag("recursive") { if action.path.is_dir() { if let Err(e) = action.recurse() { if !matches.get_flag("quiet") { return Err(e.into()); } } } } } } Ok(()) } fn path(&self) -> Option { Some(crate::Path::Bin) } } struct Action<'a> { path: PathBuf, feedback: Option, mode: &'a str, } impl Action<'_> { fn apply(&self) -> Result<(), Box> { let oldmode = { let fd = File::open(&self.path)?; let meta = fd.metadata()?; meta.mode() }; let mut parser = Parser::new(oldmode); let mode = parser.parse(self.mode)?; let permissions = Permissions::from_mode(mode); fs::set_permissions(&self.path, permissions)?; if let Some(f) = self.feedback { match f { Feedback::Full => { if oldmode == mode { self.display_retained(oldmode)?; } else { self.display_changes(oldmode, mode)?; } } Feedback::Changes => { if oldmode != mode { self.display_changes(oldmode, mode)?; } } } } Ok(()) } fn display_changes(&self, oldmode: u32, mode: u32) -> Result<(), Box> { let oldstring = oldmode.mode_string()?; let newstring = mode.mode_string()?; println!( "mode of '{}' changed from {oldmode:o} ({oldstring}) to {mode:o} ({newstring})", self.path.display() ); Ok(()) } fn display_retained(&self, mode: u32) -> Result<(), Box> { let modestring = mode.mode_string()?; println!( "mode of '{}' retained as {mode:o} ({modestring})", self.path.display() ); Ok(()) } fn into_child(&self, entry: DirEntry) -> Self { Self { path: entry.path().to_path_buf(), feedback: self.feedback, mode: self.mode, } } fn recurse(&self) -> Result<(), Box> { let walker = WalkDir::new(&self.path).max_open(1); for entry in walker { let entry = entry?; let action = self.into_child(entry); action.apply()?; } Ok(()) } }