Author SHA1 Message Date
Sylvestre Ledru 5c98e4d68f docs: move extensions and incompatibilities to docs/src/extensions.md
Start an mdBook under docs/, laid out as in coreutils, and move the list of
extensions and incompatibilities out of the README into it. The README keeps
a pointer.

A Documentation workflow builds the book with mdbook 0.5.0, the version the
uutils website uses, whenever docs/ changes, and uploads it as an artifact.
mdbook 0.5 rejects the `multilingual` field coreutils still carries (the
website strips it before building), so the book leaves it out.
2026-09-26 20:05:52 +02:00
7 changed files with 45 additions and 164 deletions

No files matched your search

Generated
+1 -1
View File
@@ -1103,7 +1103,7 @@ dependencies = [
[[package]]
name = "sed"
version = "0.3.0"
version = "0.2.0"
dependencies = [
"assert_fs",
"chrono",
+2 -3
View File
@@ -1,12 +1,11 @@
# sed (uutils)
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
# spell-checker:ignore (libs) mangen sed binstall chrono codspeed libc memchr memmap rlimit rustix textwrap uucore uutests xattr
# spell-checker:ignore (misc) clippy codegen msvc startswith uninlined uutils
# spell-checker:ignore (libs) mangen sed
[package]
name = "sed"
version = "0.3.0"
version = "0.2.0"
license = "MIT"
description = "sed ~ implemented as universal (cross-platform) utils, written in Rust"
default-run = "sed"
+1 -1
View File
@@ -897,7 +897,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "sed"
version = "0.3.0"
version = "0.2.0"
dependencies = [
"clap",
"clap_complete",
+31 -38
View File
@@ -359,7 +359,7 @@ fn compile_address_range(
let is_step_match = line.current() == '~'; // E.g. 0~2: Pick even-numbered lines
line.advance();
line.eat_spaces();
let is_step_end = if !line.eol() && line.current() == '~' {
let is_step_end = if line.current() == '~' {
// E.g. /foo/,~10: Start at foo, include all lines until multiple of 10 is reached.
line.advance();
line.eat_spaces();
@@ -373,46 +373,39 @@ fn compile_address_range(
return compilation_error(lines, line, "~step is invalid in POSIX mode");
}
// Look for second address; a missing ~step counts as 0.
let has_addr2 = !line.eol() && (is_address_char(line.current()) || line.current() == '+');
if !(has_addr2 || is_step_match || is_step_end) {
return compilation_error(lines, line, "unexpected `,'");
}
let addr2 = if has_addr2 {
compile_address(lines, line, context)?
} else {
Address::Line(0)
};
// Set step_n to the number specified in the (required numeric) address.
let step_n = if is_step_match || is_step_end {
match addr2 {
Address::Line(n) => n,
_ => {
return compilation_error(
lines,
line,
"~step can only be specified through numeric values",
);
// Look for second address.
if !line.eol() {
let addr2 = compile_address(lines, line, context)?;
// Set step_n to the number specified in the (required numeric) address.
let step_n = if is_step_match || is_step_end {
match addr2 {
Address::Line(n) => n,
_ => {
return compilation_error(
lines,
line,
"~step can only be specified through numeric values",
);
}
}
} else {
0 // dummy, not used
};
if is_line0 && !matches!(addr2, Address::Re(_)) && !is_step_match {
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
}
} else {
0 // dummy, not used
};
// Address 0 needs a regex end or a positive ~step (0~0 selects no line).
if is_line0 && !matches!(addr2, Address::Re(_)) && !(is_step_match && step_n > 0) {
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
// If needed, transform Address::Line into Address::Step*.
cmd.addr2 = if is_step_match {
Some(Address::StepMatch(step_n))
} else if is_step_end {
Some(Address::StepEnd(step_n))
} else {
Some(addr2)
};
n_addr += 1;
}
// If needed, transform Address::Line into Address::Step*.
cmd.addr2 = if is_step_match {
Some(Address::StepMatch(step_n))
} else if is_step_end {
Some(Address::StepEnd(step_n))
} else {
Some(addr2)
};
n_addr += 1;
}
// Zero-address read command check
@@ -503,7 +496,7 @@ fn compile_address(
let number = parse_number(lines, line, true)?.unwrap();
Ok(Address::Line(number))
}
_ => compilation_error(lines, line, "expected context address"),
_ => panic!("invalid context address"),
}
}
+1 -1
View File
@@ -129,7 +129,7 @@ fn applies(
Address::Line(n) if linenum >= *n => {
context.last_address = true;
}
Address::RelLine(0) | Address::StepMatch(0) | Address::StepEnd(0) => {
Address::RelLine(n) if *n == 0 => {
context.last_address = true;
}
_ => {
+8 -38
View File
@@ -40,12 +40,7 @@ enum State {
input_name: String, // Input description (path or script string)
line_number: usize, // Current line number
},
// All scripts have been processed; keeps the last position for errors
// raised at the end of the script.
Done {
input_name: String,
line_number: usize,
},
Done, // All scripts have been processed
}
impl ScriptLineProvider {
@@ -60,18 +55,16 @@ impl ScriptLineProvider {
/// Return the currently processed script line number.
pub fn get_line_number(&self) -> usize {
match &self.state {
State::Active { line_number, .. } | State::Done { line_number, .. } => *line_number,
State::NotStarted => 0,
State::Active { line_number, .. } => *line_number,
_ => 0,
}
}
/// Return the currently processed script descriptive name.
pub fn get_input_name(&self) -> &str {
match &self.state {
State::Active { input_name, .. } | State::Done { input_name, .. } => {
input_name.as_str()
}
State::NotStarted => "",
State::Active { input_name, .. } => input_name.as_str(),
_ => "",
}
}
@@ -101,7 +94,7 @@ impl ScriptLineProvider {
return Ok(Some(line));
}
}
State::Done { .. } => {
State::Done => {
return Ok(None);
}
};
@@ -115,23 +108,7 @@ impl ScriptLineProvider {
// Move to the next available script source.
fn advance_source(&mut self, next_index: usize) -> UResult<()> {
if next_index >= self.sources.len() {
let (input_name, line_number) =
match std::mem::replace(&mut self.state, State::NotStarted) {
State::Active {
input_name,
line_number,
..
}
| State::Done {
input_name,
line_number,
} => (input_name, line_number),
State::NotStarted => (String::new(), 0),
};
self.state = State::Done {
input_name,
line_number,
};
self.state = State::Done;
return Ok(());
}
@@ -174,14 +151,7 @@ impl fmt::Debug for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
State::NotStarted => f.debug_struct("NotStarted").finish(),
State::Done {
input_name,
line_number,
} => f
.debug_struct("Done")
.field("input_name", input_name)
.field("line_number", line_number)
.finish(),
State::Done => f.debug_struct("Done").finish(),
State::Active {
index,
input_name,
+1 -82
View File
@@ -2487,20 +2487,7 @@ fn test_incomplete_test_command_posix() {
.args(&["--posix", "i\\"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:3: error: incomplete command\n");
}
#[test]
fn test_error_at_end_of_script_keeps_its_position() {
// The script is exhausted when the error is found: report its last line,
// as GNU sed does ("-e expression #1").
new_ucmd!()
.args(&["s/a/b\\"])
.fails()
.code_is(1)
.stderr_is(
"sed: <script argument 1>:1:7: error: unterminated substitute replacement (unexpected EOF)\n",
);
.stderr_is("sed: :0:3: error: incomplete command\n");
}
#[test]
@@ -2559,74 +2546,6 @@ fn test_step_end_non_posix() {
.stderr_is("sed: <script argument 1>:1:4: error: ~step is invalid in POSIX mode\n");
}
#[test]
fn test_step_without_number_is_zero() {
// `N~` and `N,~` select line N alone, like a step of 0.
for script in ["3~=", "3,~=", "3,~0="] {
new_ucmd!()
.args(&["-n", script])
.pipe_in("q\nr\ns\nt\n")
.succeeds()
.stdout_is("3\n");
}
}
#[test]
fn test_step_match_zero_closes_range() {
// `3~0` selects line 3 alone, so `c` prints its text there.
new_ucmd!()
.args(&["3~0c\\X"])
.pipe_in("1\n2\n3\n4\n5\n")
.succeeds()
.stdout_is("1\n2\nX\n4\n5\n");
}
#[test]
fn test_step_non_numeric() {
for (script, column) in [("1~/x/p", 6), ("1,~/x/p", 7)] {
new_ucmd!()
.args(&[script])
.fails()
.code_is(1)
.stderr_is(format!(
"sed: <script argument 1>:1:{column}: error: ~step can only be specified through numeric values\n"
));
}
}
#[test]
fn test_step_without_number_at_end_of_line() {
new_ucmd!()
.args(&["7~"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:3: error: command expected\n");
}
#[test]
fn test_addr0_step_zero() {
for script in ["0~0p", "0~p"] {
new_ucmd!()
.args(&[script])
.fails()
.code_is(1)
.stderr_contains("address 0 can only be used with ~step");
}
}
#[test]
fn test_comma_without_second_address() {
for (script, column) in [("5,", 3), ("5,=", 3), ("5, x", 4)] {
new_ucmd!()
.args(&[script])
.fails()
.code_is(1)
.stderr_is(format!(
"sed: <script argument 1>:1:{column}: error: unexpected `,'\n"
));
}
}
// The following test diverse ways in which regexes are matched.
// Search for 'regex\.' to find them in the code.
#[test]