aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock116
-rw-r--r--Cargo.toml8
-rw-r--r--LICENSE14
-rw-r--r--src/encoded_string.rs26
-rw-r--r--src/error.rs34
-rw-r--r--src/main.rs112
-rw-r--r--src/main_result.rs32
8 files changed, 343 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..efe1d36
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,116 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "mmv"
+version = "0.1.0"
+dependencies = [
+ "itertools",
+ "tempfile",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..a1f3210
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "mmv"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+itertools = "0.10.5"
+tempfile = "3.3.0"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6117b2a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2022 Thomas Voss
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/src/encoded_string.rs b/src/encoded_string.rs
new file mode 100644
index 0000000..55090a9
--- /dev/null
+++ b/src/encoded_string.rs
@@ -0,0 +1,26 @@
+use std::str;
+
+pub struct EncodedString<'a> {
+ pub s: str::Bytes<'a>,
+}
+
+impl<'a> Iterator for EncodedString<'a> {
+ type Item = u8;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.s.next().map(|c| match c {
+ b'\\' => match self.s.next() {
+ Some(b'n') => b'\n',
+ Some(b'\\') | None => b'\\',
+ Some(c) => c
+ },
+ c => c
+ })
+ }
+}
+
+impl<'a> EncodedString<'a> {
+ pub fn decode(self) -> String {
+ String::from_utf8(self.s.collect()).unwrap()
+ }
+}
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..06bfbbe
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,34 @@
+use std::{
+ env,
+ fmt::{self, Display},
+ io
+};
+
+pub enum Error {
+ BadLengths,
+ DuplicateElems(Vec<String>),
+ IOError(io::Error),
+ NoEditor,
+ SpawnFailed(String, io::Error),
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let p = env::args().next().unwrap();
+ match self {
+ 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::IOError(e) => writeln!(f, "{p}: {e}"),
+ Self::NoEditor => writeln!(f, "{p}: \"EDITOR\" environment variable is not set"),
+ Self::SpawnFailed(ed, e) => writeln!(f, "{p}: Failed to spawn editor \"{ed}\": {e}")
+ }
+ }
+}
+
+impl From<io::Error> for Error {
+ fn from(e: io::Error) -> Self {
+ Self::IOError(e)
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..e6b1b78
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,112 @@
+mod encoded_string;
+mod error;
+mod main_result;
+
+use std::{
+ collections::HashSet,
+ env,
+ hash::Hash,
+ io::{self, BufRead, BufReader, BufWriter, Write, Seek, SeekFrom},
+ process::Command
+};
+
+use encoded_string::*;
+use error::*;
+use main_result::*;
+
+use {
+ itertools::Itertools,
+ tempfile::NamedTempFile
+};
+
+fn main() -> MainResult {
+ work().into()
+}
+
+fn work() -> Result<(), Error> {
+ let files = env::args().skip(1).collect::<Vec<String>>();
+ let dups = duplicate_elements(files.clone());
+ if !dups.is_empty() {
+ return Err(Error::DuplicateElems(dups));
+ }
+
+ let mut tmpfile = NamedTempFile::new()?;
+ let mut writer = BufWriter::new(&tmpfile);
+
+ files.iter().try_for_each(|f| encode_to_file(&mut writer, f))?;
+ writer.flush()?;
+ drop(writer);
+
+ let editor = env::var("EDITOR")
+ .ok()
+ .filter(|e| !e.is_empty())
+ .ok_or(Error::NoEditor)?;
+
+ Command::new(&editor)
+ .arg(tmpfile.path().as_os_str())
+ .spawn()
+ .map_err(|err| Error::SpawnFailed(editor, err))?
+ .wait()?;
+
+ tmpfile.seek(SeekFrom::Start(0))?;
+ let new_files = decode_from_file(&tmpfile)?;
+
+ assert_changes(
+ files.iter().cloned().collect(),
+ new_files.iter().cloned().collect()
+ )?;
+ new_files.iter().for_each(|f| println!("{}", f));
+
+ Ok(())
+}
+
+fn assert_changes(old: Vec<String>, new: Vec<String>) -> Result<(), Error> {
+ if old.len() != new.len() {
+ return Err(Error::BadLengths);
+ }
+
+ let dups = duplicate_elements(new);
+ if !dups.is_empty() {
+ return Err(Error::DuplicateElems(dups));
+ }
+
+ Ok(())
+}
+
+fn duplicate_elements<T>(iter: T) -> Vec<T::Item>
+where
+ T: IntoIterator,
+ T::Item: Clone + Eq + Hash
+{
+ let mut uniq = HashSet::new();
+ iter
+ .into_iter()
+ .filter(|x| !uniq.insert(x.clone()))
+ .unique()
+ .collect::<Vec<_>>()
+}
+
+fn encode_to_file<W: Write>(f: &mut W, s: &str) -> io::Result<()> {
+ s.chars().try_for_each(|c| {
+ write!(f, "{}", match c {
+ '\\' => "\\\\",
+ '\n' => "\\n",
+ _ => return write!(f, "{}", c),
+ }
+ )
+ })?;
+ write!(f, "{}", '\n')
+}
+
+fn decode_from_file(tmpfile: &NamedTempFile) -> Result<Vec<String>, io::Error> {
+ BufReader::new(tmpfile)
+ .lines()
+ .map(|r| match r {
+ Ok(s) => {
+ let es = EncodedString { s: s.bytes() };
+ Ok(es.decode())
+ },
+ Err(_) => r
+ })
+ .collect::<Result<Vec<String>, _>>()
+}
diff --git a/src/main_result.rs b/src/main_result.rs
new file mode 100644
index 0000000..2d28874
--- /dev/null
+++ b/src/main_result.rs
@@ -0,0 +1,32 @@
+use std::{
+ io::{self, Write},
+ process::{ExitCode, Termination}
+};
+
+use super::error::Error;
+
+pub enum MainResult {
+ Success,
+ Failure(Error)
+}
+
+impl Termination for MainResult {
+ fn report(self) -> ExitCode {
+ match self {
+ Self::Success => ExitCode::SUCCESS,
+ Self::Failure(e) => {
+ let _ = write!(io::stderr(), "{e}");
+ ExitCode::FAILURE
+ }
+ }
+ }
+}
+
+impl From<Result<(), Error>> for MainResult {
+ fn from(r: Result<(), Error>) -> Self {
+ match r {
+ Ok(()) => Self::Success,
+ Err(e) => Self::Failure(e)
+ }
+ }
+}