Skip to content
Open
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 src/sed/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ pub struct Substitution {
pub occurrence: usize, // Which occurrence to substitute
pub print_flag: bool, // True if 'p' flag
pub ignore_case: bool, // True if 'I' flag
pub execute: bool, // True if 'e' flag (GNU extension)
pub write_file: Option<Rc<RefCell<NamedWriter>>>, // Writer to file if 'w' flag is used
}

Expand Down
70 changes: 59 additions & 11 deletions src/sed/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,7 @@ fn compile_subst_command(
let mut subst = Box::new(Substitution::default());

subst.replacement = compile_replacement(lines, line)?;
compile_subst_flags(lines, line, &mut subst)?;
compile_subst_flags(lines, line, &mut subst, context.posix, context.sandbox)?;

if pattern.is_empty() && subst.ignore_case {
return compilation_error(
Expand Down Expand Up @@ -865,12 +865,15 @@ pub fn compile_subst_flags(
lines: &ScriptLineProvider,
line: &mut ScriptCharProvider,
subst: &mut Substitution,
posix: bool,
sandbox: bool,
) -> UResult<()> {
let mut seen_g_or_n = false;

subst.occurrence = 1; // default
subst.print_flag = false;
subst.ignore_case = false;
subst.execute = false;
subst.write_file = None;

loop {
Expand Down Expand Up @@ -903,6 +906,18 @@ pub fn compile_subst_flags(
line.advance();
}

'e' => {
if posix || sandbox {
return compilation_error(
lines,
line,
"the 'e' substitute flag is not allowed with --posix or --sandbox",
);
}
subst.execute = true;
line.advance();
}

_c @ '1'..='9' => {
if seen_g_or_n {
return compilation_error(
Expand Down Expand Up @@ -2033,7 +2048,7 @@ mod tests {
let (lines, mut chars) = make_providers("g");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert_eq!(subst.occurrence, 0); // 'g' means all occurrences
}

Expand All @@ -2042,7 +2057,7 @@ mod tests {
let (lines, mut chars) = make_providers("p");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.print_flag);
}

Expand All @@ -2051,7 +2066,7 @@ mod tests {
let (lines, mut chars) = make_providers("I");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.ignore_case);
}

Expand All @@ -2060,7 +2075,7 @@ mod tests {
let (lines, mut chars) = make_providers("i");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.ignore_case);
}

Expand All @@ -2069,7 +2084,7 @@ mod tests {
let (lines, mut chars) = make_providers("3");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert_eq!(subst.occurrence, 3);
}

Expand All @@ -2078,7 +2093,7 @@ mod tests {
let (lines, mut chars) = make_providers("g3");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst).unwrap_err();
let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err();
assert!(
err.to_string()
.contains("multiple 'g' or numeric flags in substitute command")
Expand All @@ -2090,7 +2105,7 @@ mod tests {
let (lines, mut chars) = make_providers("2g");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst).unwrap_err();
let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err();
assert!(
err.to_string()
.contains("multiple 'g' or numeric flags in substitute command")
Expand All @@ -2102,7 +2117,7 @@ mod tests {
let (lines, mut chars) = make_providers("w ");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst).unwrap_err();
let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err();
assert!(err.to_string().contains("missing file path"));
}

Expand All @@ -2113,19 +2128,52 @@ mod tests {
let (lines, mut chars) = make_providers(&format!("w {}", out.display()));
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst).unwrap();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert_eq!(
subst.write_file.as_ref().map(|w| w.borrow().path.clone()),
Some(out)
);
}

#[test]
fn test_compile_subst_flag_e() {
let (lines, mut chars) = make_providers("e");
let mut subst = Substitution::default();

compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.execute);
}

#[test]
fn test_compile_subst_flag_e_rejected_under_posix() {
let (lines, mut chars) = make_providers("e");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst, true, false).unwrap_err();
assert!(
err.to_string()
.contains("not allowed with --posix or --sandbox")
);
}

#[test]
fn test_compile_subst_flag_e_rejected_under_sandbox() {
let (lines, mut chars) = make_providers("e");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, true).unwrap_err();
assert!(
err.to_string()
.contains("not allowed with --posix or --sandbox")
);
}

#[test]
fn test_compile_subst_flag_invalid_flag() {
let (lines, mut chars) = make_providers("z");
let mut subst = Substitution::default();

let err = compile_subst_flags(&lines, &mut chars, &mut subst).unwrap_err();
let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap_err();
assert!(err.to_string().contains("invalid substitute flag"));
}

Expand Down
42 changes: 42 additions & 0 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,26 @@ fn re_or_saved_re<'a>(
}
}

