Skip to content

Commit 4f77cfe

Browse files
Rework the replacements flag (#267)
* Add replacements tests * Honor `-n` when using `--preview` * Rework CLI for replacements flag * Remove dead code * Remove lingering TODO
1 parent 79f5de0 commit 4f77cfe

10 files changed

Lines changed: 157 additions & 41 deletions

File tree

gen/completions/_sd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ _sd() {
1515

1616
local context curcontext="$curcontext" state line
1717
_arguments "${_arguments_options[@]}" \
18-
'-n+[Limit the number of replacements]:REPLACEMENTS: ' \
18+
'-n+[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \
19+
'--max-replacements=[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \
1920
'-f+[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
2021
'--flags=[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
2122
'-p[Display changes in a human reviewable format (the specifics of the format are likely to change in the future)]' \

gen/completions/_sd.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Register-ArgumentCompleter -Native -CommandName 'sd' -ScriptBlock {
2121

2222
$completions = @(switch ($command) {
2323
'sd' {
24-
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Limit the number of replacements')
24+
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements')
25+
[CompletionResult]::new('--max-replacements', 'max-replacements', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements')
2526
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
2627
[CompletionResult]::new('--flags', 'flags', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
2728
[CompletionResult]::new('-p', 'p', [CompletionResultType]::ParameterName, 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)')

gen/completions/sd.bash

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ _sd() {
1919

2020
case "${cmd}" in
2121
sd)
22-
opts="-p -F -n -f -h -V --preview --fixed-strings --flags --help --version <FIND> <REPLACE_WITH> [FILES]..."
22+
opts="-p -F -n -f -h -V --preview --fixed-strings --max-replacements --flags --help --version <FIND> <REPLACE_WITH> [FILES]..."
2323
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
2424
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
2525
return 0
2626
fi
2727
case "${prev}" in
28+
--max-replacements)
29+
COMPREPLY=($(compgen -f "${cur}"))
30+
return 0
31+
;;
2832
-n)
2933
COMPREPLY=($(compgen -f "${cur}"))
3034
return 0

gen/completions/sd.elv

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ set edit:completion:arg-completer[sd] = {|@words|
1818
}
1919
var completions = [
2020
&'sd'= {
21-
cand -n 'Limit the number of replacements'
21+
cand -n 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements'
22+
cand --max-replacements 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements'
2223
cand -f 'Regex flags. May be combined (like `-f mc`).'
2324
cand --flags 'Regex flags. May be combined (like `-f mc`).'
2425
cand -p 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)'

gen/completions/sd.fish

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
complete -c sd -s n -d 'Limit the number of replacements' -r
1+
complete -c sd -s n -l max-replacements -d 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements' -r
22
complete -c sd -s f -l flags -d 'Regex flags. May be combined (like `-f mc`).' -r
33
complete -c sd -s p -l preview -d 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)'
44
complete -c sd -s F -l fixed-strings -d 'Treat FIND and REPLACE_WITH args as literal strings'

gen/sd.1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ sd
88
.ie \n(.g .ds Aq \(aq
99
.el .ds Aq '
1010
.SH SYNOPSIS
11-
\fBsd\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-F\fR|\fB\-\-fixed\-strings\fR] [\fB\-n \fR] [\fB\-f\fR|\fB\-\-flags\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIFIND\fR> <\fIREPLACE_WITH\fR> [\fIFILES\fR]
11+
\fBsd\fR [\fB\-p\fR|\fB\-\-preview\fR] [\fB\-F\fR|\fB\-\-fixed\-strings\fR] [\fB\-n\fR|\fB\-\-max\-replacements\fR] [\fB\-f\fR|\fB\-\-flags\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] <\fIFIND\fR> <\fIREPLACE_WITH\fR> [\fIFILES\fR]
1212
.ie \n(.g .ds Aq \(aq
1313
.el .ds Aq '
1414
.SH DESCRIPTION
@@ -22,8 +22,8 @@ Display changes in a human reviewable format (the specifics of the format are li
2222
\fB\-F\fR, \fB\-\-fixed\-strings\fR
2323
Treat FIND and REPLACE_WITH args as literal strings
2424
.TP
25-
\fB\-n\fR=\fIREPLACEMENTS\fR
26-
Limit the number of replacements
25+
\fB\-n\fR, \fB\-\-max\-replacements\fR=\fILIMIT\fR [default: 0]
26+
Limit the number of replacements that can occur per file. 0 indicates unlimited replacements
2727
.TP
2828
\fB\-f\fR, \fB\-\-flags\fR=\fIFLAGS\fR
2929
Regex flags. May be combined (like `\-f mc`).

src/cli.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ pub struct Options {
2929
/// Treat FIND and REPLACE_WITH args as literal strings
3030
pub literal_mode: bool,
3131

32-
#[arg(short = 'n')]
33-
/// Limit the number of replacements
34-
pub replacements: Option<usize>,
32+
#[arg(
33+
short = 'n',
34+
long = "max-replacements",
35+
value_name = "LIMIT",
36+
default_value_t
37+
)]
38+
/// Limit the number of replacements that can occur per file. 0 indicates
39+
/// unlimited replacements.
40+
pub replacements: usize,
3541

3642
#[arg(short, long, verbatim_doc_comment)]
3743
#[rustfmt::skip]

src/replacer/mod.rs

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{fs, fs::File, io::prelude::*, path::Path};
1+
use std::{borrow::Cow, fs, fs::File, io::prelude::*, path::Path};
22

33
use crate::{utils, Error, Result};
44

@@ -23,7 +23,7 @@ impl Replacer {
2323
replace_with: String,
2424
is_literal: bool,
2525
flags: Option<String>,
26-
replacements: Option<usize>,
26+
replacements: usize,
2727
) -> Result<Self> {
2828
let (look_for, replace_with) = if is_literal {
2929
(regex::escape(&look_for), replace_with.into_bytes())
@@ -70,7 +70,7 @@ impl Replacer {
7070
regex: regex.build()?,
7171
replace_with,
7272
is_literal,
73-
replacements: replacements.unwrap_or(0),
73+
replacements,
7474
})
7575
}
7676

@@ -88,46 +88,92 @@ impl Replacer {
8888
&'a self,
8989
content: &'a [u8],
9090
) -> std::borrow::Cow<'a, [u8]> {
91+
let regex = &self.regex;
92+
let limit = self.replacements;
93+
let use_color = false;
9194
if self.is_literal {
92-
self.regex.replacen(
95+
Self::replacen(
96+
regex,
97+
limit,
9398
content,
94-
self.replacements,
99+
use_color,
95100
regex::bytes::NoExpand(&self.replace_with),
96101
)
97102
} else {
98-
self.regex
99-
.replacen(content, self.replacements, &*self.replace_with)
103+
Self::replacen(
104+
regex,
105+
limit,
106+
content,
107+
use_color,
108+
&*self.replace_with,
109+
)
100110
}
101111
}
102112

103-
pub(crate) fn replace_preview<'a>(
104-
&'a self,
105-
content: &[u8],
106-
) -> std::borrow::Cow<'a, [u8]> {
107-
let mut v = Vec::<u8>::new();
108-
let mut captures = self.regex.captures_iter(content);
109-
110-
self.regex.split(content).for_each(|sur_text| {
111-
use regex::bytes::Replacer;
112-
113-
v.extend(sur_text);
114-
if let Some(capture) = captures.next() {
115-
v.extend_from_slice(
113+
/// A modified form of [`regex::bytes::Regex::replacen`] that supports
114+
/// coloring replacements
115+
pub(crate) fn replacen<'haystack, R: regex::bytes::Replacer>(
116+
regex: &regex::bytes::Regex,
117+
limit: usize,
118+
haystack: &'haystack [u8],
119+
use_color: bool,
120+
mut rep: R,
121+
) -> Cow<'haystack, [u8]> {
122+
let mut it = regex.captures_iter(haystack).enumerate().peekable();
123+
if it.peek().is_none() {
124+
return Cow::Borrowed(haystack);
125+
}
126+
let mut new = Vec::with_capacity(haystack.len());
127+
let mut last_match = 0;
128+
for (i, cap) in it {
129+
// unwrap on 0 is OK because captures only reports matches
130+
let m = cap.get(0).unwrap();
131+
new.extend_from_slice(&haystack[last_match..m.start()]);
132+
if use_color {
133+
new.extend_from_slice(
116134
ansi_term::Color::Green.prefix().to_string().as_bytes(),
117135
);
118-
if self.is_literal {
119-
regex::bytes::NoExpand(&self.replace_with)
120-
.replace_append(&capture, &mut v);
121-
} else {
122-
(&*self.replace_with).replace_append(&capture, &mut v);
123-
}
124-
v.extend_from_slice(
136+
}
137+
rep.replace_append(&cap, &mut new);
138+
if use_color {
139+
new.extend_from_slice(
125140
ansi_term::Color::Green.suffix().to_string().as_bytes(),
126141
);
127142
}
128-
});
143+
last_match = m.end();
144+
if limit > 0 && i >= limit - 1 {
145+
break;
146+
}
147+
}
148+
new.extend_from_slice(&haystack[last_match..]);
149+
Cow::Owned(new)
150+
}
129151

130-
return std::borrow::Cow::Owned(v);
152+
pub(crate) fn replace_preview<'a>(
153+
&self,
154+
content: &'a [u8],
155+
) -> std::borrow::Cow<'a, [u8]> {
156+
let regex = &self.regex;
157+
let limit = self.replacements;
158+
// TODO: refine this condition more
159+
let use_color = true;
160+
if self.is_literal {
161+
Self::replacen(
162+
regex,
163+
limit,
164+
content,
165+
use_color,
166+
regex::bytes::NoExpand(&self.replace_with),
167+
)
168+
} else {
169+
Self::replacen(
170+
regex,
171+
limit,
172+
content,
173+
use_color,
174+
&*self.replace_with,
175+
)
176+
}
131177
}
132178

133179
pub(crate) fn replace_file(&self, path: &Path) -> Result<()> {

src/replacer/tests.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ fn replace(
2929
src: &'static str,
3030
target: &'static str,
3131
) {
32+
const UNLIMITED_REPLACEMENTS: usize = 0;
3233
let replacer = Replacer::new(
3334
look_for.into(),
3435
replace_with.into(),
3536
literal,
3637
flags.map(ToOwned::to_owned),
37-
None,
38+
UNLIMITED_REPLACEMENTS,
3839
)
3940
.unwrap();
4041
assert_eq!(

tests/cli.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,60 @@ mod cli {
194194
<b>^^^^</b>
195195
"###);
196196
}
197+
198+
#[test]
199+
fn limit_replacements_file() -> Result<()> {
200+
let mut file = tempfile::NamedTempFile::new()?;
201+
file.write_all(b"foo\nfoo\nfoo")?;
202+
let path = file.into_temp_path();
203+
204+
sd().args(["-n", "1", "foo", "bar", path.to_str().unwrap()])
205+
.assert()
206+
.success();
207+
assert_file(&path, "bar\nfoo\nfoo");
208+
209+
Ok(())
210+
}
211+
212+
#[test]
213+
fn limit_replacements_file_preview() -> Result<()> {
214+
let mut file = tempfile::NamedTempFile::new()?;
215+
file.write_all(b"foo\nfoo\nfoo")?;
216+
let path = file.into_temp_path();
217+
218+
sd().args([
219+
"--preview",
220+
"-n",
221+
"1",
222+
"foo",
223+
"bar",
224+
path.to_str().unwrap(),
225+
])
226+
.assert()
227+
.success()
228+
.stdout(format!(
229+
"{}\nfoo\nfoo\n",
230+
ansi_term::Color::Green.paint("bar")
231+
));
232+
233+
Ok(())
234+
}
235+
236+
#[test]
237+
fn limit_replacements_stdin() {
238+
sd().args(["-n", "1", "foo", "bar"])
239+
.write_stdin("foo\nfoo\nfoo")
240+
.assert()
241+
.success()
242+
.stdout("bar\nfoo\nfoo");
243+
}
244+
245+
#[test]
246+
fn limit_replacements_stdin_preview() {
247+
sd().args(["--preview", "-n", "1", "foo", "bar"])
248+
.write_stdin("foo\nfoo\nfoo")
249+
.assert()
250+
.success()
251+
.stdout("bar\nfoo\nfoo");
252+
}
197253
}

0 commit comments

Comments
 (0)