Skip to content
103 changes: 103 additions & 0 deletions src/sed/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,104 @@ fn compile_text_command_posix(
Ok(CommandHandling::Continue)
}

// Handles e
// With no argument, the command executes the pattern space as a shell
// command at runtime. With an argument, the rest of the line is the
// command to run, following the same escape and backslash-newline
// continuation rules as the GNU a/c/i text argument.
fn compile_execute_command(
lines: &mut ScriptLineProvider,
line: &mut ScriptCharProvider,
cmd: &mut Command,
context: &mut ProcessingContext,
) -> UResult<CommandHandling> {
if context.posix || context.sandbox {
return compilation_error(
lines,
line,
"the 'e' command is not allowed with --posix or --sandbox",
);
}

line.advance(); // Skip the command character.
line.eat_spaces(); // Skip any leading whitespace.

if line.eol() {
// No argument: execute the pattern space itself at runtime.
cmd.data = CommandData::None;
return Ok(CommandHandling::Continue);
}

// True after a \ at the end of a line
let mut escaped_newline = false;

// Skip optional \
if line.current() == '\\' {
line.advance();
escaped_newline = line.eol();
}

// Gather the command text. Stop on a non-escaped newline. Unlike most
// other commands, ';' does not terminate the argument. The rest of the
// (possibly continued) line is consumed unconditionally.
let mut text = String::new();
// True once a continuation line has actually been pulled in. A dangling
// leading backslash with no line to continue into is treated as no
// argument at all, matching GNU sed. However once a continuation succeeds,
// even into an empty line, we're committed to producing a (possibly empty)
// Text argument from then on.
let mut continued = false;
'text_content: loop {
if escaped_newline {
match lines.next_line()? {
None => {
break 'text_content;
}
Some(line_string) => {
*line = ScriptCharProvider::new(&line_string);
continued = true;
}
}
escaped_newline = false;
}

// Non-escaped newline
if line.eol() {
break 'text_content;
}

if line.current() == '\\' {
line.advance();

if line.eol() {
escaped_newline = true;
text.push('\n');
continue 'text_content;
}

if let Some(decoded) = parse_char_escape(line) {
text.push(decoded);
} else {
// Invalid escapes result in the escaped character.
text.push(line.current());
line.advance();
}
} else {
text.push(line.current());
line.advance();
}
}

cmd.data = if text.is_empty() && !continued {
// A dangling leading backslash with nothing left to continue into.
// Treat this the same as no argument at all.
CommandData::None
} else {
CommandData::Text(Rc::from(text))
};
Ok(CommandHandling::Continue)
}

// Return the specification for the command letter at the current line position
// checking for diverse errors.
fn get_verified_cmd_spec(
Expand Down Expand Up @@ -1422,6 +1520,11 @@ fn get_cmd_spec(
n_addr: 1,
handler: compile_number_command,
}),
// e is a GNU extension
'e' => Ok(CommandSpec {
n_addr: 2,
handler: compile_execute_command,
}),
'r' => Ok(CommandSpec {
n_addr: if posix { 1 } else { 2 },
handler: compile_read_file_command,
Expand Down
2 changes: 1 addition & 1 deletion src/sed/delimited_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ pub fn parse_char_escape(line: &mut ScriptCharProvider) -> Option<char> {
}

'U' => {
// Short Unicode escape \UXXXXXXXX (exactly eight heax digits)
// Short Unicode escape \UXXXXXXXX (exactly eight hex digits)
line.advance(); // move past 'x'
match parse_numeric_escape(line, |c| c.is_ascii_hexdigit(), 8, 16) {
Some(decoded) => Some(decoded),
Expand Down
100 changes: 81 additions & 19 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::sed::named_writer;

use std::borrow::Cow;
use std::cell::RefCell;
use std::io::{self, IsTerminal};
use std::io::{self, IsTerminal, Read};
use std::path::PathBuf;
use std::rc::Rc;
use uucore::display::Quotable;
Expand Down Expand Up @@ -215,6 +215,60 @@ fn shell_command(_cmd: &str) -> std::process::Command {
unimplemented!("the 'e' substitute flag requires a platform shell (/bin/sh or cmd.exe)");
}

/// Run `cmd` in a shell, returning its standard output. The child's
/// standard error is left connected to this process's own, matching GNU
/// sed's behavior of letting shell errors surface directly rather than
/// being silently captured.
fn shell_stdout(cmd: &str) -> io::Result<Vec<u8>> {
let mut child = shell_command(cmd)
.stdout(std::process::Stdio::piped())
.spawn()?;
let mut stdout = child.stdout.take().expect("stdout should be piped");
let mut buf = Vec::new();
stdout.read_to_end(&mut buf)?;
child.wait()?;
Ok(buf)
}

/// Execute the pattern space as a shell command, replacing its contents
/// with the command's standard output, minus one trailing newline.
fn execute_pattern_as_shell_command(
pattern: &mut IOChunk,
command: &Command,
context: &mut ProcessingContext,
) -> UResult<()> {
let cmd_str = pattern.as_str()?.to_string();
let stdout_bytes = shell_stdout(&cmd_str).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(stdout_bytes).map_err(|e| {
input_runtime_error::<()>(
&command.location,
context,
format!("shell command output is not valid UTF-8: {e}"),
)
.unwrap_err()
})?;
#[cfg(windows)]
{
if shell_out.ends_with("\r\n") {
// On Windows a trailing \r\n is the line terminator; strip both.
shell_out.truncate(shell_out.len() - 2);
}
}
// Unix (and some Windows tool) has the trailing \n. Strip it, as GNU sed does.
if shell_out.ends_with('\n') {
shell_out.pop();
}
pattern.set_to_string(shell_out, pattern.is_newline_terminated());
Ok(())
}

/// Perform the specified RE replacement in the provided pattern space.
fn substitute(
pattern: &mut IOChunk,
Expand Down Expand Up @@ -328,24 +382,7 @@ fn substitute(

// 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());
execute_pattern_as_shell_command(pattern, command, context)?;
}

if sub.print_flag {
Expand Down Expand Up @@ -581,6 +618,31 @@ fn process_file(
pattern.clear();
break;
}
'e' => match &command.data {
CommandData::None => {
execute_pattern_as_shell_command(&mut pattern, &command, context)?;
}
CommandData::Text(cmd_str) => {
let stdout_bytes = shell_stdout(cmd_str).map_err(|e| {
input_runtime_error::<()>(
&command.location,
context,
format!("failed to execute shell command: {e}"),
)
.unwrap_err()
})?;
let shell_out = String::from_utf8(stdout_bytes).map_err(|e| {
input_runtime_error::<()>(
&command.location,
context,
format!("shell command output is not valid UTF-8: {e}"),
)
.unwrap_err()
})?;
output.write_str(shell_out)?;
}
_ => panic!("invalid 'e' command data"),
},
'g' => {
// Replace pattern with the contents of the hold space.
pattern.set_to_string(context.hold.content.clone(), context.hold.has_newline);
Expand Down
Loading
Loading