Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bdf96d08d | ||
|
|
72ed709088 | ||
|
|
7f9ce84f34 | ||
|
|
cb9135ddac |
No files matched your search
@@ -0,0 +1,41 @@
|
||||
name: Documentation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/documentation.yml'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/documentation.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-doc:
|
||||
name: Build the mdBook documentation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install mdbook
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: mdbook@0.5.0
|
||||
|
||||
- name: Build documentation
|
||||
run: mdbook build docs
|
||||
|
||||
- name: Upload documentation
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: documentation
|
||||
path: docs/book
|
||||
Generated
+1
-1
@@ -1103,7 +1103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sed"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"assert_fs",
|
||||
"chrono",
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
# sed (uutils)
|
||||
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
|
||||
|
||||
# spell-checker:ignore (libs) mangen sed
|
||||
# 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
|
||||
|
||||
[package]
|
||||
name = "sed"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
license = "MIT"
|
||||
description = "sed ~ implemented as universal (cross-platform) utils, written in Rust"
|
||||
default-run = "sed"
|
||||
|
||||
@@ -88,73 +88,8 @@ cargo test
|
||||
```
|
||||
|
||||
## Extensions and incompatibilities
|
||||
### Supported GNU extensions
|
||||
* Command-line arguments can be specified in long (`--`) form.
|
||||
* 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.
|
||||
* The `a`, `c`, and `i` commands do not require an initial backslash,
|
||||
allow text to appear on the same line, and support escape sequences
|
||||
in the specified text.
|
||||
* The `a`, `i`, `=`, `l`, `q` and `r` commands support address range as an extension to POSIX.
|
||||
* The substitution command replacement group `\0` is a synonym for &.
|
||||
* An `F` command outputs the name of the file currently being processed.
|
||||
* A `Q` command (optionally followed by an exit code) quits immediately.
|
||||
* The `q` command can be optionally followed by an exit code.
|
||||
* A `W` command writes to a file the pattern's first line.
|
||||
* An `R` command reads one line at a time from a file.
|
||||
* The `l` command can be optionally followed by the output width.
|
||||
* 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.
|
||||
* In-place editing of file with the `-i` flag.
|
||||
|
||||
### New extensions
|
||||
* Unicode characters can be specified in regular expression pattern, replacement
|
||||
and transliteration sequences using `\uXXXX` or `\UXXXXXXXX` sequences.
|
||||
|
||||
### Incompatible extensions
|
||||
The `-U` or `--uutil-extensions` option enables useful extensions or bug fixes
|
||||
that aren't compatible with GNU sed or POSIX.
|
||||
|
||||
* The `l` command lists Unicode characters using the `\uXXXX` and `\UXXXXXXXX`
|
||||
escapes rather than as octal UTF-8 byte sequences.
|
||||
|
||||
### Incompatibilities
|
||||
* Similarly to GNU _sed_, input is processed as raw bytes or as valid UTF-8
|
||||
(this includes 7-bit ASCII) based on the locale as specified by the
|
||||
`LC_ALL`, `LC_CTYPE`, and `LANG` environment variables,
|
||||
with the default being byte processing.
|
||||
However, in contrast with GNU _sed_, other locales (e.g. ISO-8859-1)
|
||||
are not supported. If the input is in another code page or encoding
|
||||
and requires locale-specific processing (e.g. ignore/map case,
|
||||
character classes), consider converting it through UTF-8 to ensure
|
||||
the correct handling of locale-specific regular expressions.
|
||||
This _sed_ program can also handle arbitrary byte sequences
|
||||
if no part of the input requires treating it as a Rust String.
|
||||
* Back-references aren't supported when input is processed as bytes
|
||||
(`LC_ALL=C`).
|
||||
* The command will report an error and fail if duplicate labels are found
|
||||
in the script.
|
||||
This matches the BSD behavior. The GNU version accepts duplicate labels.
|
||||
* The last line (`$`) address is interpreted as the last non-empty line of
|
||||
the last file. If files specified in subsequent arguments until the last
|
||||
one are empty, then the last line condition will never be triggered.
|
||||
This behavior is consistent with the
|
||||
[original implementation](https://github.com/dspinellis/unix-history-repo/blob/Research-V7/usr/src/cmd/sed/sed1.c#L665).
|
||||
* Labels are parsed for alphanumeric characters. The BSD version parses them
|
||||
until the end of the line, preventing ; to be used as a separator.
|
||||
The GNU, BSD and new extensions _sed_ supports, and where it differs from GNU
|
||||
_sed_, are listed in [docs/src/extensions.md](docs/src/extensions.md).
|
||||
|
||||
## GNU test suite compatibility
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
book
|
||||
@@ -0,0 +1,7 @@
|
||||
[book]
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "uutils sed Documentation"
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/uutils/sed/tree/main/docs/src"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](index.md)
|
||||
|
||||
* [Extensions and incompatibilities](extensions.md)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Extensions and incompatibilities
|
||||
|
||||
The main goal of the project is compatibility with GNU _sed_, but _sed_ also
|
||||
supports features that GNU _sed_ does not, and differs from it in a few places.
|
||||
Below is a list of these extensions and incompatibilities.
|
||||
|
||||
## Supported GNU extensions
|
||||
* Command-line arguments can be specified in long (`--`) form.
|
||||
* 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.
|
||||
* The `a`, `c`, and `i` commands do not require an initial backslash,
|
||||
allow text to appear on the same line, and support escape sequences
|
||||
in the specified text.
|
||||
* The `a`, `i`, `=`, `l`, `q` and `r` commands support address range as an extension to POSIX.
|
||||
* The substitution command replacement group `\0` is a synonym for &.
|
||||
* An `F` command outputs the name of the file currently being processed.
|
||||
* A `Q` command (optionally followed by an exit code) quits immediately.
|
||||
* The `q` command can be optionally followed by an exit code.
|
||||
* A `W` command writes to a file the pattern's first line.
|
||||
* An `R` command reads one line at a time from a file.
|
||||
* The `l` command can be optionally followed by the output width.
|
||||
* 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.
|
||||
* In-place editing of file with the `-i` flag.
|
||||
|
||||
## New extensions
|
||||
* Unicode characters can be specified in regular expression pattern, replacement
|
||||
and transliteration sequences using `\uXXXX` or `\UXXXXXXXX` sequences.
|
||||
|
||||
## Incompatible extensions
|
||||
The `-U` or `--uutil-extensions` option enables useful extensions or bug fixes
|
||||
that aren't compatible with GNU sed or POSIX.
|
||||
|
||||
* The `l` command lists Unicode characters using the `\uXXXX` and `\UXXXXXXXX`
|
||||
escapes rather than as octal UTF-8 byte sequences.
|
||||
|
||||
## Incompatibilities
|
||||
* Similarly to GNU _sed_, input is processed as raw bytes or as valid UTF-8
|
||||
(this includes 7-bit ASCII) based on the locale as specified by the
|
||||
`LC_ALL`, `LC_CTYPE`, and `LANG` environment variables,
|
||||
with the default being byte processing.
|
||||
However, in contrast with GNU _sed_, other locales (e.g. ISO-8859-1)
|
||||
are not supported. If the input is in another code page or encoding
|
||||
and requires locale-specific processing (e.g. ignore/map case,
|
||||
character classes), consider converting it through UTF-8 to ensure
|
||||
the correct handling of locale-specific regular expressions.
|
||||
This _sed_ program can also handle arbitrary byte sequences
|
||||
if no part of the input requires treating it as a Rust String.
|
||||
* Back-references aren't supported when input is processed as bytes
|
||||
(`LC_ALL=C`).
|
||||
* The command will report an error and fail if duplicate labels are found
|
||||
in the script.
|
||||
This matches the BSD behavior. The GNU version accepts duplicate labels.
|
||||
* The last line (`$`) address is interpreted as the last non-empty line of
|
||||
the last file. If files specified in subsequent arguments until the last
|
||||
one are empty, then the last line condition will never be triggered.
|
||||
This behavior is consistent with the
|
||||
[original implementation](https://github.com/dspinellis/unix-history-repo/blob/Research-V7/usr/src/cmd/sed/sed1.c#L665).
|
||||
* Labels are parsed for alphanumeric characters. The BSD version parses them
|
||||
until the end of the line, preventing ; to be used as a separator.
|
||||
@@ -0,0 +1,10 @@
|
||||
# uutils sed
|
||||
|
||||
_sed_ is a Rust reimplementation of the
|
||||
[sed utility](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sed.html)
|
||||
with some [GNU sed](https://www.gnu.org/software/sed/manual/sed.html),
|
||||
[FreeBSD sed](https://man.freebsd.org/cgi/man.cgi?sed(1)),
|
||||
and other extensions.
|
||||
|
||||
It is part of the [uutils](https://uutils.github.io/) project. The source code
|
||||
is on [GitHub](https://github.com/uutils/sed).
|
||||
Generated
+1
-1
@@ -897,7 +897,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "sed"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_complete",
|
||||
|
||||
+41
-34
@@ -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.current() == '~' {
|
||||
let is_step_end = if !line.eol() && line.current() == '~' {
|
||||
// E.g. /foo/,~10: Start at foo, include all lines until multiple of 10 is reached.
|
||||
line.advance();
|
||||
line.eat_spaces();
|
||||
@@ -373,39 +373,46 @@ fn compile_address_range(
|
||||
return compilation_error(lines, line, "~step is invalid in POSIX mode");
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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",
|
||||
);
|
||||
}
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
// Zero-address read command check
|
||||
@@ -496,7 +503,7 @@ fn compile_address(
|
||||
let number = parse_number(lines, line, true)?.unwrap();
|
||||
Ok(Address::Line(number))
|
||||
}
|
||||
_ => panic!("invalid context address"),
|
||||
_ => compilation_error(lines, line, "expected context address"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ fn applies(
|
||||
Address::Line(n) if linenum >= *n => {
|
||||
context.last_address = true;
|
||||
}
|
||||
Address::RelLine(n) if *n == 0 => {
|
||||
Address::RelLine(0) | Address::StepMatch(0) | Address::StepEnd(0) => {
|
||||
context.last_address = true;
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -40,7 +40,12 @@ enum State {
|
||||
input_name: String, // Input description (path or script string)
|
||||
line_number: usize, // Current line number
|
||||
},
|
||||
Done, // All scripts have been processed
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
|
||||
impl ScriptLineProvider {
|
||||
@@ -55,16 +60,18 @@ impl ScriptLineProvider {
|
||||
/// Return the currently processed script line number.
|
||||
pub fn get_line_number(&self) -> usize {
|
||||
match &self.state {
|
||||
State::Active { line_number, .. } => *line_number,
|
||||
_ => 0,
|
||||
State::Active { line_number, .. } | State::Done { line_number, .. } => *line_number,
|
||||
State::NotStarted => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the currently processed script descriptive name.
|
||||
pub fn get_input_name(&self) -> &str {
|
||||
match &self.state {
|
||||
State::Active { input_name, .. } => input_name.as_str(),
|
||||
_ => "",
|
||||
State::Active { input_name, .. } | State::Done { input_name, .. } => {
|
||||
input_name.as_str()
|
||||
}
|
||||
State::NotStarted => "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +101,7 @@ impl ScriptLineProvider {
|
||||
return Ok(Some(line));
|
||||
}
|
||||
}
|
||||
State::Done => {
|
||||
State::Done { .. } => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
@@ -108,7 +115,23 @@ impl ScriptLineProvider {
|
||||
// Move to the next available script source.
|
||||
fn advance_source(&mut self, next_index: usize) -> UResult<()> {
|
||||
if next_index >= self.sources.len() {
|
||||
self.state = State::Done;
|
||||
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,
|
||||
};
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -151,7 +174,14 @@ 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 => f.debug_struct("Done").finish(),
|
||||
State::Done {
|
||||
input_name,
|
||||
line_number,
|
||||
} => f
|
||||
.debug_struct("Done")
|
||||
.field("input_name", input_name)
|
||||
.field("line_number", line_number)
|
||||
.finish(),
|
||||
State::Active {
|
||||
index,
|
||||
input_name,
|
||||
|
||||
@@ -2487,7 +2487,20 @@ fn test_incomplete_test_command_posix() {
|
||||
.args(&["--posix", "i\\"])
|
||||
.fails()
|
||||
.code_is(1)
|
||||
.stderr_is("sed: :0:3: error: incomplete command\n");
|
||||
.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",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2546,6 +2559,74 @@ 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]
|
||||
|
||||
Reference in new issue
Block a user