Author SHA1 Message Date
copilot-swe-agent[bot] 4ec214b40e Fix UB in IOChunk::as_str(): only cache utf8_verified on successful UTF-8 validation 2026-07-07 12:41:33 +00:00
copilot-swe-agent[bot] a18cd3fff7 Initial plan 2026-07-07 12:37:21 +00:00
renovate[bot] 30607c4ddc chore(deps): update rust crate ctor to v1.0.8 2026-07-06 08:07:50 +02:00
Diomidis Spinellis 01c8412327 Optimize multiline matching (#483)
The multiline matching resulted in a large performance regression due to
the indiscriminate use of the dotall (?s) flag.  Change the regex
selection process to use only the flag when probably needed.
2026-07-05 15:13:53 +02:00
Diomidis SpinellisandMukunda Katta f3a79c5847 Support multiline substitution flag (#482)
While at it fix . matching of embedded newlines.

This commit increases GNU passed tests by 2.

Co-authored-by: Mukunda Katta <mukunda.vjcs6@gmail.com>
2026-07-05 10:48:39 +02:00
Diomidis Spinellis a9e5fe778c Add sandbox checks (#477)
Following the introduction of the first sandbox check in b14272c1
complete the checks for the remaining commands.
2026-07-03 16:55:50 +02:00
renovate[bot] 02d0239447 chore(deps): update rust crate rand to v0.10.2 2026-07-02 16:50:42 +02:00
renovate[bot] 8b5dea703a chore(deps): update rust crate console to v0.16.4 2026-07-02 08:05:54 +02:00
renovate[bot] 34dfbc2ef1 chore(deps): update rust crate clap_complete to v4.6.7 2026-07-02 08:05:18 +02:00
Develandpre-commit-ci[bot] 8cb7090bbc --posix should reject GNU substitute flags i/I #401 (#419)
* reject 'i', 'I', 'm', 'M' and 'E' when --posix flag is set

* add test for issue 401

* formatting

* add a test for 'm' flag in test_posix_reject_flags

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update compiler.rs

* cargo fmt

* add a const variable with the err msg

* Update compiler.rs

* cargo fmt

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-01 19:30:25 +03:00
Mukunda Rao Katta 2a5ab5a9a2 feat: add z command (#428)
* feat: add z command

* test: avoid stdin race in z POSIX rejection
2026-07-01 12:51:11 +03:00
Pranav Ram Joshi b14272c1d7 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>
2026-07-01 11:18:02 +03:00
Diomidis Spinellis cd3b10db48 Document uutils Playground 2026-07-01 10:48:54 +03:00
Diomidis Spinellis 48c2f56358 Document r command extension 2026-07-01 10:42:22 +03:00
Diomidis SpinellisandPranavRJoshi dcd2df848a Implement zero-address r command to insert file before first line (#474)
* feat: support zero-address file insertion

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

* Polish the handling of 0r

- Use common constant for error message.
- Simplify command check at compile and runtime.
- Separate runtime handling into a function.

---------

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>
Co-authored-by: PranavRJoshi <pranavrjoshi1@gmail.com>
2026-07-01 08:36:03 +02:00
renovate[bot] 9efab796be chore(deps): update rust crate clap_complete to v4.6.6 2026-07-01 07:05:51 +02:00
11 changed files with 693 additions and 62 deletions

No files matched your search

Generated
+8 -8
View File
@@ -219,9 +219,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.6.5"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
checksum = "db8b397918185f0161ff3d6fcaa9e4bfc09b8367caf6e1d4a2848e5477ed027b"
dependencies = [
"clap",
]
@@ -450,9 +450,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "1.0.7"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a"
checksum = "fb22e947478ccf9dc44d8922042c677a63fbb88f2cb468521d1145816e5087cb"
dependencies = [
"link-section",
"linktime-proc-macro",
@@ -889,9 +889,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "link-section"
version = "0.18.0"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a63f9687974de7e2caf6c96a1b19c6e882db299fdde09075c5b9e11c58421cd"
checksum = "e333fe507b738576d6da5bb3f1a7d7a1c80307ed9ef31624c057d844c19c93e9"
[[package]]
name = "linktime-proc-macro"
@@ -1162,9 +1162,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.2",
+9 -2
View File
@@ -26,7 +26,7 @@ Further work aims to:
* Implement more GNU extensions,
* Improve performance where possible.
## Installation
## Installation and Use
We provide a Linux x86_64 binary archive from the main branch at
https://github.com/uutils/sed/releases/tag/latest-commit .
@@ -44,6 +44,10 @@ cargo run --release
The binary is named `sed` in `target/release/sed`.
You can also try *sed* on the web
through the [uutils Playground](https://uutils.org//playground/)
by clicking on the `Load sed` button.
## Testing
### GNU sed Compatibility Testing
@@ -81,6 +85,7 @@ cargo test
* Spaces can precede a regular expression modifier.
* `I` can be used in as a synonym for the `i` (case insensitive) substitution
flag.
* `M` and `m` substitution flags allow multi-line matching.
* In addition to `\n`, other escape sequences (octal, hex, C) are supported
in the strings of the `y` command.
Under POSIX these yield undefined behavior.
@@ -92,11 +97,13 @@ cargo test
* A `Q` command (optionally followed by an exit code) quits immediately.
* The `q` command can be optionally followed by an exit code.
* The `l` command can be optionally followed by the output width.
* The `--follow-symlinks` flag for in-place editing.
* The `--follow-symlinks` option for in-place editing.
* The `--sandbox` option that limits potentially destructive commands.
* Address 0 can be used to specify an address range that is already
active on line 1 and can finish with the specified regular expression.
* Address steps can be specified in the form of start~step and start,~step
ranges.
* Address 0 can be used in the `r` command to prepend a file.
### Supported BSD and GNU extensions
* The second address in a range can be specified as a relative address with +N.
+13 -13
View File
@@ -245,9 +245,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "console"
version = "0.16.3"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
dependencies = [
"encode_unicode",
"libc",
@@ -725,9 +725,9 @@ dependencies = [
[[package]]
name = "phf"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
dependencies = [
"phf_shared",
"serde",
@@ -735,9 +735,9 @@ dependencies = [
[[package]]
name = "phf_codegen"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe"
dependencies = [
"phf_generator",
"phf_shared",
@@ -745,9 +745,9 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
dependencies = [
"fastrand",
"phf_shared",
@@ -755,9 +755,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.13.1"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
dependencies = [
"siphasher",
]
@@ -848,9 +848,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.1"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.2",
@@ -1076,7 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys",
+2
View File
@@ -212,6 +212,8 @@ 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 multiline: bool, // True if 'm' or 'M' flag (GNU extension)
pub write_file: Option<Rc<RefCell<NamedWriter>>>, // Writer to file if 'w' flag is used
}
+252 -32
View File
@@ -29,6 +29,12 @@ use uucore::error::{UResult, USimpleError};
const DEFAULT_OUTPUT_WIDTH: usize = 60;
const ERR_ADDRESS_0_USAGE: &str =
"address 0 can only be used with ~step, a second regular expression, or a read command";
const ERR_SANDBOX: &str = "command not allowed with --sandbox";
const ERR_UNKNOWN_OPTION_TO_S: &str = "unknown option to 's'";
// Handling required after processing a command
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CommandHandling {
@@ -380,11 +386,7 @@ fn compile_address_range(
};
if is_line0 && !matches!(addr2, Address::Re(_)) && !is_step_match {
return compilation_error(
lines,
line,
"address 0 can only be used with a regular expression or ~step",
);
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
}
// If needed, transform Address::Line into Address::Step*.
@@ -399,8 +401,14 @@ fn compile_address_range(
}
}
// Zero-address read command check
if is_line0 && n_addr == 1 {
return compilation_error(lines, line, "address 0 requires a second address");
// After retrieval of first address, subsequent spaces
// are consumed unconditionally. By now, the position
// must be in non-whitespace character or EOL.
if line.eol() || line.current() != 'r' {
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
}
}
Ok(n_addr)
@@ -461,7 +469,7 @@ fn compile_address(
}
Ok(Address::Re(compile_regex(
lines, line, &re, context, icase,
lines, line, &re, context, icase, false,
)?))
}
'$' => {
@@ -637,6 +645,7 @@ fn compile_regex(
pattern: &str,
context: &ProcessingContext,
icase: bool,
multiline: bool,
) -> UResult<Option<Regex>> {
if pattern.is_empty() {
return Ok(None);
@@ -648,11 +657,18 @@ fn compile_regex(
} else {
&bre_to_ere(pattern)
};
// Add case-insensitive modifier if needed.
let pattern = if icase {
format!("(?i){pattern}")
} else {
let mut modifiers = String::new();
if icase {
modifiers.push('i');
}
if multiline {
modifiers.push('m');
}
let pattern = if modifiers.is_empty() {
pattern.to_string()
} else {
format!("(?{modifiers}){pattern}")
};
// Compile into engine.
@@ -803,9 +819,9 @@ 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 {
if pattern.is_empty() && (subst.ignore_case || subst.multiline) {
return compilation_error(
lines,
line,
@@ -813,8 +829,15 @@ fn compile_subst_command(
);
}
// Compile regex with now known ignore_case flag.
subst.regex = compile_regex(lines, line, &pattern, context, subst.ignore_case)?;
// Compile regex with now known modifier flags.
subst.regex = compile_regex(
lines,
line,
&pattern,
context,
subst.ignore_case,
subst.multiline,
)?;
// Catch invalid group references at compile time, if possible.
if let Some(regex) = &subst.regex
@@ -876,12 +899,16 @@ 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.multiline = false;
subst.write_file = None;
loop {
@@ -910,10 +937,33 @@ pub fn compile_subst_flags(
}
'i' | 'I' => {
if posix {
return compilation_error(lines, line, ERR_UNKNOWN_OPTION_TO_S);
}
subst.ignore_case = true;
line.advance();
}
'm' | 'M' => {
if posix {
return compilation_error(lines, line, ERR_UNKNOWN_OPTION_TO_S);
}
subst.multiline = true;
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(
@@ -944,6 +994,9 @@ pub fn compile_subst_flags(
}
'w' => {
if sandbox {
return compilation_error(lines, line, ERR_SANDBOX);
}
let location = ScriptLocation::at_position(lines, line);
let path = read_file_path(lines, line)?;
subst.write_file = Some(NamedWriter::new(path, location)?);
@@ -1018,8 +1071,11 @@ fn compile_read_file_command(
lines: &mut ScriptLineProvider,
line: &mut ScriptCharProvider,
cmd: &mut Command,
_context: &mut ProcessingContext,
context: &mut ProcessingContext,
) -> UResult<CommandHandling> {
if context.sandbox {
return compilation_error(lines, line, ERR_SANDBOX);
}
let path = read_file_path(lines, line)?;
cmd.data = CommandData::Path(path);
Ok(CommandHandling::Continue)
@@ -1030,8 +1086,11 @@ fn compile_write_file_command(
lines: &mut ScriptLineProvider,
line: &mut ScriptCharProvider,
cmd: &mut Command,
_context: &mut ProcessingContext,
context: &mut ProcessingContext,
) -> UResult<CommandHandling> {
if context.sandbox {
return compilation_error(lines, line, ERR_SANDBOX);
}
let location = ScriptLocation::at_position(lines, line);
let path = read_file_path(lines, line)?;
cmd.data = CommandData::NamedWriter(NamedWriter::new(path, location)?);
@@ -1346,6 +1405,10 @@ fn get_cmd_spec(
n_addr: 2,
handler: compile_empty_command,
}),
'z' if !posix => Ok(CommandSpec {
n_addr: 2,
handler: compile_empty_command,
}),
'l' => Ok(CommandSpec {
n_addr: 2,
handler: compile_number_command,
@@ -1394,7 +1457,7 @@ mod tests {
fn make_line_provider(lines: &[&str]) -> ScriptLineProvider {
let input = lines
.iter()
.map(|s| ScriptValue::StringVal(s.to_string()))
.map(|s| ScriptValue::StringVal((*s).to_string()))
.collect();
ScriptLineProvider::new(input)
}
@@ -1581,7 +1644,7 @@ mod tests {
#[test]
fn test_compile_re_basic() {
let (lines, chars) = dummy_providers();
let regex = compile_regex(&lines, &chars, "abc", &ctx(), false)
let regex = compile_regex(&lines, &chars, "abc", &ctx(), false, false)
.unwrap()
.expect("regex should be present");
assert!(regex.is_match(&mut IOChunk::new_from_str("abc")).unwrap());
@@ -1593,7 +1656,7 @@ mod tests {
let (lines, chars) = make_providers("acaa\nbbb\nccc");
let mut ctx = ctx();
ctx.regex_extended = true;
let regex = compile_regex(&lines, &chars, "cc{0,}", &ctx, false)
let regex = compile_regex(&lines, &chars, "cc{0,}", &ctx, false, false)
.unwrap()
.expect("regex should be present");
assert!(
@@ -1606,7 +1669,7 @@ mod tests {
#[test]
fn test_compile_re_case_insensitive() {
let (lines, chars) = dummy_providers();
let regex = compile_regex(&lines, &chars, "abc", &ctx(), true)
let regex = compile_regex(&lines, &chars, "abc", &ctx(), true, false)
.unwrap()
.expect("regex should be present");
assert!(regex.is_match(&mut IOChunk::new_from_str("abc")).unwrap());
@@ -1617,10 +1680,36 @@ mod tests {
#[test]
fn test_compile_re_invalid() {
let (lines, chars) = dummy_providers();
let result = compile_regex(&lines, &chars, "a[d", &ctx(), false);
let result = compile_regex(&lines, &chars, "a[d", &ctx(), false, false);
assert!(result.is_err()); // Should fail due to open bracketed expression
}
#[test]
fn test_compile_re_multiline_start() {
let (lines, chars) = dummy_providers();
let regex = compile_regex(&lines, &chars, "^bar", &ctx(), false, true)
.unwrap()
.expect("regex should be present");
assert!(
regex
.is_match(&mut IOChunk::new_from_str("foo\nbar"))
.unwrap()
);
}
#[test]
fn test_compile_re_multiline_end() {
let (lines, chars) = dummy_providers();
let regex = compile_regex(&lines, &chars, "foo$", &ctx(), false, true)
.unwrap()
.expect("regex should be present");
assert!(
regex
.is_match(&mut IOChunk::new_from_str("foo\nbar"))
.unwrap()
);
}
// compile_address
#[test]
fn test_compile_addr_line_number() {
@@ -1849,6 +1938,51 @@ mod tests {
ScriptCharProvider::new("")
}
#[test]
fn test_zero_addr_r_accepted() {
for input in ["0r", "0 r"] {
let (lines, mut chars) = make_providers(input);
let mut cmd = Rc::new(RefCell::new(Command::default()));
let n_addr = compile_address_range(&lines, &mut chars, &mut cmd, &ctx()).unwrap();
assert_eq!(n_addr, 1);
assert!(matches!(cmd.borrow().addr1, Some(Address::Line(0))));
assert_eq!(chars.current(), 'r');
}
}
// Zero-address with no commands
#[test]
fn test_zero_addr_no_commands() {
let (lines, mut chars) = make_providers("0");
let mut cmd = Rc::new(RefCell::new(Command::default()));
let result = compile_address_range(&lines, &mut chars, &mut cmd, &ctx());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains(ERR_ADDRESS_0_USAGE)
);
}
// Zero-address with a command other than 'r' must still be rejected.
#[test]
fn test_zero_addr_non_r_rejected() {
let (lines, mut chars) = make_providers("0p");
let mut cmd = Rc::new(RefCell::new(Command::default()));
let result = compile_address_range(&lines, &mut chars, &mut cmd, &ctx());
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains(ERR_ADDRESS_0_USAGE)
);
}
#[test]
fn test_compile_sequence_empty_input() {
let mut provider = make_line_provider(&[]);
@@ -2078,7 +2212,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
}
@@ -2087,7 +2221,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);
}
@@ -2096,7 +2230,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);
}
@@ -2105,16 +2239,34 @@ 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);
}
#[test]
fn test_compile_subst_flag_uppercase_m() {
let (lines, mut chars) = make_providers("M");
let mut subst = Substitution::default();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.multiline);
}
#[test]
fn test_compile_subst_flag_m_lowercase() {
let (lines, mut chars) = make_providers("m");
let mut subst = Substitution::default();
compile_subst_flags(&lines, &mut chars, &mut subst, false, false).unwrap();
assert!(subst.multiline);
}
#[test]
fn test_compile_subst_flag_number() {
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);
}
@@ -2123,7 +2275,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")
@@ -2135,7 +2287,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")
@@ -2147,7 +2299,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"));
}
@@ -2158,19 +2310,61 @@ 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_w_rejected_under_sandbox() {
let (lines, mut chars) = make_providers("w out.txt");
let mut subst = Substitution::default();
let err = compile_subst_flags(&lines, &mut chars, &mut subst, false, true).unwrap_err();
assert!(err.to_string().contains(ERR_SANDBOX));
}
#[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"));
}
@@ -2431,6 +2625,32 @@ mod tests {
assert_eq!(collect_codes(inner), vec!['x', 'b']);
}
// compile_read_file_command
#[test]
fn test_compile_read_file_command_rejected_under_sandbox() {
let (mut lines, mut chars) = make_providers("r input.txt");
let mut cmd = Command::default();
let mut context = ctx();
context.sandbox = true;
let err =
compile_read_file_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap_err();
assert!(err.to_string().contains(ERR_SANDBOX));
}
// compile_write_file_command
#[test]
fn test_compile_write_file_command_rejected_under_sandbox() {
let (mut lines, mut chars) = make_providers("w out.txt");
let mut cmd = Command::default();
let mut context = ctx();
context.sandbox = true;
let err =
compile_write_file_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap_err();
assert!(err.to_string().contains(ERR_SANDBOX));
}
// compile_label_command
#[test]
fn test_compile_label_command() {
+7 -3
View File
@@ -244,9 +244,13 @@ impl<'a> IOChunk<'a> {
// Use cached result
Ok(unsafe { self.content.as_str_unchecked() })
} else {
let result = str::from_utf8(content);
self.utf8_verified.set(true);
result.map_err(|e| USimpleError::new(2, e.to_string()))
match str::from_utf8(content) {
Ok(s) => {
self.utf8_verified.set(true);
Ok(s)
}
Err(e) => Err(USimpleError::new(2, e.to_string())),
}
}
}
IOChunkContent::Owned { content, .. } => Ok(content),
+54 -2
View File
@@ -209,13 +209,36 @@ pub enum Regex {
Fancy(FancyRegex), // Slowest: RE supporting UTF-8 and back-references
}
/// Ensure that a regex matches GNU sed's default semantics for `.`
/// through the appropriate use of the s flag.
pub fn ensure_dotall(pattern: &str) -> String {
// Add (?s) if no flags present.
if !pattern.starts_with("(?") {
return format!("(?s){pattern}");
}
let Some(close) = pattern.find(')') else {
// Malformed inline flag group.
return pattern.to_owned();
};
// Add s flag to ?(...) unless 's' or its complement 'm' is there.
let flags = &pattern[2..close];
if flags.contains('m') || flags.contains('s') {
pattern.to_owned()
} else {
format!("(?{flags}s){}", &pattern[close + 1..])
}
}
impl Regex {
/// Construct the most efficient RE-like matching engine possible.
pub fn new(pattern: &str) -> Result<Self, Box<dyn Error>> {
if NEEDS_FANCY_RE.is_match(pattern) {
Ok(Self::Fancy(FancyRegex::new(pattern)?))
Ok(Self::Fancy(FancyRegex::new(&ensure_dotall(pattern))?))
} else if NEEDS_RE.is_match(pattern) {
Ok(Self::Byte(ByteRegex::new(pattern)?))
Ok(Self::Byte(ByteRegex::new(&ensure_dotall(pattern))?))
} else {
Ok(Self::Literal(LiteralMatcher::new(&remove_escapes(pattern))))
}
@@ -677,4 +700,33 @@ mod tests {
assert!(matcher.find(haystack).is_none());
assert_eq!(matcher.iter(haystack).count(), 0);
}
#[test]
fn prepends_s_when_no_flag_group() {
assert_eq!(ensure_dotall("abc"), "(?s)abc");
}
#[test]
fn adds_s_when_no_m_or_s() {
assert_eq!(ensure_dotall("(?i)abc"), "(?is)abc");
assert_eq!(ensure_dotall("(?)abc"), "(?s)abc");
}
#[test]
fn leaves_m_unchanged() {
assert_eq!(ensure_dotall("(?m)abc"), "(?m)abc");
assert_eq!(ensure_dotall("(?im)abc"), "(?im)abc");
assert_eq!(ensure_dotall("(?mi)abc"), "(?mi)abc");
}
#[test]
fn leaves_existing_s_unchanged() {
assert_eq!(ensure_dotall("(?s)abc"), "(?s)abc");
assert_eq!(ensure_dotall("(?is)abc"), "(?is)abc");
}
#[test]
fn leaves_malformed_flag_group_unchanged() {
assert_eq!(ensure_dotall("(?iabc"), "(?iabc");
}
}
+79
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)?;
}
@@ -415,6 +457,35 @@ fn list(output: &mut OutputBuffer, line: &IOChunk, max_width: usize) -> UResult<
Ok(())
}
/// Handle address 0 read at the beginning of each file.
fn process_address_0(
commands: Option<Rc<RefCell<Command>>>,
output: &mut OutputBuffer,
) -> UResult<()> {
// Prescan for zero-address which must produce output
// before any input line is read.
{
let mut current = commands;
while let Some(cmd_rc) = current {
let next = {
let cmd = cmd_rc.borrow();
if cmd.code == 'r'
&& matches!(cmd.addr1, Some(Address::Line(0)))
&& cmd.addr2.is_none()
{
let path = extract_variant!(cmd, Path);
output.copy_file(path)?;
}
cmd.next.clone()
};
current = next;
}
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
/// Process a single input file
fn process_file(
@@ -423,6 +494,8 @@ fn process_file(
output: &mut OutputBuffer,
context: &mut ProcessingContext,
) -> UResult<()> {
process_address_0(commands.clone(), output)?;
// Loop over the input lines as pattern space.
'lines: while let Some(mut pattern) = reader.get_line()? {
context.line_number += 1;
@@ -621,6 +694,12 @@ fn process_file(
let trans = extract_variant!(command, Transliteration);
transliterate(&mut pattern, trans)?;
}
'z' => {
// Clear the pattern contents, but preserve newline state
// so automatic printing still emits an empty record.
let (pat_content, _) = pattern.fields_mut()?;
pat_content.clear();
}
':' => {
// Branch target; do nothing.
}
+223 -2
View File
@@ -517,6 +517,42 @@ check_output!(subst_re_reuse, ["-e", r"2s//M/;1s/l/L/", LINES1]);
check_output!(subst_newline_class, ["-n", r"1{;N;s/[\n]/X/;p;}", LINES1]);
check_output!(subst_newline_re, ["-n", r"1{;N;s/\n/X/;p;}", LINES1]);
#[test]
fn subst_dot_matches_newline() {
new_ucmd!()
.arg("N;s/foo.*bar/X/")
.pipe_in("foo\nbar\n")
.succeeds()
.stdout_is("X\n");
}
#[test]
fn subst_multiline_dot_does_not_match_newline() {
new_ucmd!()
.arg("N;s/foo.*bar/X/m")
.pipe_in("foo\nbar\n")
.succeeds()
.stdout_is("foo\nbar\n");
}
#[test]
fn subst_multiline_flag_matches_embedded_line_start() {
new_ucmd!()
.arg("N;s/^./X/gm")
.pipe_in("foo\nbar\n")
.succeeds()
.stdout_is("Xoo\nXar\n");
}
#[test]
fn subst_multiline_flag_matches_embedded_line_end() {
new_ucmd!()
.arg("N;s/.$/X/gM")
.pipe_in("foo\nbar\n")
.succeeds()
.stdout_is("foX\nbaX\n");
}
// Check appropriate selection and behavior of fast_Regex matcher
// Literal matcher
check_output!(subst_literal_start, ["-e", r"s/^l1/L1/", LINES1]);
@@ -549,6 +585,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]);
@@ -557,6 +676,42 @@ check_output!(
["-e", r"y10\123456789198765432\101", LINES1]
);
check_output!(trans_no_new_line, ["-e", r"y/l/L/", NO_NEW_LINE]);
#[test]
fn pattern_clear_with_z_command() {
new_ucmd!()
.arg("z")
.pipe_in("a\nb\n")
.succeeds()
.stdout_is("\n\n");
}
#[test]
fn pattern_clear_with_z_command_silent_print() {
new_ucmd!()
.args(&["-n", "z;p"])
.pipe_in("a\nb\n")
.succeeds()
.stdout_is("\n\n");
}
#[test]
fn pattern_clear_with_z_preserves_substitution_flag() {
new_ucmd!()
.args(&["-n", "s/a/b/;z;t changed;b;:changed;c\\\nchanged"])
.pipe_in("a\n")
.succeeds()
.stdout_is("changed\n");
}
#[test]
fn pattern_clear_with_z_is_non_posix() {
new_ucmd!()
.args(&["--posix", "z"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:1: error: invalid command code `z'\n");
}
check_output!(trans_newline, ["-e", r"1N;2y/\n/X/", LINES1]);
////////////////////////////////////////////////////////////
@@ -973,6 +1128,56 @@ check_output!(
check_output!(read_ok, [format!("4r {LINES2}"), LINES1.to_string()]);
check_output!(read_missing, ["5r /xyzzyxyzy42", LINES1]);
check_output!(read_empty, ["6r input/empty", LINES1]);
check_output!(
cmd_read_zero_addr,
[format!("0r {LINES2}"), LINES1.to_string()]
);
check_output!(
cmd_read_one_addr,
[format!("1r {LINES2}"), LINES1.to_string()]
);
#[test]
fn sandbox_rejects_read_command() {
new_ucmd!()
.args(&["--sandbox", &format!("1r {LINES2}"), LINES1])
.fails()
.stderr_contains("command not allowed with --sandbox");
}
#[test]
fn sandbox_rejects_subst_write_flag() -> std::io::Result<()> {
let temp = NamedTempFile::new()?;
let cmd = format!("s/l1/x/w {}", temp.path().display());
new_ucmd!()
.args(&["--sandbox", &cmd, LINES1])
.fails()
.stderr_contains("command not allowed with --sandbox");
let mut actual = String::new();
temp.reopen()?.read_to_string(&mut actual)?;
assert!(actual.is_empty());
Ok(())
}
#[test]
fn sandbox_rejects_write_command() -> std::io::Result<()> {
let temp = NamedTempFile::new()?;
let cmd = format!("w {}", temp.path().display());
new_ucmd!()
.args(&["--sandbox", &cmd, LINES1])
.fails()
.stderr_contains("command not allowed with --sandbox");
let mut actual = String::new();
temp.reopen()?.read_to_string(&mut actual)?;
assert!(actual.is_empty());
Ok(())
}
#[test]
fn write_single_file() -> std::io::Result<()> {
@@ -1339,7 +1544,7 @@ fn test_addr0_second_required() {
.args(&["0p"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:2: error: address 0 requires a second address\n");
.stderr_is("sed: <script argument 1>:1:2: error: address 0 can only be used with ~step, a second regular expression, or a read command\n");
}
#[test]
@@ -1348,7 +1553,7 @@ fn test_addr0_second_re_only() {
.args(&["0,4p"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:4: error: address 0 can only be used with a regular expression or ~step\n");
.stderr_is("sed: <script argument 1>:1:4: error: address 0 can only be used with ~step, a second regular expression, or a read command\n");
}
#[test]
@@ -1513,3 +1718,19 @@ fn test_print_first_line_no_newline() {
.succeeds()
.stdout_is("foo");
}
//--posix should reject GNU substitute flags i/I https://github.com/uutils/sed/issues/401
#[test]
fn test_posix_reject_flags() {
new_ucmd!()
.args(&["--posix", "s/a/b/i"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:7: error: unknown option to 's'\n");
new_ucmd!()
.args(&["--posix", "s/a/b/m"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:7: error: unknown option to 's'\n");
}
+23
View File
@@ -0,0 +1,23 @@
l1_1
l2_1
l2_2
l2_3
l2_4
l2_5
l2_6
l2_7
l2_8
l2_9
l1_2
l1_3
l1_4
l1_5
l1_6
l1_7
l1_8
l1_9
l1_10
l1_11
l1_12
l1_13
l1_14
+23
View File
@@ -0,0 +1,23 @@
l2_1
l2_2
l2_3
l2_4
l2_5
l2_6
l2_7
l2_8
l2_9
l1_1
l1_2
l1_3
l1_4
l1_5
l1_6
l1_7
l1_8
l1_9
l1_10
l1_11
l1_12
l1_13
l1_14