diff options
-rw-r--r-- | mmv.1 | 24 | ||||
-rw-r--r-- | src/main.rs | 64 |
2 files changed, 73 insertions, 15 deletions
@@ -1,4 +1,4 @@ -.Dd $Mdocdate: September 18 2023 $ +.Dd $Mdocdate: September 24 2023 $ .Dt MMV 1 .Os .Sh NAME @@ -7,11 +7,11 @@ .Nd mapped file moves and -copies .Sh SYNOPSIS .Nm -.Op Fl 0deinv +.Op Fl 0bdeinv .Ar command .Op Ar argument ... .Nm mcp -.Op Fl 0deiv +.Op Fl 0bdeiv .Ar command .Op Ar argument ... .Sh DESCRIPTION @@ -62,6 +62,14 @@ separated instead of newline .Pq Sq \en separated. This is useful if input filenames might contain embedded newline characters. +.It Fl b , Fl Fl basename +Only apply the mapping command to the basenames of the given file paths. +This stops you from accidentally mutating directory components, which is not +typically a desired behavior. +If no basename can be derived +.Pq the root directory for example has no basename , +then a warning diagnostic will be printed to the standard error, and the path +will remain unchanged. .It Fl d , Fl Fl dry-run Print the renamings that would take place with the given inputs and arguments to the standard error without actually executing any moves. @@ -192,6 +200,16 @@ knowledge of the input files position in the input list .Pc : .Pp .Dl $ ls --zero | mmv -0i cmd +.Pp +Uppercase the files in the +.Pa /foo/bar +directory, while leaving the names of +.Pa foo +and +.Pa bar +unchanged: +.Pp +.Dl $ ls /foo/bar/* | mmv -b tr a-z A-Z .Sh SEE ALSO .Xr awk 1 , .Xr cp 1 , diff --git a/src/main.rs b/src/main.rs index c1f1311..c3ac72c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::{ cmp::Reverse, - collections::{hash_map::DefaultHasher, HashSet}, + collections::{hash_map::DefaultHasher, HashSet, VecDeque}, env, ffi::OsString, fs, @@ -24,6 +24,7 @@ const MCP_DEFAULT_NAME: &str = "mcp"; struct Flags { pub backup: bool, + pub basename: bool, pub dryrun: bool, pub encode: bool, pub individual: bool, @@ -36,6 +37,7 @@ impl Default for Flags { fn default() -> Self { Flags { backup: true, + basename: false, dryrun: false, encode: false, individual: false, @@ -66,6 +68,7 @@ impl Flags { while let Some(arg) = parser.next()? { match arg { Short('0') | Long("nul") => flags.nul = true, + Short('b') | Long("basename") => flags.basename = true, Short('d') | Long("dry-run") => flags.dryrun = true, Short('e') | Long("encode") => flags.encode = true, Short('i') | Long("individual") => flags.individual = true, @@ -92,12 +95,12 @@ fn usage(bad_flags: Option<lexopt::Error>) -> ! { let mcp_name = option_env!("MCP_NAME").unwrap_or(MCP_DEFAULT_NAME); if p == mcp_name { eprintln!( - "Usage: {} [-0deiv] command [argument ...]", + "Usage: {} [-0bdeiv] command [argument ...]", p.to_str().unwrap() ); } else { eprintln!( - "Usage: {} [-0deinv] command [argument ...]", + "Usage: {} [-0bdeinv] command [argument ...]", p.to_str().unwrap() ); } @@ -303,17 +306,25 @@ fn run_indiv( err!("Failed to spawn utility: “{}”: {e}", cmd.to_str().unwrap()); }); + let mut components = vec![]; { let mut ci = child.stdin.take().unwrap_or_else(|| { err!("Could not open the child process’ stdin"); }); + let s; + if flags.basename { + components = Path::new(src).components().collect_vec(); + s = components.pop().unwrap().as_os_str().to_str().unwrap(); + } else { + s = src; + } write!( ci, "{}", if flags.encode { - encode_string(src) + encode_string(s) } else { - src.to_owned() + s.to_string() } )?; } @@ -323,11 +334,18 @@ fn run_indiv( }); let mut s = String::with_capacity(src.len()); co.read_to_string(&mut s)?; - dsts.push(if flags.encode { + let s = if flags.encode { decode_string(s.as_str()) } else { s - }); + }; + + if flags.basename { + let path = components.iter().collect::<PathBuf>().join(s); + dsts.push(path.to_str().unwrap().to_string()); + } else { + dsts.push(s); + } /* If the process failed, it is expected to print an error message; as such, we exit directly. */ @@ -356,19 +374,36 @@ fn run_multi( }); /* Pass the source files to the child process. */ + let mut components_vec = VecDeque::with_capacity(srcs.len()); + { let ci = child.stdin.take().unwrap_or_else(|| { err!("Could not open the child process’ stdin"); }); let mut ci = BufWriter::new(ci); for src in srcs { + let s = if flags.basename { + let components = Path::new(src).components().collect_vec(); + components_vec.push_back(components); + components_vec + .back_mut() + .unwrap() + .pop() + .unwrap() + .as_os_str() + .to_str() + .unwrap() + } else { + src + }; + write!( ci, "{}", if flags.encode { - encode_string(src) + encode_string(s) } else { - src.to_owned() + s.to_owned() } )?; ci.write_all(if flags.nul && !flags.encode { @@ -395,11 +430,16 @@ fn run_multi( }) .for_each(|x| { let dst = require!(String::from_utf8(x.collect_vec())); - dsts.push(if flags.encode { - decode_string(&dst) + + let s = if flags.basename { + let components = require!(components_vec.pop_front(), "WOW"); + let path = components.iter().collect::<PathBuf>().join(dst); + path.to_str().unwrap().to_string() } else { dst - }); + }; + + dsts.push(if flags.encode { decode_string(&s) } else { s }); }); /* If the process failed, it is expected to print an error message; as such, |