From bb0f6f3a76d9e1099460fb2b3b7b41653d697898 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Fri, 4 Nov 2022 11:32:07 +0100 Subject: Initial commit --- src/encoded_string.rs | 26 ++++++++++++ src/error.rs | 34 +++++++++++++++ src/main.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main_result.rs | 32 +++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 src/encoded_string.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/main_result.rs (limited to 'src') 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.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), + 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 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::>(); + 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, new: Vec) -> 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(iter: T) -> Vec +where + T: IntoIterator, + T::Item: Clone + Eq + Hash +{ + let mut uniq = HashSet::new(); + iter + .into_iter() + .filter(|x| !uniq.insert(x.clone())) + .unique() + .collect::>() +} + +fn encode_to_file(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, io::Error> { + BufReader::new(tmpfile) + .lines() + .map(|r| match r { + Ok(s) => { + let es = EncodedString { s: s.bytes() }; + Ok(es.decode()) + }, + Err(_) => r + }) + .collect::, _>>() +} 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> for MainResult { + fn from(r: Result<(), Error>) -> Self { + match r { + Ok(()) => Self::Success, + Err(e) => Self::Failure(e) + } + } +} -- cgit v1.2.3