28 Commits
Author SHA1 Message Date
renovate[bot] 58608af612 chore(deps): update rust crate divan to v5.0.3 2026-10-14 15:26:57 +02:00
renovate[bot] d1f49fdb8a chore(deps): update rust crate sha2 to v0.11.1 2026-10-13 07:14:20 +02:00
oech3 49fc67a25e ci.yml: avoid full git clone (--depth=1) 2026-10-12 09:06:30 +02:00
Anthony DePasquale 96afb29187 sed: test q newline behavior with explicit printing 2026-10-10 19:55:31 +02:00
Anthony DePasquale 7db579b0b8 sed: don't write a missing final newline on Q, as GNU sed does 2026-10-10 19:55:31 +02:00
sap1110 cccbcec479 fixup! sed: start a range whose numeric first address was already passed 2026-10-10 16:09:45 +02:00
sap1110 443b085fb9 sed: start a range whose numeric first address was already passed
When a range command is first evaluated on a line after its numeric first
address (inside a block, or after d, n, N, c or a branch skipped it), start
the range on the current line, as GNU sed does, unless a numeric second
address has also been passed. The range can start this way only once.

Fixes #541
2026-10-10 16:09:45 +02:00
Sylvestre Ledru 10fa717ee9 sed: report where an unmatched { was opened 2026-10-10 16:08:53 +02:00
Sichen e6829c3d16 compiler: report an unterminated s or y command instead of aborting
The substitution and transliteration compilers read the delimiter with
ScriptCharProvider::current() even when the command letter was the last
character on the line. This indexed past the end of the line and
aborted.

Check for the end of the line first and report an unterminated command,
as GNU sed does. Scripts such as `s`, `1y`, and `p;s` now exit 1 with a
diagnostic instead of aborting.

The address-parser aborts from #570 were fixed separately in #578.
2026-10-06 11:19:59 +02:00
Anthony DePasquale 8a7fc1bed9 sed: write appended text, R text and e output as GNU sed does 2026-10-05 22:21:04 +02:00
Anthony DePasquale d43d4dea6b sed: discard text queued by a, r and R on Q, as GNU sed does 2026-10-05 12:34:05 +02:00
Anthony DePasquale 479c4bedf0 sed: show an empty pattern space as $ in l, even without a newline 2026-10-05 00:14:25 +02:00
Anthony DePasquale 3676eac374 sed: name the two ways n and N read the next line, and test n with -i 2026-10-03 13:11:26 -04:00
Anthony DePasquale 8ba5a5f440 sed: continue with the next command after n instead of starting a new cycle 2026-10-03 13:11:26 -04:00
Anthony DePasquale 21ffff7a8e ci: use an absolute LLVM_PROFILE_FILE path so coverage includes the sed binary 2026-10-03 06:25:14 -04:00
renovate[bot] 7e46328d8a Update Rust crate libc to v0.2.190 2026-10-03 07:05:23 +02:00
Daniel Hofstetter 111347950e clippy: fix warnings from assert_is_empty lint 2026-10-02 08:20:21 -04:00
Daniel Hofstetter 95804a298b Remove unused chrono 2026-10-01 06:59:36 -04:00
xtqqczze 0554c50b65 build(deps): update rust crates 2026-10-01 09:31:29 +02:00
renovate[bot] f7fb0d797e Update dawidd6/action-download-artifact action to v27 2026-09-30 07:19:19 +02:00
Anthony DePasquale 75fbaca15b sed: leave other options' values alone when rewriting -i 2026-09-29 16:45:34 -04:00
Anthony DePasquale e40667ffc5 sed: take the -i suffix only when attached, as GNU sed does 2026-09-29 16:45:34 -04:00
renovate[bot] dfae00da61 Update dawidd6/action-download-artifact action to v26 2026-09-29 07:00:50 +02:00
Sylvestre Ledru a05d74344e sed: underline the offending script character at a terminal
When stderr is a terminal (or UUTILS_DIAG=always), quote the offending
script line and underline the faulty character, in the layout uucore's
diagnostics use. Pipes and the test suite still get the single-line
message.

Both error constructors handle this, so no call site changes.
ScriptLocation now carries the line text, captured only when diagnostics
are on and shared between commands from the same line, so semantic
errors raised after compilation can still be drawn.

Anything that cannot be drawn (non-UTF-8 script, empty line, no line
recorded) leaves the message unchanged.
2026-09-27 20:55:01 +02:00
Sylvestre Ledru b451333205 sed: keep script position for errors at end of script 2026-09-27 20:54:49 +02:00
Sylvestre Ledru 72ed709088 sed: fix panics and zero steps in ~ and , addresses
A range address that ended early made the parser panic: `1~` read past
the end of the line, and `1~p`, `1,~p` or `1,p` handed a command letter
to the address parser, which only knew how to panic on it. A missing
~step now counts as 0, and a comma with no address after it is reported
as an unexpected `,'. The address parser returns an error instead of
panicking.

A step of 0 was not handled either: `2,~0` ran to the end of the input
instead of selecting line 2 alone, and `0~0`, which can select no line,
was accepted.
2026-09-27 16:44:36 +02:00
Sylvestre Ledru 7f9ce84f34 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-27 11:42:09 +02:00
Sylvestre Ledru cb9135ddac prepare version 0.3.0 2026-09-27 08:14:38 +02:00
21 changed files with 2161 additions and 930 deletions

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ jobs:
persist-credentials: false
- name: Retrieve reference artifacts
uses: dawidd6/action-download-artifact@v25
uses: dawidd6/action-download-artifact@v27
continue-on-error: true
with:
workflow: GnuTests.yml
+2 -2
View File
@@ -97,12 +97,12 @@ jobs:
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "sed-%p-%m.profraw"
LLVM_PROFILE_FILE: "${{ github.workspace }}/sed-%p-%m.profraw"
- name: "`grcov` ~ install"
id: build_grcov
shell: bash
run: |
git clone https://github.com/mozilla/grcov.git ~/grcov/
git clone --depth=1 https://github.com/mozilla/grcov.git ~/grcov/
cd ~/grcov
cargo install --path .
cd -
+41
View File
@@ -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
+416 -330
View File
File diff suppressed because it is too large. Load diff
+6 -7
View File
@@ -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 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"
@@ -41,9 +42,6 @@ sed = []
[workspace.dependencies]
assert_fs = "1.1.3"
chrono = { version = "0.4.37", default-features = false, features = [
"clock",
] }
clap = { version = "4.4", features = ["wrap_help", "cargo"] }
clap_complete = "4.5"
clap_mangen = "0.3"
@@ -62,7 +60,8 @@ sha2 = { version = "0.11.0", default-features = false, features = ["alloc"] }
tempfile = "3.10.1"
terminal_size = "0.4.2"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
uucore = { version = "0.12.0", features = ["libc"] }
unicode-width = "0.2.2"
uucore = { version = "0.12.0", features = ["diagnostics", "libc"] }
rustix = "1.1.4"
xattr = "1.3.1"
@@ -80,12 +79,12 @@ regex = { workspace = true }
tempfile = { workspace = true }
terminal_size = { workspace = true }
textwrap = { workspace = true }
unicode-width = { workspace = true }
uucore = { workspace = true }
rustix = { workspace = true }
[dev-dependencies]
assert_fs = { workspace = true }
chrono = { workspace = true }
ctor = "1.0.0"
divan = { workspace = true }
hex = { workspace = true }
+2 -67
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
book
+7
View File
@@ -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"
+5
View File
@@ -0,0 +1,5 @@
# Summary
[Introduction](index.md)
* [Extensions and incompatibilities](extensions.md)
+86
View File
@@ -0,0 +1,86 @@
# 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.
As in GNU _sed_, a backup suffix must be attached (`-i.bak`,
`--in-place=.bak`); BSD's `-i .bak` and `-i ''` are not supported.
## New extensions
* Unicode characters can be specified in regular expression pattern, replacement
and transliteration sequences using `\uXXXX` or `\UXXXXXXXX` sequences.
* On a terminal, script errors also quote the script line and underline the
character at fault (`UUTILS_DIAG=always` or `never` overrides this):
```
$ sed 's/a/b/q'
sed: <script argument 1>:1:7: error: invalid substitute flag: 'q'
╭─[ <script argument 1>:1:7 ]
│
1 │ s/a/b/q
│ ─
───╯
```
## 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.
+10
View File
@@ -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).
+391 -406
View File
File diff suppressed because it is too large. Load diff
+17 -4
View File
@@ -54,6 +54,8 @@ pub struct ProcessingContext {
pub last_file: bool,
/// Stop processing further input.
pub stop_processing: bool,
/// Stop without writing a missing final newline (`Q`).
pub quit_silently: bool,
/// Whether sed operates on bytes or UTF-8 characters
pub character_mode: CharacterMode,
/// Previously compiled RE, saved for reuse when specifying an empty RE
@@ -64,8 +66,8 @@ pub struct ProcessingContext {
pub input_action: Option<InputAction>,
/// Hold space
pub hold: ByteSpace,
/// Nesting of { } at compile time
pub parsed_block_nesting: usize,
/// Location of each `{` still open at compile time, innermost last
pub open_blocks: Vec<ScriptLocation>,
/// Command associated with each label
pub label_to_command_map: HashMap<String, Rc<RefCell<Command>>>,
/// Shared `R` readers keyed by file path, so that all `R` commands naming
@@ -334,6 +336,7 @@ pub struct Command {
pub addr2: Option<Address>, // End address
pub non_select: bool, // True if '!'
pub start_line: Option<usize>, // Start line number (or None if unlatched)
pub range_started: bool, // True once the range has been entered
pub data: CommandData, // Command-specific data
pub next: Option<Rc<RefCell<Command>>>, // Pointer to next command
pub location: ScriptLocation, // Command's definition location
@@ -347,6 +350,7 @@ impl Default for Command {
addr2: None,
non_select: false,
start_line: None,
range_started: false,
data: CommandData::None,
next: None,
location: ScriptLocation::default(),
@@ -392,8 +396,17 @@ pub enum SpaceFlag {
pub struct InputAction {
/// Next command to execute (rather than commands from start)
pub next_command: Option<Rc<RefCell<Command>>>,
/// Data to prepend to the read contents
pub prepend: Vec<u8>,
/// How the read line becomes the pattern space
pub next_line: NextLine,
}
#[derive(Debug, Clone)]
/// How a line read by `n` or `N` becomes the pattern space
pub enum NextLine {
/// Replace the pattern space with the line (`n`)
Replace,
/// Append the line, after a newline, to these contents (`N`)
Append(Vec<u8>),
}
#[cfg(test)]
+57 -41
View File
@@ -29,7 +29,7 @@ use std::path::PathBuf;
use std::rc::Rc;
use terminal_size::{Width, terminal_size};
use uucore::error::{UResult, USimpleError};
use uucore::error::UResult;
const DEFAULT_OUTPUT_WIDTH: usize = 60;
@@ -77,6 +77,11 @@ pub fn compile(
#[cfg(any())]
dbg!(&result);
// Like GNU sed, point at the innermost `{` left open.
if let Some(location) = context.open_blocks.last() {
return semantic_error(location, "unmatched `{'");
}
// Link branch commands to the target label commands.
populate_label_map(result.clone(), context)?;
populate_range_commands(result.clone(), context);
@@ -85,9 +90,6 @@ pub fn compile(
// Link the ends of command blocks to their following commands.
// This converts the tree into a graph, so it must be the last
// conversion that traverses the structure as a tree.
if context.parsed_block_nesting > 0 {
return Err(USimpleError::new(1, "unmatched `{'"));
}
patch_block_endings(result.clone());
Ok(result)
@@ -359,7 +361,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 +375,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 +505,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"),
}
}
@@ -834,6 +843,10 @@ fn compile_subst_command(
) -> UResult<CommandHandling> {
line.advance(); // move past 's'
if line.eol() {
return compilation_error(lines, line, "unterminated `s' command");
}
let delimiter = line.current();
if delimiter == '\0' || delimiter == '\\' {
return compilation_error(
@@ -900,6 +913,10 @@ fn compile_trans_command(
) -> UResult<CommandHandling> {
line.advance(); // move past 'y'
if line.eol() {
return compilation_error(lines, line, "unterminated `y' command");
}
let delimiter = line.current();
if delimiter == '\0' || delimiter == '\\' {
return compilation_error(
@@ -1067,10 +1084,9 @@ fn compile_end_group_command(
cmd: &mut Command,
context: &mut ProcessingContext,
) -> UResult<CommandHandling> {
if context.parsed_block_nesting == 0 {
if context.open_blocks.pop().is_none() {
return compilation_error(lines, line, "unexpected `}'");
}
context.parsed_block_nesting -= 1;
line.advance();
line.eat_spaces();
parse_command_ending(lines, line, cmd)?;
@@ -1169,7 +1185,7 @@ fn compile_block_command(
context: &mut ProcessingContext,
) -> UResult<CommandHandling> {
line.advance(); // move past '{'
context.parsed_block_nesting += 1;
context.open_blocks.push(cmd.location.clone());
let block_body = compile_sequence(lines, line, context)?;
cmd.data = CommandData::BranchTarget(block_body);
Ok(CommandHandling::Continue)
+151 -7
View File
@@ -12,8 +12,13 @@ use crate::sed::command::ProcessingContext;
use crate::sed::script_char_provider::ScriptCharProvider;
use crate::sed::script_line_provider::ScriptLineProvider;
use std::env;
use std::io::{IsTerminal, stderr};
use std::rc::Rc;
use std::sync::OnceLock;
use unicode_width::UnicodeWidthChar;
use uucore::diagnostics;
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError};
@@ -23,6 +28,9 @@ pub struct ScriptLocation {
pub input_name: Rc<str>, // Shared input name
pub line_number: usize, // 1-based line number
pub column_number: usize, // 1-based column number
/// The script line, for errors raised after it was consumed.
/// `None` when diagnostics are off.
pub line_text: Option<Rc<str>>,
}
impl Default for ScriptLocation {
@@ -31,6 +39,7 @@ impl Default for ScriptLocation {
input_name: Rc::from("<unknown>"),
line_number: 1,
column_number: 1,
line_text: None,
}
}
}
@@ -42,10 +51,123 @@ impl ScriptLocation {
line_number: lines.get_line_number(),
column_number: line.get_pos() + 1,
input_name: Rc::from(lines.get_input_name()),
line_text: if diagnostics_enabled() {
line.line_text()
} else {
None
},
}
}
}
/// Whether script errors get a snippet. Evaluated once: a location is
/// recorded for every compiled command, and the terminal check is a syscall.
fn diagnostics_enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(diagnostics::enabled)
}
/// Format `msg` as an error at the given script position, followed by the
/// offending script line, underlined, when diagnostics are enabled.
fn script_error(
input_name: &str,
line_number: usize,
column_number: usize,
line_text: Option<&str>,
msg: impl ToString,
) -> String {
let message = format!(
"{input_name}:{line_number}:{column_number}: error: {}",
msg.to_string()
);
// Line 0: the script was exhausted, so there is no line to show.
if !diagnostics_enabled() || line_number == 0 {
return message;
}
// A script line that is not valid UTF-8 cannot be drawn.
let Some(text) = line_text.filter(|text| !text.is_empty()) else {
return message;
};
// `column_number` is a 1-based byte column; errors at end of line point
// one past the last character.
let Some(prefix) = text.get(..(column_number - 1).min(text.len())) else {
return message;
};
let drawn = render_snippet(
input_name,
line_number,
column_number,
text,
prefix,
use_color(),
);
format!("{message}\n{drawn}")
}
/// Whether the underline is colored. uucore's diagnostics make the same
/// check but keep it private.
fn use_color() -> bool {
env::var_os("NO_COLOR").is_none() && stderr().is_terminal()
}
/// Draw `text` as line `line_number` of `input_name`, underlining the
/// character that follows `prefix`, in the layout uucore's diagnostics use:
///
/// ```text
/// ╭─[ <script argument 1>:1:7 ]
/// │
/// 1 │ s/a/b/q
/// │ ─
/// ───╯
/// ```
fn render_snippet(
input_name: &str,
line_number: usize,
column_number: usize,
text: &str,
prefix: &str,
color: bool,
) -> String {
let number = line_number.to_string();
let margin = " ".repeat(number.len() + 2);
// Pad by display width so the underline lines up with the character
// above it; tabs are kept as they are, since their width depends on the
// terminal.
let indent: String = prefix
.chars()
.flat_map(|c| {
let (fill, width) = if c == '\t' {
('\t', 1)
} else {
(' ', c.width().unwrap_or(0))
};
std::iter::repeat_n(fill, width)
})
.collect();
// As wide as the offending character; one column past the end of line.
let width = text[prefix.len()..]
.chars()
.next()
.and_then(UnicodeWidthChar::width)
.unwrap_or(1)
.max(1);
let underline = "─".repeat(width);
let underline = if color {
format!("\x1b[31m{underline}\x1b[0m")
} else {
underline
};
format!(
"{margin}╭─[ {input_name}:{line_number}:{column_number} ]\n\
{margin}│\n\
\x20{number} │ {text}\n\
{margin}│ {indent}{underline}\n\
{rule}╯",
rule = "─".repeat(margin.len()),
)
}
/// Fail with msg as a compile error at the provider location.
/// The error's exit code is 1 (compilation phase).
pub fn compilation_error<T>(
@@ -55,12 +177,12 @@ pub fn compilation_error<T>(
) -> UResult<T> {
Err(USimpleError::new(
1,
format!(
"{}:{}:{}: error: {}",
script_error(
lines.get_input_name(),
lines.get_line_number(),
line.get_pos() + 1,
msg.to_string()
str::from_utf8(line.get_line()).ok(),
msg,
),
))
}
@@ -70,12 +192,12 @@ pub fn compilation_error<T>(
fn location_error<T>(location: &ScriptLocation, msg: impl ToString, exit_code: i32) -> UResult<T> {
Err(USimpleError::new(
exit_code,
format!(
"{}:{}:{}: error: {}",
location.input_name,
script_error(
&location.input_name,
location.line_number,
location.column_number,
msg.to_string()
location.line_text.as_deref(),
msg,
),
))
}
@@ -115,3 +237,25 @@ pub fn input_runtime_error<T>(
),
))
}
#[cfg(test)]
mod tests {
use super::*;
// The integration tests check the layout; they cannot get color, as
// their stderr is not a terminal.
#[test]
fn the_underline_is_red_when_colored() {
let drawn = render_snippet("f.sed", 1, 7, "s/a/b/q", "s/a/b/", true);
assert_eq!(
drawn,
concat!(
" ╭─[ f.sed:1:7 ]\n",
" │\n",
" 1 │ s/a/b/q\n",
" │ \x1b[31m─\x1b[0m\n",
"───╯",
)
);
}
}
+13 -6
View File
@@ -678,13 +678,20 @@ impl OutputBuffer {
)))
}
/// Copy the specified file to the output.
pub fn copy_file(&mut self, path: &PathBuf) -> io::Result<()> {
// Flush mmap writes, if any.
/// Schedule the specified text for output unchanged, after any
/// deferred newline.
pub fn write_raw(&mut self, bytes: &[u8]) -> io::Result<()> {
self.flush_pending_newline()?;
#[cfg(unix)]
{
self.flush_mmap(WriteRange::Complete)?;
}
self.flush_mmap(WriteRange::Complete)?;
self.out.write_all(bytes)
}
/// Copy the specified file to the output, after any deferred newline.
pub fn copy_file(&mut self, path: &PathBuf) -> io::Result<()> {
// As in GNU sed, write the deferred newline even when the file is
// empty or cannot be read.
self.write_raw(b"")?;
let Ok(file) = File::open(path) else {
// Per POSIX, if the file can't be read treat it as empty.
+194 -8
View File
@@ -28,6 +28,7 @@ use crate::sed::script_line_provider::ScriptValue;
use clap::{Arg, ArgMatches, Command, arg};
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::format_usage;
@@ -38,7 +39,7 @@ const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (uutils)");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let matches = parse_args(args)?;
// Don't use arg_required_else_help when declaring command
// as it exits with code 2 and we use it to check
@@ -56,6 +57,120 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Ok(())
}
fn parse_args(args: impl IntoIterator<Item = OsString>) -> clap::error::Result<ArgMatches> {
let mut cmd = uu_app();
cmd.build();
let args = gnu_in_place_args(&cmd, args);
cmd.try_get_matches_from(args)
}
/// Rewrite GNU's `-iSUFFIX`, which clap cannot parse, as `--in-place=SUFFIX`,
/// splitting it from flags clustered before it: `-ni.bak` becomes
/// `-n --in-place=.bak`. The values of other options are left as they are.
fn gnu_in_place_args(cmd: &Command, args: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
let mut args = args.into_iter();
// The program name.
let mut out: Vec<OsString> = args.next().into_iter().collect();
while let Some(arg) = args.next() {
// Pass non-UTF-8 arguments to clap unchanged.
let arg = match arg.into_string() {
Ok(arg) => arg,
Err(arg) => {
out.push(arg);
continue;
}
};
if arg == "--" {
out.push(arg.into());
break;
}
match option_kind(cmd, &arg) {
OptionKind::InPlace(pos) => {
if pos > 1 {
out.push(arg[..pos].into());
}
out.push(format!("--in-place={}", &arg[pos + 1..]).into());
}
OptionKind::ValueFollows => {
out.push(arg.into());
// Keep the value as is, even if it looks like `-iSUFFIX`.
out.extend(args.next());
}
OptionKind::Other => out.push(arg.into()),
}
}
out.extend(args);
out
}
enum OptionKind {
/// A cluster like `-ni.bak`, with the position of its `i`.
InPlace(usize),
/// An option whose value is the next argument, like `-f FILE`.
ValueFollows,
Other,
}
fn option_kind(cmd: &Command, arg: &str) -> OptionKind {
if let Some(name) = arg.strip_prefix("--") {
return match long_option(cmd, name) {
Some(opt) if opt.get_action().takes_values() && !opt.is_require_equals_set() => {
OptionKind::ValueFollows
}
_ => OptionKind::Other,
};
}
let Some(cluster) = arg.strip_prefix('-') else {
return OptionKind::Other;
};
for (pos, c) in cluster.char_indices() {
// Leave unknown options for clap to report.
let Some(opt) = cmd.get_arguments().find(|a| {
a.get_short() == Some(c) || a.get_all_short_aliases().is_some_and(|s| s.contains(&c))
}) else {
return OptionKind::Other;
};
let attached = pos + c.len_utf8() < cluster.len();
if opt.get_id() == "in-place" {
return if attached {
OptionKind::InPlace(pos + 1)
} else {
OptionKind::Other
};
}
if opt.get_action().takes_values() {
// The rest of the cluster, or else the next argument, is the value.
return if attached {
OptionKind::Other
} else {
OptionKind::ValueFollows
};
}
}
OptionKind::Other
}
/// Find a long option by its name or, as `infer_long_args` allows, a unique
/// prefix of it. `None` if `name` includes a value.
fn long_option<'a>(cmd: &'a Command, name: &str) -> Option<&'a Arg> {
if name.contains('=') {
return None;
}
let names = |a: &'a Arg| {
a.get_long()
.into_iter()
.chain(a.get_all_aliases().into_iter().flatten())
};
if let Some(opt) = cmd.get_arguments().find(|a| names(a).any(|n| n == name)) {
return Some(opt);
}
let mut found = cmd
.get_arguments()
.filter(|a| names(a).any(|n| n.starts_with(name)));
let opt = found.next()?;
found.next().is_none().then_some(opt)
}
#[allow(clippy::cognitive_complexity)]
pub fn uu_app() -> Command {
#[cfg(windows)]
@@ -87,7 +202,9 @@ pub fn uu_app() -> Command {
.short_alias('r')
.help("Use extended regular expressions.")
.action(clap::ArgAction::SetTrue),
// As in GNU sed, a value may begin with `-`.
arg!(-e --expression <SCRIPT> "Add script to executed commands.")
.allow_hyphen_values(true)
.action(clap::ArgAction::Append),
// Access with .get_many::<PathBuf>("file")
Arg::new("script-file")
@@ -95,20 +212,25 @@ pub fn uu_app() -> Command {
.long("script-file")
.help("Specify script file.")
.value_parser(clap::value_parser!(PathBuf))
.allow_hyphen_values(true)
.action(clap::ArgAction::Append),
Arg::new("follow-symlinks")
.long("follow-symlinks")
.help("Follow symlinks when processing in place.")
.action(clap::ArgAction::SetTrue),
// Access with .get_one::<String>("in-place")
// The SUFFIX must be attached, as in GNU sed.
Arg::new("in-place")
.short('i')
.long("in-place")
.help("Edit files in place, making a backup if SUFFIX is supplied.")
.value_name("SUFFIX")
.num_args(0..=1)
.require_equals(true)
.default_missing_value(""),
// Access with .get_one::<u32>("line-length")
arg!(-l --length <NUM> "Specify the 'l' command line-wrap length.")
.allow_hyphen_values(true)
.value_parser(clap::value_parser!(u32)),
arg!(-n --quiet "Suppress automatic printing of pattern space.").aliases(["silent"]),
arg!(--posix "Disable non-POSIX extensions."),
@@ -261,13 +383,14 @@ fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
last_line: false,
last_file: false,
stop_processing: false,
quit_silently: false,
saved_regex: None,
input_action: None,
hold: ByteSpace {
content: Vec::new(),
has_newline: true,
},
parsed_block_nesting: 0,
open_blocks: Vec::new(),
label_to_command_map: HashMap::new(),
named_readers: HashMap::new(),
range_commands: Vec::new(),
@@ -445,13 +568,76 @@ mod tests {
assert!(ctx.regex_extended);
}
#[test]
fn test_in_place_with_suffix() {
let matches = test_matches(&["-i", ".bak"]);
let ctx = build_context(&matches).unwrap();
// In-place argument rewriting
fn in_place_args(args: &[&str]) -> Vec<String> {
let mut cmd = uu_app();
cmd.build();
gnu_in_place_args(&cmd, ["sed"].iter().chain(args).map(OsString::from))
.into_iter()
.skip(1)
.map(|arg| arg.into_string().unwrap())
.collect()
}
assert!(ctx.in_place);
assert_eq!(ctx.in_place_suffix, Some(".bak".to_string()));
#[test]
fn test_in_place_args_rewrite_attached_suffix() {
let cases: &[(&[&str], &[&str])] = &[
(&["-i.bak"], &["--in-place=.bak"]),
// Everything after the `i` is the suffix, as in GNU sed.
(&["-iE"], &["--in-place=E"]),
(&["-i=.bak"], &["--in-place==.bak"]),
// Flags clustered before `-i` are kept.
(&["-ni.bak"], &["-n", "--in-place=.bak"]),
(&["-Esi.bak"], &["-Es", "--in-place=.bak"]),
(&["-ri.bak"], &["-r", "--in-place=.bak"]),
(
&["-i.bak", "--", "-i.keep"],
&["--in-place=.bak", "--", "-i.keep"],
),
// Only the next argument is another option's value.
(&["-e", "p", "-i.bak"], &["-e", "p", "--in-place=.bak"]),
(
&["--expression=p", "-i.bak"],
&["--expression=p", "--in-place=.bak"],
),
(&["--quiet", "-i.bak"], &["--quiet", "--in-place=.bak"]),
(
&["--in-place", "-i.bak"],
&["--in-place", "--in-place=.bak"],
),
// A `--` that is an option's value does not end the options.
(&["-e", "--", "-i.bak"], &["-e", "--", "--in-place=.bak"]),
];
for (args, expected) in cases {
assert_eq!(in_place_args(args), *expected, "args: {args:?}");
}
}
#[test]
fn test_in_place_args_leave_other_arguments_alone() {
let cases: &[&[&str]] = &[
&["-i", "s/a/b/", "file"],
&["-Ei", "s/a/b/", "file"],
&["-En", "s/a/b/", "file"],
&["--in-place=.bak", "s/a/b/", "file"],
&["--in=.bak", "s/a/b/", "file"],
// Values attached to other options.
&["-fi.sed", "file"],
&["-nfi.sed", "file"],
&["-ei", "file"],
// Values that follow other options.
&["-f", "-ifoo.sed", "file"],
&["-nf", "-ifoo.sed", "file"],
&["-l", "-i5"],
&["--expression", "-i.bak"],
&["--expr", "-i.bak"],
// Unknown options are left for clap to report.
&["-xi.bak"],
&["--", "-i.bak"],
];
for args in cases {
assert_eq!(in_place_args(args), *args, "args: {args:?}");
}
}
#[test]
+47 -28
View File
@@ -9,8 +9,8 @@
// file that was distributed with this source code.
use crate::sed::command::{
Address, AppendElement, CharacterMode, Command, CommandData, InputAction, ProcessingContext,
Transliteration,
Address, AppendElement, CharacterMode, Command, CommandData, InputAction, NextLine,
ProcessingContext, Transliteration,
};
use crate::sed::delimited_parser::os_string_from_bytes;
use crate::sed::error_handling::{ScriptLocation, input_runtime_error};
@@ -124,12 +124,20 @@ fn applies(
}
} else if let Some(addr1) = &command.addr1 {
// See if latch must start.
if match_address(addr1, reader, pattern, context, &command.location)? {
let starts = match addr1 {
// Numeric start already passed (block, d, n, branch): start once here.
Address::Line(n) if linenum > *n => {
!command.range_started && !matches!(addr2, Address::Line(m) if *m < linenum)
}
_ => match_address(addr1, reader, pattern, context, &command.location)?,
};
if starts {
command.range_started = true;
match addr2 {
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;
}
_ => {
@@ -467,7 +475,7 @@ fn flush_appends(output: &mut OutputBuffer, context: &mut ProcessingContext) ->
for elem in &context.append_elements {
match elem {
AppendElement::Text(text) => {
output.write_bytes(text.as_ref())?;
output.write_raw(text.as_ref())?;
}
AppendElement::Path(path) => {
output.copy_file(path)?;
@@ -567,9 +575,7 @@ fn list(
) -> UResult<()> {
// Special case for an empty pattern space
if line.is_empty() {
if line.is_newline_terminated() {
output.write_str("$\n")?;
}
output.write_str("$\n")?;
return Ok(());
}
@@ -654,12 +660,13 @@ fn process_file(
// Set the script command from which to start.
let mut current: Option<Rc<RefCell<Command>>> =
if let Some(action) = context.input_action.take() {
// Continue processing the `N` command.
let mut combined_lines = action.prepend;
combined_lines.push(b'\n');
combined_lines.extend_from_slice(pattern.as_bytes());
// Continue processing the `N` or `n` command.
if let NextLine::Append(mut combined_lines) = action.next_line {
combined_lines.push(b'\n');
combined_lines.extend_from_slice(pattern.as_bytes());
pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated());
pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated());
}
action.next_command
} else {
// Start from the script top.
@@ -737,7 +744,7 @@ fn process_file(
}
CommandData::Text(cmd_bytes) => {
let shell_out = shell_stdout(cmd_bytes.to_vec(), &command, context)?;
output.write_bytes(&shell_out)?;
output.write_raw(&shell_out)?;
}
_ => panic!("invalid 'e' command data"),
},
@@ -779,7 +786,17 @@ fn process_file(
list(output, &pattern, width, &command.location, context)?;
}
'n' => {
break;
// Print the pattern space and continue with the next
// command once the next line is read, as `N` does.
if !context.quiet {
write_chunk(output, context, &pattern)?;
}
flush_appends(output, context)?;
context.input_action = Some(InputAction {
next_command: command.next.clone(),
next_line: NextLine::Replace,
});
continue 'lines;
}
'N' => {
flush_appends(output, context)?;
@@ -789,7 +806,7 @@ fn process_file(
// to perform when the next line is read.
context.input_action = Some(InputAction {
next_command: command.next.clone(),
prepend: pattern.as_bytes().to_vec(),
next_line: NextLine::Append(pattern.as_bytes().to_vec()),
});
continue 'lines;
}
@@ -818,7 +835,10 @@ fn process_file(
i32::try_from(*extract_variant!(command, Number)).unwrap_or(i32::MAX),
);
context.stop_processing = true;
context.quit_silently = true;
context.quiet = true;
// Like GNU sed, discard text queued by `a`, `r` and `R`.
context.append_elements.clear();
break;
}
'R' => {
@@ -934,18 +954,20 @@ fn process_file(
flush_appends(output, context)?;
if context.stop_processing {
output.flush_pending_newline()?;
if !context.quit_silently {
output.flush_pending_newline()?;
}
break;
}
}
// Handle any N command remains.
// Take it even with -n, so that it does not carry over to the next file.
// Handle any n or N command remains.
// Take them even with -n, so that they do not carry over to the next file.
if context.separate
&& let Some(action) = context.input_action.take()
&& let NextLine::Append(mut pending) = action.next_line
&& !context.quiet
{
let mut pending = action.prepend;
pending.push(b'\n');
output.write_bytes(&pending)?;
if context.unbuffered {
@@ -961,13 +983,10 @@ fn reset_latched_address_ranges(range_commands: &mut [Rc<RefCell<Command>>]) {
for cmd_rc in range_commands.iter() {
let mut cmd = cmd_rc.borrow_mut();
cmd.start_line =
// Check for address-spec line 0 pre-latch extension.
if let Some(addr1) = &cmd.addr1 && matches!(addr1, Address::Line(0)) {
Some(0)
} else {
None
};
// Check for address-spec line 0 pre-latch extension.
let pre_latched = matches!(cmd.addr1, Some(Address::Line(0)));
cmd.start_line = if pre_latched { Some(0) } else { None };
cmd.range_started = pre_latched;
}
}
@@ -1005,8 +1024,8 @@ pub fn process_all_files(
&& !context.separate
&& !context.quiet
&& let Some(action) = context.input_action.take()
&& let NextLine::Append(mut pending) = action.next_line
{
let mut pending = action.prepend;
pending.push(b'\n');
output.write_bytes(&pending)?;
}
+18
View File
@@ -8,10 +8,15 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::cell::OnceCell;
use std::rc::Rc;
#[derive(Debug)]
pub struct ScriptCharProvider {
line: Vec<u8>,
pos: usize,
/// The line as text, shared by the commands compiled from it.
text: OnceCell<Option<Rc<str>>>,
}
impl ScriptCharProvider {
@@ -20,6 +25,7 @@ impl ScriptCharProvider {
Self {
line: line.as_ref().to_vec(),
pos: 0,
text: OnceCell::new(),
}
}
@@ -66,6 +72,18 @@ impl ScriptCharProvider {
pub fn get_pos(&self) -> usize {
self.pos
}
/// Return the whole line being scanned.
pub fn get_line(&self) -> &[u8] {
&self.line
}
/// Return the line as shared text, or `None` if it is not valid UTF-8.
pub fn line_text(&self) -> Option<Rc<str>> {
self.text
.get_or_init(|| str::from_utf8(&self.line).ok().map(Rc::from))
.clone()
}
}
#[cfg(test)]
+25 -8
View File
@@ -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,10 @@ 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;
self.state = State::Done {
input_name: self.get_input_name().to_string(),
line_number: self.get_line_number(),
};
return Ok(());
}
@@ -151,7 +161,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,
Loaded 20 of 21 files, more files were not shown because too many files have changed in this diff. Show more