mirror of
https://github.com/chmln/sd
synced 2024-11-22 03:03:03 +00:00
Rework the replacements flag (#267)
* Add replacements tests * Honor `-n` when using `--preview` * Rework CLI for replacements flag * Remove dead code * Remove lingering TODO
This commit is contained in:
parent
79f5de0db7
commit
4f77cfe1b8
10 changed files with 157 additions and 41 deletions
|
@ -15,7 +15,8 @@ _sd() {
|
||||||
|
|
||||||
local context curcontext="$curcontext" state line
|
local context curcontext="$curcontext" state line
|
||||||
_arguments "${_arguments_options[@]}" \
|
_arguments "${_arguments_options[@]}" \
|
||||||
'-n+[Limit the number of replacements]:REPLACEMENTS: ' \
|
'-n+[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \
|
||||||
|
'--max-replacements=[Limit the number of replacements that can occur per file. 0 indicates unlimited replacements]:LIMIT: ' \
|
||||||
'-f+[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
|
'-f+[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
|
||||||
'--flags=[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
|
'--flags=[Regex flags. May be combined (like \`-f mc\`).]:FLAGS: ' \
|
||||||
'-p[Display changes in a human reviewable format (the specifics of the format are likely to change in the future)]' \
|
'-p[Display changes in a human reviewable format (the specifics of the format are likely to change in the future)]' \
|
||||||
|
|
|
@ -21,7 +21,8 @@ Register-ArgumentCompleter -Native -CommandName 'sd' -ScriptBlock {
|
||||||
|
|
||||||
$completions = @(switch ($command) {
|
$completions = @(switch ($command) {
|
||||||
'sd' {
|
'sd' {
|
||||||
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Limit the number of replacements')
|
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements')
|
||||||
|
[CompletionResult]::new('--max-replacements', 'max-replacements', [CompletionResultType]::ParameterName, 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements')
|
||||||
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
|
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
|
||||||
[CompletionResult]::new('--flags', 'flags', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
|
[CompletionResult]::new('--flags', 'flags', [CompletionResultType]::ParameterName, 'Regex flags. May be combined (like `-f mc`).')
|
||||||
[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)')
|
[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)')
|
||||||
|
|
|
@ -19,12 +19,16 @@ _sd() {
|
||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
sd)
|
sd)
|
||||||
opts="-p -F -n -f -h -V --preview --fixed-strings --flags --help --version <FIND> <REPLACE_WITH> [FILES]..."
|
opts="-p -F -n -f -h -V --preview --fixed-strings --max-replacements --flags --help --version <FIND> <REPLACE_WITH> [FILES]..."
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
|
--max-replacements)
|
||||||
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
-n)
|
-n)
|
||||||
COMPREPLY=($(compgen -f "${cur}"))
|
COMPREPLY=($(compgen -f "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -18,7 +18,8 @@ set edit:completion:arg-completer[sd] = {|@words|
|
||||||
}
|
}
|
||||||
var completions = [
|
var completions = [
|
||||||
&'sd'= {
|
&'sd'= {
|
||||||
cand -n 'Limit the number of replacements'
|
cand -n 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements'
|
||||||
|
cand --max-replacements 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements'
|
||||||
cand -f 'Regex flags. May be combined (like `-f mc`).'
|
cand -f 'Regex flags. May be combined (like `-f mc`).'
|
||||||
cand --flags 'Regex flags. May be combined (like `-f mc`).'
|
cand --flags 'Regex flags. May be combined (like `-f mc`).'
|
||||||
cand -p 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)'
|
cand -p 'Display changes in a human reviewable format (the specifics of the format are likely to change in the future)'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
complete -c sd -s n -d 'Limit the number of replacements' -r
|
complete -c sd -s n -l max-replacements -d 'Limit the number of replacements that can occur per file. 0 indicates unlimited replacements' -r
|
||||||
complete -c sd -s f -l flags -d 'Regex flags. May be combined (like `-f mc`).' -r
|
complete -c sd -s f -l flags -d 'Regex flags. May be combined (like `-f mc`).' -r
|
||||||
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)'
|
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)'
|
||||||
complete -c sd -s F -l fixed-strings -d 'Treat FIND and REPLACE_WITH args as literal strings'
|
complete -c sd -s F -l fixed-strings -d 'Treat FIND and REPLACE_WITH args as literal strings'
|
||||||
|
|
6
gen/sd.1
6
gen/sd.1
|
@ -8,7 +8,7 @@ sd
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
\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]
|
\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]
|
||||||
.ie \n(.g .ds Aq \(aq
|
.ie \n(.g .ds Aq \(aq
|
||||||
.el .ds Aq '
|
.el .ds Aq '
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
|
@ -22,8 +22,8 @@ Display changes in a human reviewable format (the specifics of the format are li
|
||||||
\fB\-F\fR, \fB\-\-fixed\-strings\fR
|
\fB\-F\fR, \fB\-\-fixed\-strings\fR
|
||||||
Treat FIND and REPLACE_WITH args as literal strings
|
Treat FIND and REPLACE_WITH args as literal strings
|
||||||
.TP
|
.TP
|
||||||
\fB\-n\fR=\fIREPLACEMENTS\fR
|
\fB\-n\fR, \fB\-\-max\-replacements\fR=\fILIMIT\fR [default: 0]
|
||||||
Limit the number of replacements
|
Limit the number of replacements that can occur per file. 0 indicates unlimited replacements
|
||||||
.TP
|
.TP
|
||||||
\fB\-f\fR, \fB\-\-flags\fR=\fIFLAGS\fR
|
\fB\-f\fR, \fB\-\-flags\fR=\fIFLAGS\fR
|
||||||
Regex flags. May be combined (like `\-f mc`).
|
Regex flags. May be combined (like `\-f mc`).
|
||||||
|
|
12
src/cli.rs
12
src/cli.rs
|
@ -29,9 +29,15 @@ pub struct Options {
|
||||||
/// Treat FIND and REPLACE_WITH args as literal strings
|
/// Treat FIND and REPLACE_WITH args as literal strings
|
||||||
pub literal_mode: bool,
|
pub literal_mode: bool,
|
||||||
|
|
||||||
#[arg(short = 'n')]
|
#[arg(
|
||||||
/// Limit the number of replacements
|
short = 'n',
|
||||||
pub replacements: Option<usize>,
|
long = "max-replacements",
|
||||||
|
value_name = "LIMIT",
|
||||||
|
default_value_t
|
||||||
|
)]
|
||||||
|
/// Limit the number of replacements that can occur per file. 0 indicates
|
||||||
|
/// unlimited replacements.
|
||||||
|
pub replacements: usize,
|
||||||
|
|
||||||
#[arg(short, long, verbatim_doc_comment)]
|
#[arg(short, long, verbatim_doc_comment)]
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{fs, fs::File, io::prelude::*, path::Path};
|
use std::{borrow::Cow, fs, fs::File, io::prelude::*, path::Path};
|
||||||
|
|
||||||
use crate::{utils, Error, Result};
|
use crate::{utils, Error, Result};
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ impl Replacer {
|
||||||
replace_with: String,
|
replace_with: String,
|
||||||
is_literal: bool,
|
is_literal: bool,
|
||||||
flags: Option<String>,
|
flags: Option<String>,
|
||||||
replacements: Option<usize>,
|
replacements: usize,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (look_for, replace_with) = if is_literal {
|
let (look_for, replace_with) = if is_literal {
|
||||||
(regex::escape(&look_for), replace_with.into_bytes())
|
(regex::escape(&look_for), replace_with.into_bytes())
|
||||||
|
@ -70,7 +70,7 @@ impl Replacer {
|
||||||
regex: regex.build()?,
|
regex: regex.build()?,
|
||||||
replace_with,
|
replace_with,
|
||||||
is_literal,
|
is_literal,
|
||||||
replacements: replacements.unwrap_or(0),
|
replacements,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,46 +88,92 @@ impl Replacer {
|
||||||
&'a self,
|
&'a self,
|
||||||
content: &'a [u8],
|
content: &'a [u8],
|
||||||
) -> std::borrow::Cow<'a, [u8]> {
|
) -> std::borrow::Cow<'a, [u8]> {
|
||||||
|
let regex = &self.regex;
|
||||||
|
let limit = self.replacements;
|
||||||
|
let use_color = false;
|
||||||
if self.is_literal {
|
if self.is_literal {
|
||||||
self.regex.replacen(
|
Self::replacen(
|
||||||
|
regex,
|
||||||
|
limit,
|
||||||
content,
|
content,
|
||||||
self.replacements,
|
use_color,
|
||||||
regex::bytes::NoExpand(&self.replace_with),
|
regex::bytes::NoExpand(&self.replace_with),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
self.regex
|
Self::replacen(
|
||||||
.replacen(content, self.replacements, &*self.replace_with)
|
regex,
|
||||||
|
limit,
|
||||||
|
content,
|
||||||
|
use_color,
|
||||||
|
&*self.replace_with,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_preview<'a>(
|
/// A modified form of [`regex::bytes::Regex::replacen`] that supports
|
||||||
&'a self,
|
/// coloring replacements
|
||||||
content: &[u8],
|
pub(crate) fn replacen<'haystack, R: regex::bytes::Replacer>(
|
||||||
) -> std::borrow::Cow<'a, [u8]> {
|
regex: ®ex::bytes::Regex,
|
||||||
let mut v = Vec::<u8>::new();
|
limit: usize,
|
||||||
let mut captures = self.regex.captures_iter(content);
|
haystack: &'haystack [u8],
|
||||||
|
use_color: bool,
|
||||||
self.regex.split(content).for_each(|sur_text| {
|
mut rep: R,
|
||||||
use regex::bytes::Replacer;
|
) -> Cow<'haystack, [u8]> {
|
||||||
|
let mut it = regex.captures_iter(haystack).enumerate().peekable();
|
||||||
v.extend(sur_text);
|
if it.peek().is_none() {
|
||||||
if let Some(capture) = captures.next() {
|
return Cow::Borrowed(haystack);
|
||||||
v.extend_from_slice(
|
}
|
||||||
|
let mut new = Vec::with_capacity(haystack.len());
|
||||||
|
let mut last_match = 0;
|
||||||
|
for (i, cap) in it {
|
||||||
|
// unwrap on 0 is OK because captures only reports matches
|
||||||
|
let m = cap.get(0).unwrap();
|
||||||
|
new.extend_from_slice(&haystack[last_match..m.start()]);
|
||||||
|
if use_color {
|
||||||
|
new.extend_from_slice(
|
||||||
ansi_term::Color::Green.prefix().to_string().as_bytes(),
|
ansi_term::Color::Green.prefix().to_string().as_bytes(),
|
||||||
);
|
);
|
||||||
if self.is_literal {
|
}
|
||||||
regex::bytes::NoExpand(&self.replace_with)
|
rep.replace_append(&cap, &mut new);
|
||||||
.replace_append(&capture, &mut v);
|
if use_color {
|
||||||
} else {
|
new.extend_from_slice(
|
||||||
(&*self.replace_with).replace_append(&capture, &mut v);
|
|
||||||
}
|
|
||||||
v.extend_from_slice(
|
|
||||||
ansi_term::Color::Green.suffix().to_string().as_bytes(),
|
ansi_term::Color::Green.suffix().to_string().as_bytes(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
last_match = m.end();
|
||||||
|
if limit > 0 && i >= limit - 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new.extend_from_slice(&haystack[last_match..]);
|
||||||
|
Cow::Owned(new)
|
||||||
|
}
|
||||||
|
|
||||||
return std::borrow::Cow::Owned(v);
|
pub(crate) fn replace_preview<'a>(
|
||||||
|
&self,
|
||||||
|
content: &'a [u8],
|
||||||
|
) -> std::borrow::Cow<'a, [u8]> {
|
||||||
|
let regex = &self.regex;
|
||||||
|
let limit = self.replacements;
|
||||||
|
// TODO: refine this condition more
|
||||||
|
let use_color = true;
|
||||||
|
if self.is_literal {
|
||||||
|
Self::replacen(
|
||||||
|
regex,
|
||||||
|
limit,
|
||||||
|
content,
|
||||||
|
use_color,
|
||||||
|
regex::bytes::NoExpand(&self.replace_with),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Self::replacen(
|
||||||
|
regex,
|
||||||
|
limit,
|
||||||
|
content,
|
||||||
|
use_color,
|
||||||
|
&*self.replace_with,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_file(&self, path: &Path) -> Result<()> {
|
pub(crate) fn replace_file(&self, path: &Path) -> Result<()> {
|
||||||
|
|
|
@ -29,12 +29,13 @@ fn replace(
|
||||||
src: &'static str,
|
src: &'static str,
|
||||||
target: &'static str,
|
target: &'static str,
|
||||||
) {
|
) {
|
||||||
|
const UNLIMITED_REPLACEMENTS: usize = 0;
|
||||||
let replacer = Replacer::new(
|
let replacer = Replacer::new(
|
||||||
look_for.into(),
|
look_for.into(),
|
||||||
replace_with.into(),
|
replace_with.into(),
|
||||||
literal,
|
literal,
|
||||||
flags.map(ToOwned::to_owned),
|
flags.map(ToOwned::to_owned),
|
||||||
None,
|
UNLIMITED_REPLACEMENTS,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
56
tests/cli.rs
56
tests/cli.rs
|
@ -194,4 +194,60 @@ mod cli {
|
||||||
<b>^^^^</b>
|
<b>^^^^</b>
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn limit_replacements_file() -> Result<()> {
|
||||||
|
let mut file = tempfile::NamedTempFile::new()?;
|
||||||
|
file.write_all(b"foo\nfoo\nfoo")?;
|
||||||
|
let path = file.into_temp_path();
|
||||||
|
|
||||||
|
sd().args(["-n", "1", "foo", "bar", path.to_str().unwrap()])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
assert_file(&path, "bar\nfoo\nfoo");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn limit_replacements_file_preview() -> Result<()> {
|
||||||
|
let mut file = tempfile::NamedTempFile::new()?;
|
||||||
|
file.write_all(b"foo\nfoo\nfoo")?;
|
||||||
|
let path = file.into_temp_path();
|
||||||
|
|
||||||
|
sd().args([
|
||||||
|
"--preview",
|
||||||
|
"-n",
|
||||||
|
"1",
|
||||||
|
"foo",
|
||||||
|
"bar",
|
||||||
|
path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(format!(
|
||||||
|
"{}\nfoo\nfoo\n",
|
||||||
|
ansi_term::Color::Green.paint("bar")
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn limit_replacements_stdin() {
|
||||||
|
sd().args(["-n", "1", "foo", "bar"])
|
||||||
|
.write_stdin("foo\nfoo\nfoo")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("bar\nfoo\nfoo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn limit_replacements_stdin_preview() {
|
||||||
|
sd().args(["--preview", "-n", "1", "foo", "bar"])
|
||||||
|
.write_stdin("foo\nfoo\nfoo")
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stdout("bar\nfoo\nfoo");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue