diff options
author | Thomas Voss <mail@thomasvoss.com> | 2022-11-05 00:04:54 +0100 |
---|---|---|
committer | Thomas Voss <mail@thomasvoss.com> | 2022-11-05 00:04:54 +0100 |
commit | fb7d63f1a9c553ed0b48569db2e646f42684be55 (patch) | |
tree | a216cd632b80af6b5ab6ac69bb727af3cd2865d9 /src | |
parent | 65f315266bff003ef4d66bbd351871ae341559dd (diff) |
Begin work on v2.0.0
The original release of mmv took filenames as command line arguments,
opened them in an editor, and then used the saved changes to rename
files. This commit begins work on a new version of mmv where files are
provided via the standard input and the command line arguments specify a
process to spawn. The spawned process reads command line arguments from
the standard input, processes them, and prints new names to the standard
output. Those new names represent the new file names. Here are a few
example usages, some more useful than others.
Reverse file names:
$ ls * | mmv rev
Edit file names in your editor (v1.0.0 behavior):
$ ls * | mmv vipe
Number movies so they’re automatically sorted:
$ ls movie1 movie2 ... movieN \
| mmv awk '{ printf "%02d-%s\n", NR, $0 }'
Diffstat (limited to 'src')
-rw-r--r-- | src/error.rs | 29 | ||||
-rw-r--r-- | src/flags.rs | 16 | ||||
-rw-r--r-- | src/main.rs | 78 |
3 files changed, 91 insertions, 32 deletions
diff --git a/src/error.rs b/src/error.rs index f303780..7df065d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,18 @@ use std::{ env, fmt::{self, Display}, - io + io, + string }; pub enum Error { + BadArgs, BadLengths, - DuplicateElems(Vec<String>), + DupInputElems(Vec<String>), + DupOutputElems(Vec<String>), IOError(io::Error), - NoArgs, - NoEditor, + Nop, + UTF8Error(string::FromUtf8Error), SpawnFailed(String, io::Error), } @@ -17,13 +20,17 @@ impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let p = env::args().next().unwrap(); match self { + Self::BadArgs => writeln!(f, "Usage: {p} [-0ei] [--] utility [argument ...]"), Self::BadLengths => writeln!(f, "{p}: Files have been added or removed during editing"), - Self::DuplicateElems(ds) => ds.iter().try_for_each( - |d| writeln!(f, "{p}: Multiple files named \"{}\" specified", d) + Self::DupInputElems(ds) => ds.iter().try_for_each( + |d| writeln!(f, "{p}: Multiple input files named \"{}\" specified", d) + ), + Self::DupOutputElems(ds) => ds.iter().try_for_each( + |d| writeln!(f, "{p}: Multiple output files named \"{}\" specified", d) ), Self::IOError(e) => writeln!(f, "{p}: {e}"), - Self::NoArgs => writeln!(f, "Usage: {p} file ..."), - Self::NoEditor => writeln!(f, "{p}: \"EDITOR\" environment variable is not set"), + Self::Nop => Ok(()), + Self::UTF8Error(e) => writeln!(f, "{p}: {e}"), Self::SpawnFailed(ed, e) => writeln!(f, "{p}: Failed to spawn editor \"{ed}\": {e}") } } @@ -34,3 +41,9 @@ impl From<io::Error> for Error { Self::IOError(e) } } + +impl From<string::FromUtf8Error> for Error { + fn from(e: string::FromUtf8Error) -> Self { + Self::UTF8Error(e) + } +} diff --git a/src/flags.rs b/src/flags.rs new file mode 100644 index 0000000..504f0f9 --- /dev/null +++ b/src/flags.rs @@ -0,0 +1,16 @@ +#[derive(Debug)] +pub struct Flags { + pub encode: bool, + pub individual: bool, + pub nul: bool +} + +impl Default for Flags { + fn default() -> Flags { + Flags { + encode: false, + individual: false, + nul: false + } + } +} diff --git a/src/main.rs b/src/main.rs index 71052a2..3272041 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod encoded_string; mod error; +mod flags; mod main_result; use std::{ @@ -7,9 +8,9 @@ use std::{ env, fs, hash::Hash, - io::{self, BufRead, BufReader, BufWriter, Write, Seek, SeekFrom}, + io::{self, BufRead, BufReader, Write}, path::Path, - process::Command + process::{Command, Stdio} }; use { @@ -19,6 +20,8 @@ use { }; use { + flags::Flags, + getopt::{Opt, Parser}, itertools::Itertools, tempfile::{Builder, NamedTempFile, TempDir} }; @@ -28,44 +31,71 @@ fn main() -> MainResult { } fn work() -> Result<(), Error> { - let old_files = env::args().skip(1).collect::<Vec<String>>(); - if old_files.is_empty() { - return Err(Error::NoArgs); + let mut argv = env::args().collect::<Vec<String>>(); + let mut flags = Flags { ..Default::default() }; + let mut opts = Parser::new(&argv, ":0a"); + + loop { + match opts.next().transpose() { + Ok(v) => match v { + None => break, + Some(opt) => match opt { + Opt('0', None) => flags.nul = true, + Opt('e', None) => flags.encode = true, + Opt('i', None) => flags.individual = true, + _ => { return Err(Error::BadArgs); } + } + }, + Err(_) => { return Err(Error::BadArgs); } + } + } + + let rest = argv.split_off(opts.index()); + let cmd = rest.get(0).ok_or(Error::BadArgs)?; + let args = &rest[1..]; + + let old_files: Vec<_> = io::stdin() + .lines() + .collect::<Result<_, _>>() + .unwrap(); + if old_files.iter().any(|s| s.is_empty()) { + return Err(Error::BadArgs); } let dups = duplicate_elements(old_files.clone()); if !dups.is_empty() { - return Err(Error::DuplicateElems(dups)); + return Err(Error::DupInputElems(dups)); } - let mut tmpfile = NamedTempFile::new()?; - let mut writer = BufWriter::new(&tmpfile); - - old_files.iter().try_for_each(|f| encode_to_file(&mut writer, f))?; - writer.flush()?; - drop(writer); + let mut child = Command::new(cmd) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .map_err(|e| Error::SpawnFailed(cmd.to_owned(), e))?; - let editor = env::var("EDITOR") - .ok() - .filter(|e| !e.is_empty()) - .ok_or(Error::NoEditor)?; + { + let mut stdin = child.stdin.take().expect("Failed to open stdin"); + old_files.iter().try_for_each(|f| writeln!(stdin, "{f}"))?; + } - Command::new(&editor) - .arg(tmpfile.path().as_os_str()) - .spawn() - .map_err(|err| Error::SpawnFailed(editor, err))? - .wait()?; + let output = child.wait_with_output()?; + if !output.status.success() { + return Err(Error::Nop); + } - tmpfile.seek(SeekFrom::Start(0))?; + let new_files = String::from_utf8(output.stdout)? + .lines() + .map(|s| s.to_string()) + .collect::<Vec<String>>(); - let new_files = decode_from_file(&tmpfile)?; if old_files.len() != new_files.len() { return Err(Error::BadLengths); } let dups = duplicate_elements(new_files.iter().cloned().collect::<Vec<_>>()); if !dups.is_empty() { - return Err(Error::DuplicateElems(dups)); + return Err(Error::DupOutputElems(dups)); } let tmpdir = Builder::new().prefix("mmv").tempdir()?; |