Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow for many find and replaces #241

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ version = "0.7.6"
name = "sd"
version.workspace = true
edition.workspace = true

authors = ["Gregory <[email protected]>"]
description = "An intuitive find & replace CLI"
readme = "README.md"
Expand Down
5 changes: 5 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ w - match full words only
/// Note: sd modifies files in-place by default. See documentation for
/// examples.
pub files: Vec<std::path::PathBuf>,


/// Extra find and replace pairs.
#[arg(short, long, num_args(2))]
pub extra: Vec<String>,
}

#[cfg(test)]
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fn main() -> Result<()> {
options.literal_mode,
options.flags,
options.replacements,
options.extra,
)?,
)
.run(options.preview)?;
Expand Down
197 changes: 131 additions & 66 deletions src/replacer.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
use crate::{utils, Error, Result};
use regex::bytes::Regex;
use std::{fs, fs::File, io::prelude::*, path::Path};
use std::{borrow::Cow, fs, fs::File, io::prelude::*, path::Path};

pub(crate) struct Replacer {
#[derive(Debug)]
struct Pair {
regex: Regex,
replace_with: Vec<u8>,
is_literal: bool,
replacements: usize,
rep: Vec<u8>,
}

#[derive(Debug)]
pub(crate) struct Replacer {
pairs: Vec<Pair>,
is_literal: bool, // -s
max_replacements: usize,
}

impl Replacer {
Expand All @@ -16,24 +22,31 @@ impl Replacer {
is_literal: bool,
flags: Option<String>,
replacements: Option<usize>,
extra: Vec<String>,
) -> Result<Self> {
let (look_for, replace_with) = if is_literal {
(regex::escape(&look_for), replace_with.into_bytes())
} else {
(
look_for,
utils::unescape(&replace_with)
.unwrap_or(replace_with)
.into_bytes(),
)
};

let mut regex = regex::bytes::RegexBuilder::new(&look_for);
regex.multi_line(true);

if let Some(flags) = flags {
flags.chars().for_each(|c| {
#[rustfmt::skip]
fn create(
look_for: String,
replace_with: String,
is_literal: bool,
flags: Option<&str>,
) -> Result<Pair> {
let (look_for, replace_with) = if is_literal {
(regex::escape(&look_for), replace_with.into_bytes())
} else {
(
look_for,
utils::unescape(&replace_with)
.unwrap_or(replace_with)
.into_bytes(),
)
};

let mut regex = regex::bytes::RegexBuilder::new(&look_for);
regex.multi_line(true);

if let Some(flags) = flags {
for c in flags.chars() {
#[rustfmt::skip]
match c {
'c' => { regex.case_insensitive(false); },
'i' => { regex.case_insensitive(true); },
Expand All @@ -53,19 +66,46 @@ impl Replacer {
},
_ => {},
};
});
};
}
};
Ok(Pair {
regex: regex.build()?,
rep: replace_with,
})
}

Ok(Self {
regex: regex.build()?,
let capacity = extra.len() / 2 + 1;
let mut pairs = Vec::with_capacity(capacity);
pairs.push(create(
look_for,
replace_with,
is_literal,
replacements: replacements.unwrap_or(0),
flags.as_deref(),
)?);

let mut it = extra.into_iter();
while let Some(look_for) = it.next() {
let replace_with = it
.next()
.expect("The extra pattern list doesn't have an even lenght");

pairs.push(create(
look_for,
replace_with,
is_literal,
flags.as_deref(),
)?);
}

Ok(Self {
pairs,
is_literal,
max_replacements: replacements.unwrap_or(0),
})
}

pub(crate) fn has_matches(&self, content: &[u8]) -> bool {
self.regex.is_match(content)
self.pairs.iter().any(|r| r.regex.is_match(content))
}

pub(crate) fn check_not_empty(mut file: File) -> Result<()> {
Expand All @@ -74,50 +114,56 @@ impl Replacer {
Ok(())
}

pub(crate) fn replace<'a>(
&'a self,
content: &'a [u8],
) -> std::borrow::Cow<'a, [u8]> {
if self.is_literal {
self.regex.replacen(
content,
self.replacements,
regex::bytes::NoExpand(&self.replace_with),
)
} else {
self.regex
.replacen(content, self.replacements, &*self.replace_with)
pub(crate) fn replace<'a>(&'a self, content: &'a [u8]) -> Cow<'a, [u8]> {
let mut result = Cow::Borrowed(content);
for Pair { regex, rep } in self.pairs.iter() {
let res = if self.is_literal {
let rep = regex::bytes::NoExpand(rep.as_slice());
regex.replacen(&result, self.max_replacements, rep)
} else {
regex.replacen(&result, self.max_replacements, rep)
};

result = Cow::Owned(res.into_owned());
}
result
}

pub(crate) fn replace_preview<'a>(
&'a self,
content: &[u8],
) -> std::borrow::Cow<'a, [u8]> {
let mut v = Vec::<u8>::new();
let mut captures = self.regex.captures_iter(content);

self.regex.split(content).for_each(|sur_text| {
use regex::bytes::Replacer;

v.extend(sur_text);
if let Some(capture) = captures.next() {
v.extend_from_slice(
ansi_term::Color::Green.prefix().to_string().as_bytes(),
);
if self.is_literal {
regex::bytes::NoExpand(&self.replace_with)
.replace_append(&capture, &mut v);
} else {
(&*self.replace_with).replace_append(&capture, &mut v);
content: &'a [u8],
) -> Cow<'a, [u8]> {
let mut content = Cow::Borrowed(content);

for Pair { regex, rep } in self.pairs.iter() {
let rep = rep.as_slice();

let mut v = Vec::<u8>::new();
let mut captures = regex.captures_iter(&content);

for sur_text in regex.split(&content) {
use regex::bytes::Replacer;

v.extend(sur_text);
if let Some(capture) = captures.next() {
v.extend_from_slice(
ansi_term::Color::Green.prefix().to_string().as_bytes(),
);
if self.is_literal {
regex::bytes::NoExpand(&rep)
.replace_append(&capture, &mut v);
} else {
(&*rep).replace_append(&capture, &mut v);
}
v.extend_from_slice(
ansi_term::Color::Green.suffix().to_string().as_bytes(),
);
}
v.extend_from_slice(
ansi_term::Color::Green.suffix().to_string().as_bytes(),
);
}
});
content = Cow::Owned(v);
}

return std::borrow::Cow::Owned(v);
content
}

pub(crate) fn replace_file(&self, path: &Path) -> Result<()> {
Expand All @@ -130,7 +176,7 @@ impl Replacer {

let source = File::open(path)?;
let meta = fs::metadata(path)?;
let mmap_source = unsafe { Mmap::map(&source)? };
let mmap_source = unsafe { Mmap::map(&source) }?;
let replaced = self.replace(&mmap_source);

let target = tempfile::NamedTempFile::new_in(
Expand All @@ -142,7 +188,7 @@ impl Replacer {
file.set_permissions(meta.permissions())?;

if !replaced.is_empty() {
let mut mmap_target = unsafe { MmapMut::map_mut(file)? };
let mut mmap_target = unsafe { MmapMut::map_mut(file) }?;
mmap_target.deref_mut().write_all(&replaced)?;
mmap_target.flush_async()?;
}
Expand Down Expand Up @@ -173,6 +219,7 @@ mod tests {
literal,
flags.map(ToOwned::to_owned),
None,
vec![],
)
.unwrap();
assert_eq!(
Expand Down Expand Up @@ -216,4 +263,22 @@ mod tests {
fn full_word_replace() {
replace("abc", "def", false, Some("w"), "abcd abc", "abcd def");
}

#[test]
fn test_multipattern() {
let replacer = Replacer::new(
"foo".to_owned(),
"bar".to_owned(),
false,
None,
None,
vec!["qux".into(), "quux".into(), "bing".into(), "bong".into()],
)
.unwrap();

assert_eq!(
std::str::from_utf8(&replacer.replace("foo qux bing".as_bytes())),
Ok("bar quux bong")
);
}
}