diff --git a/src/filters.rs b/src/filters.rs index dc6d783..8c45e6c 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub(crate) enum FileFilter { None, Extension(ExtensionFilter), @@ -16,6 +17,7 @@ impl FileFilter { } } +#[derive(Clone)] pub(crate) struct ExtensionFilter { ext: String, } diff --git a/src/fixer.rs b/src/fixer.rs new file mode 100644 index 0000000..13774aa --- /dev/null +++ b/src/fixer.rs @@ -0,0 +1,82 @@ +use std::{ + fs::File, + io::{Read, Seek, SeekFrom, Write}, +}; + +use crate::stats::LineSep; + +pub(crate) struct Fixer { + file_name: String, + to: Vec, +} + +impl Fixer { + pub(crate) fn new(file_name: String, to: LineSep) -> Fixer { + let to = match to { + LineSep::Lf => vec![b'\n'], + LineSep::CrLf => vec![b'\r', b'\n'], + LineSep::Cr => vec![b'\r'], + }; + + Fixer { file_name, to } + } + + pub(crate) fn fix(&mut self) { + let file = File::options().read(true).write(true).open(&self.file_name); + if file.is_err() { + println!("Could not open file: {}", self.file_name); + return; + } + + let mut file = file.unwrap(); + let mut buf = Vec::new(); + let mut write_buf = Vec::new(); + let bytes_read = file.read_to_end(&mut buf); + + if bytes_read.is_err() { + println!("Could not read file: {}", self.file_name); + return; + } + + let bytes_read = bytes_read.unwrap(); + let mut next_read_head = 0; + + loop { + if next_read_head == bytes_read { + break; + } + + next_read_head = match buf[next_read_head] { + b'\r' => { + // LL(1) to see if the next byte is '\n' + if next_read_head < bytes_read && buf[next_read_head] == b'\n' { + write_buf.extend(&self.to); + next_read_head + 2 + } else { + next_read_head + 1 + } + } + b'\n' => { + write_buf.extend(&self.to); + next_read_head + 1 + } + any_other_byte => { + write_buf.push(any_other_byte); + next_read_head + 1 + } + } + } + + let seeked = file.seek(SeekFrom::Start(0)); + if seeked.is_err() { + println!("Could not seek to start of file: {}", self.file_name); + return; + } + + let written = file.write_all(&write_buf); + if written.is_err() { + println!("Could not write to file: {}", self.file_name); + println!("Error: {:?}", written.err()); + } + } +} diff --git a/src/main.rs b/src/main.rs index f715f9a..375a111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,9 @@ use argh::FromArgs; use filters::FileFilter; -use stats::{FileNames, FileStats, FileStatsAggregate}; +use fixer::Fixer; +use stats::{FileNames, FileStats, FileStatsAggregate, LineSep}; mod filters; +mod fixer; mod stats; fn main() { @@ -12,10 +14,32 @@ fn main() { None => FileFilter::None, }; - let stats = FileNames::generate(args.dir, filter, args.recursive) + let stats = FileNames::generate(args.dir.clone(), filter.clone(), args.recursive) .filter_map(FileStats::generate) .fold(FileStatsAggregate::new(), FileStatsAggregate::fold); stats.print_table(); + + if args.fix { + let target = match args.target { + Some(target) => { + println!("Fixing line endings to provided line ending - {:}", target); + target + } + None => match stats.max() { + Some(max) => { + println!("Fixing line endings to most common line ending - {:}", max); + max + } + None => { + panic!("No line endings found to fix"); + } + }, + }; + + FileNames::generate(args.dir, filter, args.recursive) + .map(|file_name| Fixer::new(file_name, target.clone())) + .for_each(|mut fixer| fixer.fix()); + } } /// Line endings fixer tool @@ -32,4 +56,21 @@ struct Args { /// recursively traverse directories #[argh(switch, short = 'r')] recursive: bool, + + /// fix line endings + #[argh(switch, short = 'f')] + fix: bool, + + /// target line ending, applicable only with -f + #[argh(option, short = 't', from_str_fn(parse_line_sep))] + target: Option, +} + +fn parse_line_sep(s: &str) -> Result { + match s { + "lf" => Ok(LineSep::Lf), + "crlf" => Ok(LineSep::CrLf), + "cr" => Ok(LineSep::Cr), + _ => Err("Invalid line ending".to_string()), + } } diff --git a/src/stats.rs b/src/stats.rs index 434ca45..8c61d62 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -24,6 +24,7 @@ impl FileStatsAggregate { lines: 0, } } + pub(crate) fn fold(mut accumulator: FileStatsAggregate, stat: FileStats) -> FileStatsAggregate { accumulator.crlf += stat.crlf; accumulator.cr += stat.cr; @@ -45,6 +46,10 @@ impl FileStatsAggregate { accumulator } + pub(crate) fn max(&self) -> Option { + self.max.clone() + } + pub(crate) fn print_table(&self) { println!( "{:<4} | {:<4} | {:<4} | {:<4} | {:<4} | {}", @@ -164,7 +169,7 @@ impl FileStats { } #[derive(Clone)] -enum LineSep { +pub(crate) enum LineSep { Lf, CrLf, Cr,