Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ec214b40e | ||
|
|
a18cd3fff7 | ||
|
|
30607c4ddc | ||
|
|
01c8412327 | ||
|
|
f3a79c5847 | ||
|
|
a9e5fe778c | ||
|
|
02d0239447 | ||
|
|
8b5dea703a | ||
|
|
34dfbc2ef1 | ||
|
|
8cb7090bbc | ||
|
|
2a5ab5a9a2 | ||
|
|
b14272c1d7 | ||
|
|
cd3b10db48 | ||
|
|
48c2f56358 | ||
|
|
dcd2df848a | ||
|
|
9efab796be |
No files matched your search
Generated
+8
-8
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Generated
+13
-13
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user