use shuck_ast::Span;
use crate::{Checker, Diagnostic, Edit, Fix, FixAvailability, Rule, ShellDialect, Violation};
pub struct XargsWithInlineReplace;
impl Violation for XargsWithInlineReplace {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Always;
fn rule() -> Rule {
Rule::XargsWithInlineReplace
}
fn message(&self) -> String {
"replace deprecated `xargs -i` with `xargs -I{}`".to_owned()
}
fn fix_title(&self) -> Option<String> {
Some("rewrite `xargs -i` as `xargs -I`".to_owned())
}
}
pub fn xargs_with_inline_replace(checker: &mut Checker) {
if !matches!(
checker.shell(),
ShellDialect::Sh | ShellDialect::Bash | ShellDialect::Dash | ShellDialect::Ksh
) {
return;
}
let source = checker.source();
let diagnostics = checker
.facts()
.commands()
.iter()
.filter_map(|fact| fact.options().xargs())
.flat_map(|xargs| {
xargs
.inline_replace_options()
.iter()
.copied()
.filter(move |option| {
!option.uses_default_replacement()
|| !xargs.has_sc2267_default_replace_silent_shape()
})
.filter_map(|option| xargs_inline_replace_fix(option.span(), source))
})
.map(|(span, fix)| Diagnostic::new(XargsWithInlineReplace, span).with_fix(fix))
.collect::<Vec<_>>();
for diagnostic in diagnostics {
checker.report_diagnostic_dedup(diagnostic);
}
}
fn xargs_inline_replace_fix(span: Span, source: &str) -> Option<(Span, Fix)> {
let text = span.slice(source);
let index = text.find('i')?;
let mut replacement = String::with_capacity(text.len() + 2);
replacement.push_str(&text[..index]);
replacement.push('I');
replacement.push_str(&text[index + 1..]);
if index + 1 == text.len() {
replacement.push_str("{}");
}
Some((span, Fix::safe_edit(Edit::replacement(replacement, span))))
}
#[cfg(test)]
mod tests {
use crate::test::{test_snippet, test_snippet_with_fix};
use crate::{Applicability, LinterSettings, Rule};
#[test]
fn reports_inline_replace_xargs_flags() {
let source = "\
#!/bin/sh
find . -type d -name CVS | xargs -iX rm -rf X
find . -type d -name CVS | xargs -0iX rm -rf X
find . -type d -name CVS | xargs -i{} rm -rf '{}'
command xargs -i echo {}
sudo xargs -i echo {}
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::XargsWithInlineReplace),
);
assert_eq!(
diagnostics
.iter()
.map(|diagnostic| diagnostic.span.slice(source))
.collect::<Vec<_>>(),
vec!["-iX", "-0iX", "-i{}", "-i", "-i"]
);
}
#[test]
fn follows_shellcheck_silent_wrapper_cases() {
let source = "\
#!/bin/sh
find . -type f | xargs -i bash -c 'echo {}'
find . -type f | xargs -0i sh -c 'echo {}'
find . -type f | xargs -i /bin/sh -c 'echo {}'
find . -type f | xargs -i command sh -c 'echo {}'
xargs -i echo '-----> Configuring {}'
xargs -0i echo '-----> Configuring {}'
xargs -i echo \"-----> Configuring {} with $template\"
xargs -i /bin/echo '-----> Configuring {}'
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::XargsWithInlineReplace),
);
assert!(diagnostics.is_empty());
}
#[test]
fn reports_default_replace_outside_silent_sc2267_shapes() {
let source = "\
#!/bin/sh
xargs -i env sh -c 'echo {}'
xargs -i sudo sh -c 'echo {}'
xargs -i sh -ec 'echo {}'
xargs -i echo '{}'
xargs -i echo -n '-x {}'
xargs -i echo -- '-x {}'
xargs -i command echo '-----> Configuring {}'
xargs -i printf -- '-x %s\n' '{}'
xargs -i{} sh -c 'echo {}'
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::XargsWithInlineReplace),
);
assert_eq!(
diagnostics
.iter()
.map(|diagnostic| diagnostic.span.slice(source))
.collect::<Vec<_>>(),
vec!["-i", "-i", "-i", "-i", "-i", "-i", "-i", "-i", "-i{}"]
);
}
#[test]
fn ignores_modern_xargs_replace_flags() {
let source = "\
#!/bin/sh
find . -type d -name CVS | xargs -I{} rm -rf {}
find . -type d -name CVS | xargs --replace rm -rf {}
find . -type d -name CVS | xargs --replace={} rm -rf '{}'
find . -type d -name CVS | xargs -0 rm -rf
find . -type d -name CVS | xargs --null rm -rf
";
let diagnostics = test_snippet(
source,
&LinterSettings::for_rule(Rule::XargsWithInlineReplace),
);
assert!(diagnostics.is_empty());
}
#[test]
fn applies_safe_fix_to_inline_replace_options() {
let source = "#!/bin/sh\nxargs -i echo {}\nxargs -0iX rm -rf X\n";
let result = test_snippet_with_fix(
source,
&LinterSettings::for_rule(Rule::XargsWithInlineReplace),
Applicability::Safe,
);
assert_eq!(result.fixes_applied, 2);
assert_eq!(
result.fixed_source,
"#!/bin/sh\nxargs -I{} echo {}\nxargs -0IX rm -rf X\n"
);
assert!(result.fixed_diagnostics.is_empty());
}
}