63 Commits
Author SHA1 Message Date
Diomidis Spinellis 95cbd302fe Fix handling of lines ending without a newline 2026-08-20 21:05:37 +02:00
Diomidis Spinellis 1e390a63c0 Revert d7a713e fast_io.rs pending_newline addition
It violated layering by having it track higher-level newline behavior
and caused R of non-newline terminated files to fail; (see
read_one_no_newline test).
2026-08-20 21:05:37 +02:00
Diomidis SpinellisandMukunda Katta 452e249296 Implement GNU R (read next line) command
Co-authored-by: Mukunda Katta <mukunda.vjcs6@gmail.com>
2026-08-20 21:05:37 +02:00
Diomidis Spinellis c8b5dc53e3 Fix multiple write commands to same file
Maintining different writers for the same path can fail due to buffering
or underlying OS restrictions. In particular, this fails on Windows.
Fix it through a map of canonicalized write paths.
2026-08-20 10:20:38 +03:00
Diomidis SpinellisandMukunda Katta 714ec8a7bf Implement GNU W (write pattern first line) command
While at it, also fix the w command on patterns missing a newline.

Co-authored-by: Mukunda Katta <mukunda.vjcs6@gmail.com>
2026-08-19 20:22:54 +03:00
Diomidis Spinellis 79e5df0df8 Document F command extension 2026-08-19 14:32:26 +03:00
Diomidis Spinellis 3f72f93c5d Support F (output filename) command (#528)
This also requires storing input_name as PathBuf to avoid lossy
conversions when outputting an OS-specific byte sequence representing
the file name.
2026-08-19 12:20:27 +02:00
renovate[bot] d837ddf398 Update Rust crate similar to v3.2.0 2026-08-17 13:53:54 +02:00
renovate[bot] b1ca27e82d Update dawidd6/action-download-artifact action to v23 2026-08-16 10:06:49 +02:00
renovate[bot] ce711ac531 Update Rust crate clap_mangen to v0.3.3 2026-08-13 08:11:59 +02:00
renovate[bot] 618a3f7c39 Update Rust crate ctor to v1.0.13 2026-08-10 08:16:11 +02:00
Sylvestre Ledru 079eb9340e Add cargo-binstall support (#520)
Point cargo-binstall at the archives published by dist in the GitHub
releases, so 'cargo binstall sed' fetches a prebuilt binary instead of
building from source.
2026-08-09 10:00:46 +02:00
Sylvestre Ledru 2ce633cb8d prepare version 0.2.0 (#519) 2026-08-09 01:57:25 +02:00
Mukunda Rao Katta 91465e566b fix: identify uutils in cli output (#435) 2026-08-09 00:55:37 +02:00
Sylvestre Ledru d842aa305e sed: fix clippy cast_possible_truncation errors (#518) 2026-08-09 00:52:26 +02:00
xtqqczze d28a1f8e73 chore(cargo): remove deprecated package authors field (#444) 2026-08-08 23:28:22 +02:00
renovate[bot] af5f8afdcb Update Rust crate clap_complete to v4.6.9 2026-08-07 08:07:56 +02:00
renovate[bot] 8f3f126816 Update Rust crate clap_mangen to v0.3.2 2026-08-07 08:05:08 +02:00
renovate[bot] c771318a7a Update Rust crate clap to v4.6.6 2026-08-07 08:03:52 +02:00
renovate[bot] 36a1cf4954 Update Rust crate uucore to 0.10.0 2026-08-06 09:02:46 +02:00
renovate[bot] 13bb82ba13 Update Rust crate clap_mangen to v0.3.1 2026-08-06 08:32:10 +02:00
renovate[bot] cb9eb1da42 Update Rust crate uufuzz to 0.10.0 2026-08-06 08:15:55 +02:00
renovate[bot] b101462d1b Update Rust crate uutests to 0.10.0 2026-08-06 08:10:31 +02:00
oech3 9f2e1a1ad4 remove sysinfo dep 2026-08-05 14:18:30 +02:00
Franklin-Qi 2b092d53b0 fast_io.rs: replace the unsafe libc::copy_file_range call by rustix (#458)
Replace the unsafe `libc::copy_file_range` call with the safe `rustix::fs::copy_file_range`.

Closes: #442
2026-08-05 13:32:45 +02:00
renovate[bot] 550d545d72 Update Rust crate similar to v3.1.2 2026-08-05 08:01:27 +02:00
Diomidis Spinellisandmukunda katta a5d219764f feat: add T command (branch on no-substitution) (#508)
* feat: add T command (branch on no-substitution)

Implements the GNU `T LABEL` command: branch to LABEL if no s///
substitution has succeeded since the last input line was read or the
last t/T was taken. It is the inverse of `t` and shares the same
address and label parsing.

Wires 'T' into get_cmd_spec, resolve_branch_targets, and the processor
branch logic, resetting the substitution flag on evaluation just like
't'. Adds integration tests (simple branch + flag-clear) and a cmd-spec
unit test.

Closes #395

Co-authored-by: Diomidis Spinellis <dds@aueb.gr>

* ci: re-trigger checks (windows runner crates.io download flake)

* Disable T in POSIX mode

---------

Co-authored-by: mukunda katta <mukunda.vjcs6@gmail.com>
2026-08-02 17:47:18 +03:00
Develandpre-commit-ci[bot] 397ab86302 Implement v command #397 (#420)
* implement version command

* add tests for version option

* fix patch comparsion

* absurd extreme comparsion fix

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

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

* accept missing minor or patch

* guard 'v' with if posix

* add a few tests

* add test_default_version

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

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

* check if minor is empty and push zero instead of shadowing

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-01 18:18:05 +03:00
Mukunda Rao Katta 2288789f33 fix: allow closing brace as command terminator (#427) 2026-08-01 14:03:34 +03:00
Pranav Ram Joshi 84ae1b07a7 Implement e (execute) command (#481)
* chore: fix typo

* add: standalone e command

Implement the 'e' command. No argument implies execution of pattern space and any respective argument is to run with the output written to stream. Rejected under --posix and --sandbox.

Also stop silently swallowing shell stderr for e and s///e.

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

* add: e command tests

Includes 17 new tests. Cover both forms of the new 'e' command: with-argument execution (including shell side effects, addressing, ';' not separating the argument, escape decoding, and backslash continuation) and the no-argument pattern-space form. Also cover --posix/--sandbox rejection and stderr surfacing on command failure.

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

* chore: gate LF-related tests to UNIIX only

* address e command review

Validate shell output as utf-8 and error out instead of lossily converting it. Make CRLF strip conditionally compile to Windows only. Rename execute_pattern_as_shell to execute_pattern_as_shell_command. Add tests for non-utf-8 error paths.

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

* add: Windows tests for e command with argument

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

* fix: broken pipe race in sandbox e command test

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

* Apply s/// p and e flags in written order

GNU sed applies the 'p' and 'e' substitution flags in the order they appear. 'pe' prints the pre-execution text then executes, while 'ep' executes then prints the result. Record whether 'p' precedes 'e' and apply them accordingly.

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

---------

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>
2026-08-01 13:29:42 +03:00
renovate[bot] f32296fc22 Update Rust crate clap to v4.6.5 2026-08-01 07:12:40 +02:00
renovate[bot] 7239fb0e08 Update Rust crate ctor to v1.0.12 2026-07-30 07:57:26 +02:00
renovate[bot] c7e1f78486 Update mozilla-actions/sccache-action action to v0.0.11 2026-07-29 15:19:54 +02:00
renovate[bot] fb907703a0 Update CodSpeedHQ/action action to v5 2026-07-29 14:01:03 +02:00
renovate[bot] 3d52077d7f Update Rust crate clap_complete to v4.6.8 2026-07-27 17:37:39 +02:00
Diomidis Spinellis e9792a8ef9 Improve GNU compatibility through byte processing (#487)
The original Rust implementation assummed a UTF-8 environment, as is
common in modern systems.  This commit improves GNU sed compatibility
by changing most processing to run on bytes rather than Rust's Unicode
characters and strings. This change also improves the handling of binary
files. The commit adds a simple POSIX-compatible locale configuration
to support UTF-8 processing when specified. While at it, also fix
script-specified file handling to take place through OsString and
add a few missing unit tests.

The change fixes two failing GNU test suite cases.
2026-07-26 19:44:54 +02:00
renovate[bot] c732b60cfc Update Rust crate ctor to v1.0.11 (#501)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-26 19:43:52 +02:00
renovate[bot] c1e5d8ea08 Update Rust crate ctor to v1.0.10 2026-07-22 08:23:11 +02:00
renovate[bot] 6887201cce Update Rust crate clap to v4.6.4 2026-07-22 08:09:28 +02:00
renovate[bot] 75d856e4c2 Update Rust crate libc to v0.2.189 2026-07-22 08:00:30 +02:00
renovate[bot] d7a2106258 Update Rust crate libc to v0.2.188 2026-07-21 14:00:30 +02:00
renovate[bot] a726ac6959 Update Rust crate clap to v4.6.3 2026-07-21 08:13:36 +02:00
renovate[bot] 49590e63be Update Rust crate libc to v0.2.187 2026-07-21 08:03:39 +02:00
renovate[bot] 99a9957e89 Update Rust crate ctor to v1.0.9 2026-07-16 08:48:11 +02:00
renovate[bot] c521271ca8 Update Rust crate regex to v1.13.1 2026-07-16 08:19:19 +02:00
renovate[bot] 7830c20bc1 Update Rust crate clap to v4.6.2 2026-07-16 08:08:20 +02:00
renovate[bot] b37e23fa98 Update Rust crate regex to v1.13.0 2026-07-10 10:55:46 +02:00
Daniel Hofstetter 07eada8531 Bump crossbeam-epoch from 0.9.18 to 0.9.20 (#486)
https://rustsec.org/advisories/RUSTSEC-2026-0204
2026-07-08 18:57:10 +03:00
renovate[bot] 15def35a2a chore(deps): update rust crate memchr to v2.8.3 2026-07-08 07:50:55 +02:00
renovate[bot] 30607c4ddc chore(deps): update rust crate ctor to v1.0.8 2026-07-06 08:07:50 +02:00
Diomidis Spinellis 01c8412327 Optimize multiline matching (#483)
The multiline matching resulted in a large performance regression due to
the indiscriminate use of the dotall (?s) flag.  Change the regex
selection process to use only the flag when probably needed.
2026-07-05 15:13:53 +02:00
Diomidis SpinellisandMukunda Katta f3a79c5847 Support multiline substitution flag (#482)
While at it fix . matching of embedded newlines.

This commit increases GNU passed tests by 2.

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

* add test for issue 401

* formatting

* add a test for 'm' flag in test_posix_reject_flags

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

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

* Update compiler.rs

* cargo fmt

* add a const variable with the err msg

* Update compiler.rs

* cargo fmt

---------

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

* test: avoid stdin race in z POSIX rejection
2026-07-01 12:51:11 +03:00
Pranav Ram Joshi b14272c1d7 Add e substitution flag
* feat: introduce e substituion for sed

When the s command is requested for sed, the e substitution flag enables execution of the result of substitution as a shell command. It matches the GNU sed behavior.

On src/sed/command.rs, add 'execute' field to 'Substitution' structure.

On src/sed/compiler.rs, handle 'e' in 'compile_subst_flags' and reject it if --posix or --sandbox flag is used in sed.

On src/sed/processor.rs, spawn /bin/sh with -c flag for a successful match and replace the pattern space with the command's standard output stream.

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

* chore: comply with rustfmt

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

* chore: support for Windows command-line

Instead of calling bourne shell, define a new function: shell_command() with two definitions; conditionally compiling to UNIX-like and Windows. On UNIX-like, executes the bourne shell, whereas command prompt is executed on Windows.

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

* chore: check newline for Windows

Unix-like system only uses the newline character whereas Windows uses the carriage return as well as newline. Hence, when working on Windows, we need to truncate the last two characters.

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

* add: 'e' flag tests

Nine new tests have been introduced in regards to new 'e' substitution option. 'test_subst_e_flag_multiline_outuput' is conditionally compiled as printf command is not guaranteed to be present on all Windows systems.

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

* chore: fallback for shell_command

Executing a shell or a command prompt requires appropriate platforms. WASM does not currently support it.

Also fix a linting issue.

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

* chore: linting cleanup

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

---------

Signed-off-by: PranavRJoshi <pranavrjoshi1@gmail.com>
2026-07-01 11:18:02 +03:00
Diomidis Spinellis cd3b10db48 Document uutils Playground 2026-07-01 10:48:54 +03:00
Diomidis Spinellis 48c2f56358 Document r command extension 2026-07-01 10:42:22 +03:00
Diomidis SpinellisandPranavRJoshi dcd2df848a Implement zero-address r command to insert file before first line (#474)
* feat: support zero-address file insertion

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

* Polish the handling of 0r

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

---------

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

No files matched your search

+1 -1
View File
@@ -134,7 +134,7 @@ jobs:
persist-credentials: false
- name: Retrieve reference artifacts
uses: dawidd6/action-download-artifact@v21
uses: dawidd6/action-download-artifact@v23
continue-on-error: true
with:
workflow: GnuTests.yml
+2 -2
View File
@@ -33,7 +33,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.11
- name: Install tools
uses: taiki-e/install-action@v2
@@ -47,7 +47,7 @@ jobs:
cargo codspeed build -p ${{ matrix.benchmark-target.package }}
- name: Run benchmarks for ${{ matrix.benchmark-target.package }}
uses: CodSpeedHQ/action@v4
uses: CodSpeedHQ/action@v5
env:
CODSPEED_LOG: debug
with:
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
components: clippy
- uses: Swatinem/rust-cache@v2
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.10
uses: mozilla-actions/sccache-action@v0.0.11
- name: Initialize workflow variables
id: vars
shell: bash
Generated
+77 -338
View File
File diff suppressed because it is too large. Load diff
+19 -8
View File
@@ -5,8 +5,7 @@
[package]
name = "sed"
version = "0.1.1"
authors = ["uutils developers"]
version = "0.2.0"
license = "MIT"
description = "sed ~ implemented as universal (cross-platform) utils, written in Rust"
default-run = "sed"
@@ -21,6 +20,19 @@ rust-version = "1.88"
build = "build.rs"
# cargo-binstall: fetch the archives published by `dist` in the GitHub releases
# instead of building from source. The layout below has to stay in sync with
# `dist-workspace.toml` (targets) and the archives it produces:
# sed-<target>.tar.xz (zip on Windows), containing a sed-<target>/ directory.
# Note that the release tags carry no `v` prefix, hence `{ version }`.
[package.metadata.binstall]
pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ target }.{ archive-suffix }"
bin-dir = "{ name }-{ target }/{ bin }{ binary-ext }"
pkg-fmt = "txz"
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
pkg-fmt = "zip"
[features]
default = ["feat_common_core"]
@@ -29,7 +41,6 @@ sed = []
[workspace.dependencies]
assert_fs = "1.1.3"
bytesize = "2.0.0"
chrono = { version = "0.4.37", default-features = false, features = [
"clock",
] }
@@ -48,11 +59,11 @@ predicates = "3.1.3"
rand = { version = "0.10.0" }
regex = "1.10.4"
sha2 = "0.11"
sysinfo = "0.38"
tempfile = "3.10.1"
terminal_size = "0.4.2"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
uucore = { version = "0.9.0", features = ["libc"] }
uucore = { version = "0.10.0", features = ["libc"] }
rustix = "1.1.4"
xattr = "1.3.1"
@@ -66,11 +77,11 @@ memmap2.workspace = true
phf = { workspace = true }
predicates = { workspace = true }
regex = { workspace = true }
sysinfo = { workspace = true }
tempfile = { workspace = true }
terminal_size = { workspace = true }
textwrap = { workspace = true }
uucore = { workspace = true }
rustix = { workspace = true }
[dev-dependencies]
assert_fs = { workspace = true }
@@ -85,7 +96,7 @@ regex = { workspace = true }
sha2 = { workspace = true }
tempfile = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals", "benchmark"] }
uutests = "0.9.0"
uutests = "0.10.0"
[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }
@@ -135,7 +146,7 @@ must_use_candidate = "allow" # 56
needless_raw_string_hashes = "allow" # 20
needless_pass_by_value = "allow" # 15
missing_panics_doc = "allow" # 12
cast_possible_truncation = "allow" # 7
# cast_possible_truncation = "allow" # 7
unnecessary_wraps = "allow" # 6
match_wildcard_for_single_variants = "allow" # 4
cast_sign_loss = "allow" # 4
+35 -8
View File
@@ -26,11 +26,18 @@ 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 .
If you have [cargo-binstall](https://github.com/cargo-bins/cargo-binstall),
the released binaries can be installed directly with:
```bash
cargo binstall sed
```
For other platforms, ensure you have Rust installed on your system. You can install Rust through [rustup](https://rustup.rs/).
Clone the repository and build the project using Cargo:
@@ -44,6 +51,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 +92,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.
@@ -89,14 +101,22 @@ cargo test
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.
* The `p` command adds a missing newline.
(POSIX is silent; FreeBSD sed ignores the newline; original sed ignores
the line).
* A `Q` command (optionally followed by an exit code) quits immediately.
* The `q` command can be optionally followed by an exit code.
* An `R` schedules reading the next line from the specified file.
* A `W` command writes to a file the pattern's first line.
* 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.
@@ -109,12 +129,19 @@ cargo test
sequences.
### Incompatibilities
* The input is assumed to be valid UTF-8 (this includes 7-bit ASCII).
If the input is in another code page, consider converting it through UTF-8
in order to avoid errors on invalid UTF-8 sequences and for the correct
handling of regular expressions.
This _sed_ program can also handle arbitrary byte sequences if no part of the
input is treated as string.
* 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.
+25 -139
View File
@@ -245,9 +245,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "console"
version = "0.16.3"
version = "0.16.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
dependencies = [
"encode_unicode",
"libc",
@@ -571,9 +571,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libfuzzer-sys"
@@ -646,15 +646,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -683,25 +674,6 @@ dependencies = [
"autocfg",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -725,9 +697,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 +707,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 +717,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 +727,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 +820,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",
@@ -925,7 +897,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "sed"
version = "0.1.1"
version = "0.2.0"
dependencies = [
"clap",
"clap_complete",
@@ -937,7 +909,7 @@ dependencies = [
"phf_codegen",
"predicates",
"regex",
"sysinfo",
"rustix",
"tempfile",
"terminal_size",
"textwrap",
@@ -1013,9 +985,9 @@ checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "similar"
version = "3.1.1"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16"
checksum = "4f66ca1f7aca2474dc10c942eb22feffc897735f54cd1db90138c2fddb490987"
dependencies = [
"bstr",
]
@@ -1055,20 +1027,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sysinfo"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -1194,9 +1152,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uucore"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069b34217c27f611e1589f540f58118dbf226a9e407d38ab472052ff075a1dc2"
checksum = "e97dd0fd8bb2ed12affba9f956c3b94b8ad3f9432746717475420e916ae0b8d4"
dependencies = [
"bigdecimal",
"clap",
@@ -1212,6 +1170,7 @@ dependencies = [
"rustix",
"thiserror",
"unic-langid",
"unicode-width",
"uucore_procs",
"wild",
]
@@ -1233,9 +1192,9 @@ dependencies = [
[[package]]
name = "uucore_procs"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34337da211e7abfff7189b794afb3b5018fe356fb36474b73645458fc1201350"
checksum = "907f1b828046ea42cdb4bd898f05b48ab873a41937bf75ebe12f09a5bf411f58"
dependencies = [
"proc-macro2",
"quote",
@@ -1243,9 +1202,9 @@ dependencies = [
[[package]]
name = "uufuzz"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e4192b67d5e7f0a3300db353ec3217beee89442f02c11d8ae108867eee392d0"
checksum = "72d4c5bac0f8ccabaa8e8ccbf2724446f8b007c06ad343fabfe1a1525a4ff4f2"
dependencies = [
"console",
"rand",
@@ -1361,49 +1320,6 @@ dependencies = [
"glob",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -1417,17 +1333,6 @@ dependencies = [
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -1456,16 +1361,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
@@ -1493,15 +1388,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
+2 -2
View File
@@ -14,8 +14,8 @@ libc = "0.2.153"
tempfile = "3.15.0"
rand = { version = "0.10.0" }
similar = "3.0.0"
uucore = { version = "0.9.0", features = ["libc"] }
uufuzz = "0.9.0"
uucore = { version = "0.10.0", features = ["libc"] }
uufuzz = "0.10.0"
sed = { path = ".." }
+163 -82
View File
File diff suppressed because it is too large. Load diff
+821 -181
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -14,6 +14,7 @@ use crate::sed::script_line_provider::ScriptLineProvider;
use std::rc::Rc;
use uucore::display::Quotable;
use uucore::error::{UResult, USimpleError};
#[derive(Clone, Debug)]
@@ -108,7 +109,7 @@ pub fn input_runtime_error<T>(
location.input_name,
location.line_number,
location.column_number,
context.input_name,
context.input_name.quote(),
context.line_number,
msg.to_string()
),
+234 -197
View File
File diff suppressed because it is too large. Load diff
+269 -87
View File
File diff suppressed because it is too large. Load diff
+106 -20
View File
@@ -15,23 +15,25 @@ pub mod error_handling;
pub mod fast_io;
pub mod fast_regex;
pub mod in_place;
pub mod named_writer;
pub mod named_io;
pub mod processor;
pub mod script_char_provider;
pub mod script_line_provider;
use crate::sed::command::{ProcessingContext, StringSpace};
use crate::sed::command::{ByteSpace, CharacterMode, ProcessingContext};
use crate::sed::compiler::compile;
use crate::sed::processor::process_all_files;
use crate::sed::script_line_provider::ScriptValue;
use clap::{Arg, ArgMatches, Command, arg, crate_version};
use clap::{Arg, ArgMatches, Command, arg};
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;
use uucore::error::{UResult, UUsageError};
use uucore::error::{UResult, USimpleError, UUsageError};
use uucore::format_usage;
const ABOUT: &str = "Stream editor for filtering and transforming text";
const ABOUT: &str = "Stream editor for filtering and transforming text (part of uutils)";
const USAGE: &str = "sed [OPTION]... [script] [file]...";
const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (uutils)");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
@@ -46,7 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
}
let (scripts, files) = get_scripts_files(&matches)?;
let mut context = build_context(&matches);
let mut context = build_context(&matches)?;
let executable = compile(scripts, &mut context)?;
process_all_files(executable, files, &mut context)?;
@@ -61,7 +63,7 @@ pub fn uu_app() -> Command {
let util_name = uucore::util_name();
Command::new(util_name)
.version(crate_version!())
.version(VERSION)
.about(ABOUT)
.override_usage(format_usage(USAGE))
.args_override_self(true)
@@ -192,9 +194,40 @@ fn get_scripts_files(matches: &ArgMatches) -> UResult<(Vec<ScriptValue>, Vec<Pat
Ok((scripts, files))
}
// Parse CLI flag arguments and return a ProcessingContext struct based on them
fn build_context(matches: &ArgMatches) -> ProcessingContext {
ProcessingContext {
/// Return the character interpretation mode implied by the process locale.
///
/// The mode is determined from the first non-empty of LC_ALL, LC_CTYPE, and
/// LANG. The C and POSIX locales select byte mode. UTF-8 locales (including
/// C.UTF-8) select UTF-8 mode. All other locales result in an error.
pub fn character_mode_for_locale(locale: &str) -> UResult<CharacterMode> {
if locale == "C" || locale == "POSIX" {
Ok(CharacterMode::Byte)
} else if locale.eq_ignore_ascii_case("C.UTF-8")
|| locale.to_ascii_lowercase().ends_with(".utf-8")
|| locale.to_ascii_lowercase().ends_with(".utf8")
{
Ok(CharacterMode::Utf8)
} else {
Err(USimpleError::new(
1,
format!("unsupported locale: {locale}"),
))
}
}
/// Return a ProcessingContext based on parsed CLI flags and environment.
fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
let locale = ["LC_ALL", "LC_CTYPE", "LANG"]
.into_iter()
.find_map(|name| {
let value = env::var(name).ok()?;
(!value.is_empty()).then_some(value)
})
// Same default as GNU sed
.unwrap_or_else(|| "C".to_string());
Ok(ProcessingContext {
// CLI arguments
all_output_files: matches.get_flag("all-output-files"),
debug: matches.get_flag("debug"),
regex_extended: matches.get_flag("regexp-extended"),
@@ -211,8 +244,11 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
unbuffered: matches.get_flag("unbuffered"),
null_data: matches.get_flag("null-data"),
// Environment
character_mode: character_mode_for_locale(&locale)?,
// Other context
input_name: "<stdin>".to_string(),
input_name: PathBuf::from("-"),
line_number: 0,
last_address: false,
last_line: false,
@@ -220,8 +256,8 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
stop_processing: false,
saved_regex: None,
input_action: None,
hold: StringSpace {
content: String::new(),
hold: ByteSpace {
content: Vec::new(),
has_newline: true,
},
parsed_block_nesting: 0,
@@ -229,7 +265,8 @@ fn build_context(matches: &ArgMatches) -> ProcessingContext {
range_commands: Vec::new(),
substitution_made: false,
append_elements: Vec::new(),
}
rss: processor::RecordSeparatorState::default(),
})
}
#[cfg(test)]
@@ -338,7 +375,7 @@ mod tests {
#[test]
fn test_defaults() {
let matches = test_matches(&[]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(!ctx.all_output_files);
assert!(!ctx.debug);
@@ -373,7 +410,7 @@ mod tests {
"-z",
]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.all_output_files);
assert!(ctx.debug);
@@ -393,7 +430,7 @@ mod tests {
#[test]
fn test_multiple_same_arguments() {
let matches = test_matches(&["-E", "-r"]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.regex_extended);
}
@@ -401,7 +438,7 @@ mod tests {
#[test]
fn test_in_place_with_suffix() {
let matches = test_matches(&["-i", ".bak"]);
let ctx = build_context(&matches);
let ctx = build_context(&matches).unwrap();
assert!(ctx.in_place);
assert_eq!(ctx.in_place_suffix, Some(".bak".to_string()));
@@ -412,10 +449,59 @@ mod tests {
let matches_default = test_matches(&[]);
let matches_custom = test_matches(&["-l", "120"]);
let ctx_default = build_context(&matches_default);
let ctx_custom = build_context(&matches_custom);
let ctx_default = build_context(&matches_default).unwrap();
let ctx_custom = build_context(&matches_custom).unwrap();
assert_eq!(ctx_default.length, 70);
assert_eq!(ctx_custom.length, 120);
}
#[test]
fn c_locale_selects_byte_mode() {
assert_eq!(character_mode_for_locale("C").unwrap(), CharacterMode::Byte);
}
#[test]
fn posix_locale_selects_byte_mode() {
assert_eq!(
character_mode_for_locale("POSIX").unwrap(),
CharacterMode::Byte
);
}
#[test]
fn c_utf8_locale_selects_utf8_mode() {
assert_eq!(
character_mode_for_locale("C.UTF-8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn dot_utf8_locale_selects_utf8_mode() {
assert_eq!(
character_mode_for_locale("en_US.UTF-8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn dot_utf8_locale_is_case_insensitive() {
assert_eq!(
character_mode_for_locale("el_GR.utf8").unwrap(),
CharacterMode::Utf8
);
}
#[test]
fn unsupported_locale_reports_locale_name() {
let err = character_mode_for_locale("el_GR.ISO-8859-7").unwrap_err();
assert_eq!(err.code(), 1);
assert!(
err.to_string()
.contains("unsupported locale: el_GR.ISO-8859-7"),
"{err}"
);
}
}
+324
View File
@@ -0,0 +1,324 @@
// An abstraction for output files created on entry and flushed on exit
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Diomidis Spinellis
//
// This file is part of the uutils sed package.
// It is licensed under the MIT License.
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::sed::error_handling::{ScriptLocation, runtime_error};
use std::cell::RefCell;
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use uucore::display::Quotable;
use uucore::error::UResult;
thread_local! {
/// Readers indexed by canonical input path, used to share state
/// between same-named path reads.
static READERS: RefCell<HashMap<PathBuf, Rc<RefCell<NamedReader>>>> = RefCell::new(HashMap::new());
/// Writers indexed by canonical output path, used to share same-named
/// paths and to flush writes.
static WRITERS: RefCell<HashMap<PathBuf, Rc<RefCell<NamedWriter>>>> = RefCell::new(HashMap::new());
}
#[derive(Debug)]
/// Reader that shares line-by-line state for GNU sed's R command.
pub struct NamedReader {
path: PathBuf,
reader: Option<BufReader<File>>,
done: bool,
}
impl NamedReader {
/// Create or retrieve the reader associated with `path`.
pub fn new(path: PathBuf) -> Rc<RefCell<Self>> {
let canonical_path = fs::canonicalize(&path).unwrap_or(path);
READERS.with(|readers| {
readers
.borrow_mut()
.entry(canonical_path.clone())
.or_insert_with(|| {
Rc::new(RefCell::new(Self {
path: canonical_path,
reader: None,
done: false,
}))
})
.clone()
})
}
/// Return the path associated with this reader.
pub fn original_path(&self) -> &Path {
&self.path
}
/// Read the next line, including its newline. Missing files and read errors
/// are treated as end-of-file, as required by the R command.
pub fn read_line(&mut self) -> Option<Vec<u8>> {
if self.done {
return None;
}
if self.reader.is_none() {
if let Ok(file) = File::open(&self.path) {
self.reader = Some(BufReader::new(file));
} else {
self.done = true;
return None;
}
}
let mut line = Vec::new();
match self.reader.as_mut().unwrap().read_until(b'\n', &mut line) {
Ok(0) | Err(_) => {
self.done = true;
None
}
Ok(_) => Some(line),
}
}
}
#[derive(Debug)]
/// Writer that tracks its file name for better error messages
pub struct NamedWriter {
pub path: PathBuf,
writer: BufWriter<File>,
location: ScriptLocation,
}
impl NamedWriter {
/// Create a new writer, truncate the file, and register it for flushing.
pub fn new(path: PathBuf, location: ScriptLocation) -> UResult<Rc<RefCell<Self>>> {
let canonical_path = canonicalize_output_path(&path, &location)?;
if let Some(writer) = WRITERS.with(|writers| writers.borrow().get(&canonical_path).cloned())
{
return Ok(writer);
}
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.map_err(|e| {
runtime_error::<()>(&location, format!("creating file {}: {}", path.quote(), e))
.unwrap_err()
})?;
let writer = Rc::new(RefCell::new(NamedWriter {
path,
writer: BufWriter::new(file),
location,
}));
WRITERS.with(|writers| {
writers
.borrow_mut()
.insert(canonical_path, Rc::clone(&writer));
});
Ok(writer)
}
/// Return the path used when this writer was first opened.
pub fn original_path(&self) -> &Path {
&self.path
}
/// Write String to the file, possibly with a newline, returning errors.
pub fn write_line(&mut self, line: &str, newline: bool) -> UResult<()> {
self.write_line_bytes(line.as_bytes(), newline)
}
/// Write bytes to the file, possibly with a newline, returning errors.
pub fn write_line_bytes(&mut self, line: &[u8], newline: bool) -> UResult<()> {
self.writer
.write_all(line)
.and_then(|()| {
if newline {
self.writer.write_all(b"\n")
} else {
Ok(())
}
})
.map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
}
/// Flush the writer, returning a descriptive error.
pub fn flush(&mut self) -> UResult<()> {
self.writer.flush().map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {}", self.path.quote(), e),
)
.unwrap_err()
})
}
}
fn canonicalize_output_path(path: &Path, location: &ScriptLocation) -> UResult<PathBuf> {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let canonical_parent = fs::canonicalize(parent).map_err(|e| {
runtime_error::<()>(location, format!("creating file {}: {}", path.quote(), e)).unwrap_err()
})?;
Ok(match path.file_name() {
Some(file_name) => canonical_parent.join(file_name),
None => canonical_parent,
})
}
/// Flush buffered content to all open files, returning descriptive errors.
pub fn flush_all() -> UResult<()> {
WRITERS.with(|writers| {
for writer in writers.borrow().values() {
writer.borrow_mut().flush()?;
}
Ok(())
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::{NamedTempFile, tempdir};
#[test]
fn test_reader_reads_lines_as_bytes() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
fs::write(&path, b"first\nsecond\xE9").unwrap();
let reader = NamedReader::new(path);
assert_eq!(reader.borrow_mut().read_line(), Some(b"first\n".to_vec()));
assert_eq!(
reader.borrow_mut().read_line(),
Some(b"second\xE9".to_vec())
);
assert_eq!(reader.borrow_mut().read_line(), None);
assert_eq!(reader.borrow_mut().read_line(), None);
}
#[test]
fn test_new_reuses_reader_and_shared_position_for_same_path() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
fs::write(&path, b"first\nsecond\n").unwrap();
let first = NamedReader::new(path.clone());
let second = NamedReader::new(path);
assert!(Rc::ptr_eq(&first, &second));
assert_eq!(first.borrow_mut().read_line(), Some(b"first\n".to_vec()));
assert_eq!(second.borrow_mut().read_line(), Some(b"second\n".to_vec()));
}
#[test]
fn test_new_reuses_reader_for_canonical_duplicate_path() {
let dir = tempdir().unwrap();
let path = dir.path().join("input");
fs::write(&path, b"first\nsecond\n").unwrap();
let duplicate_path = dir.path().join(".").join("input");
let first = NamedReader::new(path.clone());
let second = NamedReader::new(duplicate_path);
assert!(Rc::ptr_eq(&first, &second));
assert_eq!(
first.borrow().original_path(),
fs::canonicalize(path).unwrap()
);
assert_eq!(first.borrow_mut().read_line(), Some(b"first\n".to_vec()));
assert_eq!(second.borrow_mut().read_line(), Some(b"second\n".to_vec()));
}
#[test]
fn test_reader_silently_ignores_missing_file() {
let dir = tempdir().unwrap();
let reader = NamedReader::new(dir.path().join("missing"));
assert_eq!(reader.borrow_mut().read_line(), None);
assert_eq!(reader.borrow_mut().read_line(), None);
}
#[test]
fn test_write_line_bytes_appends_newline() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let writer = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
writer
.borrow_mut()
.write_line_bytes(b"a\xE9", true)
.unwrap();
writer.borrow_mut().flush().unwrap();
assert_eq!(fs::read(path).unwrap(), b"a\xE9\n");
}
#[test]
fn test_write_line_bytes_appends_no_newline() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let writer = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
writer
.borrow_mut()
.write_line_bytes(b"a\xE9", false)
.unwrap();
writer.borrow_mut().flush().unwrap();
assert_eq!(fs::read(path).unwrap(), b"a\xE9");
}
#[test]
fn test_new_reuses_writer_for_same_path() {
let file = NamedTempFile::new().unwrap();
let path = file.path().to_path_buf();
let first = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
let second = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
assert!(Rc::ptr_eq(&first, &second));
assert_eq!(first.borrow().original_path(), path.as_path());
first.borrow_mut().write_line("first", true).unwrap();
second.borrow_mut().write_line("second", true).unwrap();
first.borrow_mut().flush().unwrap();
assert_eq!(fs::read_to_string(path).unwrap(), "first\nsecond\n");
}
#[test]
fn test_new_reuses_writer_for_canonical_duplicate_path() {
let dir = tempdir().unwrap();
let subdir = dir.path().join("subdir");
fs::create_dir(&subdir).unwrap();
let path = subdir.join("output");
let duplicate_path = subdir.join(".").join("output");
let first = NamedWriter::new(path.clone(), ScriptLocation::default()).unwrap();
let second = NamedWriter::new(duplicate_path, ScriptLocation::default()).unwrap();
assert!(Rc::ptr_eq(&first, &second));
assert_eq!(first.borrow().original_path(), path.as_path());
}
}
-90
View File
@@ -1,90 +0,0 @@
// An abstraction for output files created on entry and flushed on exit
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Diomidis Spinellis
//
// This file is part of the uutils sed package.
// It is licensed under the MIT License.
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::sed::error_handling::{ScriptLocation, runtime_error};
use std::cell::RefCell;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::rc::Rc;
use uucore::display::Quotable;
use uucore::error::UResult;
thread_local! {
/// Global list of all writers that should be flushed at shutdown
static FLUSH_LIST: RefCell<Vec<Rc<RefCell<NamedWriter>>>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug)]
/// Writer that tracks its file name for better error messages
pub struct NamedWriter {
pub path: PathBuf,
writer: BufWriter<File>,
location: ScriptLocation,
}
impl NamedWriter {
/// Create a new writer, truncate the file, and register it for flushing.
pub fn new(path: PathBuf, location: ScriptLocation) -> UResult<Rc<RefCell<Self>>> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)
.map_err(|e| {
runtime_error::<()>(&location, format!("creating file {}: {}", path.quote(), e))
.unwrap_err()
})?;
let writer = Rc::new(RefCell::new(NamedWriter {
path,
writer: BufWriter::new(file),
location,
}));
FLUSH_LIST.with(|list| list.borrow_mut().push(Rc::clone(&writer)));
Ok(writer)
}
/// Write a line to the file with a newline, returning descriptive errors.
pub fn write_line(&mut self, line: &str) -> UResult<()> {
writeln!(self.writer, "{line}").map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {e}", self.path.quote()),
)
.unwrap_err()
})
}
/// Flush the writer, returning a descriptive error.
pub fn flush(&mut self) -> UResult<()> {
self.writer.flush().map_err(|e| {
runtime_error::<()>(
&self.location,
format!("writing to file {}: {}", self.path.quote(), e),
)
.unwrap_err()
})
}
}
/// Flush buffered content to the file, returning descriptive errors.
pub fn flush_all() -> UResult<()> {
FLUSH_LIST.with(|cell| {
for handle in cell.borrow().iter() {
handle.borrow_mut().flush()?;
}
Ok(())
})
}
+624 -107
View File
File diff suppressed because it is too large. Load diff
+38 -8
View File
@@ -1,4 +1,4 @@
// Provide the script contents character by character
// Provide the script contents byte by byte
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Diomidis Spinellis
@@ -10,19 +10,20 @@
#[derive(Debug)]
pub struct ScriptCharProvider {
line: Vec<char>,
line: Vec<u8>,
pos: usize,
}
impl ScriptCharProvider {
pub fn new(line_string: &str) -> Self {
/// Build from raw script bytes.
pub fn new(line: impl AsRef<[u8]>) -> Self {
Self {
line: line_string.chars().collect(),
line: line.as_ref().to_vec(),
pos: 0,
}
}
/// Advances to the next character, if not at end of line.
/// Advances to the next byte, if not at end of line.
pub fn advance(&mut self) {
if self.pos < self.line.len() {
self.pos += 1;
@@ -39,8 +40,13 @@ impl ScriptCharProvider {
self.pos = pos;
}
/// Returns the current character. Panics if out of bounds.
/// Returns the current byte as a character. Panics if out of bounds.
pub fn current(&self) -> char {
char::from(self.line[self.pos])
}
/// Returns the current script byte. Panics if out of bounds.
pub fn current_byte(&self) -> u8 {
self.line[self.pos]
}
@@ -51,8 +57,8 @@ impl ScriptCharProvider {
/// Advances the position past any whitespace characters.
pub fn eat_spaces(&mut self) {
while self.pos < self.line.len() && self.line[self.pos].is_whitespace() {
self.pos += 1;
while self.pos < self.line.len() && self.current().is_whitespace() {
self.advance();
}
}
@@ -139,4 +145,28 @@ mod tests {
assert_eq!(chars.get_pos(), 2);
assert_eq!(chars.current(), 'c');
}
#[test]
fn test_current_for_utf8_bytes() {
let mut chars = ScriptCharProvider::new("αb");
assert_eq!(chars.current(), char::from(0xCE));
assert_eq!(chars.current_byte(), 0xCE);
chars.advance();
assert_eq!(chars.current(), char::from(0xB1));
assert_eq!(chars.current_byte(), 0xB1);
chars.advance();
assert_eq!(chars.current(), 'b');
}
#[test]
fn test_current_for_invalid_utf8_byte() {
let mut chars = ScriptCharProvider::new(vec![0xC2, b'a']);
assert_eq!(chars.current(), char::from(0xC2));
assert_eq!(chars.current_byte(), 0xC2);
chars.advance();
assert_eq!(chars.current(), 'a');
assert_eq!(chars.current_byte(), b'a');
}
}
+22 -11
View File
@@ -69,8 +69,8 @@ impl ScriptLineProvider {
}
/// Return the next script line to process across all scripts.
pub fn next_line(&mut self) -> UResult<Option<String>> {
let mut line = String::new();
pub fn next_line(&mut self) -> UResult<Option<Vec<u8>>> {
let mut line = Vec::new();
loop {
let advance = match &mut self.state {
@@ -82,13 +82,13 @@ impl ScriptLineProvider {
..
} => {
line.clear();
let bytes = reader.read_line(&mut line)?;
let bytes = reader.read_until(b'\n', &mut line)?;
if bytes == 0 {
Some(*index + 1) // finished reading this source
} else {
*line_number += 1;
// Remove trailing newline
if line.ends_with('\n') {
if line.ends_with(b"\n") {
line.pop();
}
return Ok(Some(line));
@@ -114,7 +114,7 @@ impl ScriptLineProvider {
match &self.sources[next_index] {
ScriptValue::StringVal(s) => {
let cursor = std::io::Cursor::new(s.clone());
let cursor = std::io::Cursor::new(s.as_bytes().to_vec());
self.state = State::Active {
index: next_index,
reader: Box::new(BufReader::new(cursor)),
@@ -199,7 +199,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(lines, vec!["line one", "line two", "line three"]);
@@ -216,7 +216,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(lines, vec!["file line 1", "file line 2"]);
@@ -241,7 +241,7 @@ mod tests {
let mut lines = Vec::new();
while let Some(line) = provider.next_line().unwrap() {
lines.push(line.trim_end().to_string());
lines.push(String::from_utf8(line).unwrap().trim_end().to_string());
}
assert_eq!(
@@ -266,7 +266,7 @@ mod tests {
let mut provider = ScriptLineProvider::new(input);
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l1");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l1");
assert_eq!(provider.get_line_number(), 1);
assert_eq!(provider.get_input_name(), "<script argument 1>");
} else {
@@ -274,7 +274,7 @@ mod tests {
}
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l2");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l2");
assert_eq!(provider.get_line_number(), 2);
assert_eq!(provider.get_input_name(), "<script argument 1>");
} else {
@@ -282,11 +282,22 @@ mod tests {
}
if let Some(line) = provider.next_line().unwrap() {
assert_eq!(line.trim(), "l3");
assert_eq!(String::from_utf8(line).unwrap().trim(), "l3");
assert_eq!(provider.get_line_number(), 1);
assert_eq!(provider.get_input_name(), "<script argument 2>");
} else {
panic!("Expected a line");
}
}
#[test]
fn test_file_source_preserves_invalid_utf8_bytes() {
let mut temp_file = NamedTempFile::new().unwrap();
temp_file.write_all(b"s/\xC2\xE7/X/\n").unwrap();
let input = vec![ScriptValue::PathVal(temp_file.path().to_path_buf())];
let mut provider = ScriptLineProvider::new(input);
assert_eq!(provider.next_line().unwrap().unwrap(), b"s/\xC2\xE7/X/");
}
}
Loaded 20 of 41 files, more files were not shown because too many files have changed in this diff. Show more