Add e substitution flag

* feat: introduce e substituion for sed

When the s command is requested for sed, the e substitution flag enables execution of the result of substitution as a shell command. It matches the GNU sed behavior.

On src/sed/command.rs, add 'execute' field to 'Substitution' structure.

On src/sed/compiler.rs, handle 'e' in 'compile_subst_flags' and reject it if --posix or --sandbox flag is used in sed.

On src/sed/processor.rs, spawn /bin/sh with -c flag for a successful match and replace the pattern space with the command's standard output stream.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: comply with rustfmt

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: support for Windows command-line

Instead of calling bourne shell, define a new function: shell_command() with two definitions; conditionally compiling to UNIX-like and Windows. On UNIX-like, executes the bourne shell, whereas command prompt is executed on Windows.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: check newline for Windows

Unix-like system only uses the newline character whereas Windows uses the carriage return as well as newline. Hence, when working on Windows, we need to truncate the last two characters.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* add: 'e' flag tests

Nine new tests have been introduced in regards to new 'e' substitution option. 'test_subst_e_flag_multiline_outuput' is conditionally compiled as printf command is not guaranteed to be present on all Windows systems.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: fallback for shell_command

Executing a shell or a command prompt requires appropriate platforms. WASM does not currently support it.

Also fix a linting issue.

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

* chore: linting cleanup

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>

---------

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>
This commit is contained in:
Pranav Ram Joshi authored and GitHub committed 2026-07-01 11:18:02 +03:00
1 parent cd3b10db48
commit b14272c1d7
4 files changed
+185 -11

No files matched your search

+1
View File
@@ -212,6 +212,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
}
+59 -11
View File
@@ -808,7 +808,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(
@@ -881,12 +881,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 {
@@ -919,6 +922,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(
@@ -2128,7 +2143,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
}
@@ -2137,7 +2152,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);
}
@@ -2146,7 +2161,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);
}
@@ -2155,7 +2170,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);
}
@@ -2164,7 +2179,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);
}
@@ -2173,7 +2188,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")
@@ -2185,7 +2200,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")
@@ -2197,7 +2212,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"));
}
@@ -2208,19 +2223,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"));
}
+42
View File
@@ -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,
@@ -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)?;
}
+83
View File
@@ -549,6 +549,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]);