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:
1 parent
cd3b10db48
commit
b14272c1d7
4 files changed
+185
-11
No files matched your search
@@ -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
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in new issue
Block a user