#[cfg(unix)]
fn shell_command(cmd: &str) -> std::process::Command {
let mut c = std::process::Command::new("/bin/sh");
c.arg("-c").arg(cmd);
c
}

#[cfg(windows)]
fn shell_command(cmd: &str) -> std::process::Command {
let mut c = std::process::Command::new("cmd.exe");
c.arg("/C").arg(cmd);
c
}

// Fallback if the target OS is neither Windows nor UNIX-like
#[cfg(not(any(unix, windows)))]
fn shell_command(_cmd: &str) -> std::process::Command {
unimplemented!("the 'e' substitute flag requires a platform shell (/bin/sh or cmd.exe)");
}

/// Perform the specified RE replacement in the provided pattern space.
fn substitute(
pattern: &mut IOChunk,
Expand Down Expand Up @@ -306,6 +326,28 @@ fn substitute(

pattern.set_to_string(result, pattern.is_newline_terminated());

// Execute the pattern space as a shell command if the 'e' flag is set
if sub.execute {
let cmd_str = pattern.as_str()?.to_string();
let output_bytes = shell_command(&cmd_str).output().map_err(|e| {
input_runtime_error::<()>(
&command.location,
context,
format!("failed to execute shell command: {e}"),
)
.unwrap_err()
})?;
let mut shell_out = String::from_utf8_lossy(&output_bytes.stdout).into_owned();
if shell_out.ends_with("\r\n") {
// On windows, both return carriage and newline characters are used
shell_out.truncate(shell_out.len() - 2);
} else if shell_out.ends_with('\n') {
// Strip the trailing newline, as GNU sed does
shell_out.pop();
}
pattern.set_to_string(shell_out, pattern.is_newline_terminated());
}

if sub.print_flag {
write_chunk(output, context, pattern)?;
}
Expand Down
83 changes: 83 additions & 0 deletions tests/by-util/test_sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,89 @@ fn subst_write_file() -> std::io::Result<()> {
Ok(())
}

#[test]
fn test_subst_e_flag_basic() {
new_ucmd!()
.arg("s/.*/echo hi/e")
.pipe_in("a\n")
.succeeds()
.stdout_is("hi\n");
}

#[test]
fn test_subst_e_flag_preserves_unmatched_lines() {
new_ucmd!()
.args(&["-e", "s/^match$/echo replaced/e"])
.pipe_in("no\nmatch\nno\n")
.succeeds()
.stdout_is("no\nreplaced\nno\n");
}

#[test]
fn test_subst_e_flag_strips_trailing_newline() {
// echo produces "hello\n", e flag should strip trailing newline
new_ucmd!()
.arg("s/.*/echo hello/e")
.pipe_in("x\n")
.succeeds()
.stdout_is("hello\n");
}

#[test]
#[cfg(unix)]
fn test_subst_e_flag_multiline_output() {
// Command that produces multiple lines
new_ucmd!()
.arg(r#"s/.*/printf 'a\nb'/e"#)
.pipe_in("x\n")
.succeeds()
.stdout_is("a\nb\n");
}

#[test]
fn test_subst_e_flag_combined_with_g() {
// e flag with other flags
new_ucmd!().arg("s/x/echo y/ge").pipe_in("x\n").succeeds();
}

#[test]
fn test_subst_e_flag_rejected_with_posix() {
// e flag is rejected at compile time if --posix or --sandbox is provided.
new_ucmd!()
.args(&["--posix", "s/.*/echo hi/e"])
.fails()
.stderr_contains("not allowed with --posix or --sandbox");
}

#[test]
fn test_subst_e_flag_rejected_with_sandbox() {
new_ucmd!()
.args(&["--sandbox", "s/.*/echo hi/e"])
.fails()
.stderr_contains("not allowed with --posix or --sandbox");
}

#[test]
fn test_subst_e_flag_command_failure() {
// A non-existent command produces empty output but sed itself succeeds
// (matching GNU sed behavior: the shell runs, the command inside fails)
new_ucmd!()
.arg("s/.*/nonexistent_command/e")
.pipe_in("a\n")
.succeeds()
.stdout_is("\n");
}

#[test]
fn test_subst_e_flag_no_match_no_exec() {
// If substitution doesn't match, command should not execute
new_ucmd!()
.arg("s/nomatch/echo bad/e")
.pipe_in("hello\n")
.succeeds()
.stdout_is("hello\n");
}

////////////////////////////////////////////////////////////
// Transliteration: y
check_output!(trans_simple, ["-e", r"y/0123456789/9876543210/", LINES1]);
Expand Down
Loading