Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c98e4d68f | ||
|
|
76086c8d9a | ||
|
|
ab2f0a82d8 | ||
|
|
e5474abccf | ||
|
|
961835e509 | ||
|
|
4a73dfb60c | ||
|
|
a1a146a720 | ||
|
|
c46dd6dab6 | ||
|
|
fcab8f1ca7 | ||
|
|
cffecd151f | ||
|
|
0339313d49 | ||
|
|
cffcc0c806 | ||
|
|
643266bc93 | ||
|
|
81cf88e601 | ||
|
|
cc5e214878 | ||
|
|
e4a923ac98 | ||
|
|
77f442ddc2 | ||
|
|
dc2ec59ce7 | ||
|
|
08b8594355 | ||
|
|
f1c5813aa4 | ||
|
|
32537dd5d9 | ||
|
|
b038f8fb2d | ||
|
|
b39a9eaa05 | ||
|
|
7be1f77dda | ||
|
|
e858951814 | ||
|
|
a7a303352e | ||
|
|
76efad4aa1 |
No files matched your search
@@ -49,6 +49,19 @@ jobs:
|
||||
|
||||
# Omit installing part of https://github.com/actions/runner-images/tree/main/images/ubuntu
|
||||
|
||||
### Locales
|
||||
# Several testsuite scripts guard themselves with require_*_locale_ and
|
||||
# silently skip when the locale is missing, which hides both regressions
|
||||
# and improvements in the multibyte code paths.
|
||||
- name: Generate the locales required by the testsuite
|
||||
shell: bash
|
||||
run: |
|
||||
## Generate the locales required by the testsuite
|
||||
sudo localedef -i en_US -f UTF-8 en_US.UTF-8
|
||||
sudo localedef -i ru_RU -f UTF-8 ru_RU.UTF-8
|
||||
sudo localedef -i el_GR -f ISO-8859-7 el_GR.iso88597
|
||||
locale -a
|
||||
|
||||
### Build
|
||||
- name: Build Rust sed binary
|
||||
shell: bash
|
||||
@@ -134,7 +147,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Retrieve reference artifacts
|
||||
uses: dawidd6/action-download-artifact@v23
|
||||
uses: dawidd6/action-download-artifact@v25
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: GnuTests.yml
|
||||
|
||||
@@ -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
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.33.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
Generated
+43
-330
File diff suppressed because it is too large.
Load diff
+7
-3
@@ -58,11 +58,11 @@ phf_codegen = "0.14.0"
|
||||
predicates = "3.1.3"
|
||||
rand = { version = "0.10.0" }
|
||||
regex = "1.10.4"
|
||||
sha2 = "0.11"
|
||||
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.10.0", features = ["libc"] }
|
||||
uucore = { version = "0.12.0", features = ["libc"] }
|
||||
rustix = "1.1.4"
|
||||
xattr = "1.3.1"
|
||||
|
||||
@@ -96,7 +96,7 @@ regex = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
uucore = { workspace = true, features = ["entries", "process", "signals", "benchmark"] }
|
||||
uutests = "0.10.0"
|
||||
uutests = "0.12.0"
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
xattr = { workspace = true }
|
||||
@@ -130,6 +130,10 @@ inherits = "release"
|
||||
opt-level = "z"
|
||||
strip = true
|
||||
|
||||
# The profile that 'dist' will build with
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
|
||||
|
||||
@@ -15,16 +15,17 @@ and other extensions.
|
||||
## Status
|
||||
|
||||
At this state _sed_ implements all [POSIX features](https://pubs.opengroup.org/onlinepubs/9799919799/)
|
||||
and can run correctly the two complex scripts of its integration tests:
|
||||
[hanoi.sed](https://github.com/uutils/sed/blob/main/tests/fixtures/sed/script/hanoi.sed) (solves the Towers of Hanoi puzzle) and
|
||||
and can run correctly the three complex scripts of its integration tests:
|
||||
[hanoi.sed](https://github.com/uutils/sed/blob/main/tests/fixtures/sed/script/hanoi.sed) (solves the Towers of Hanoi puzzle),
|
||||
[mandelbrot.sed](https://github.com/uutils/sed/blob/main/tests/fixtures/sed/script/mandelbrot.sed), (draws the Mandelbrot set) and
|
||||
[math.sed](https://github.com/uutils/sed/blob/main/tests/fixtures/sed/script/math.sed) (implements an arbitrary precision integer math calculator).
|
||||
|
||||
The performance of this Rust implementation is now better than the GNU and FreeBSD implementations for most benchmarked cases.
|
||||
|
||||
Further work aims to:
|
||||
* Adjust buffering on terminal output to match current implementations,
|
||||
* Implement more GNU extensions,
|
||||
* Improve performance where possible.
|
||||
* improve GNU _sed_ compatibility, especially on the regular expression front,
|
||||
* implement more GNU extensions, and
|
||||
* improve performance where possible.
|
||||
|
||||
## Installation and Use
|
||||
|
||||
@@ -87,71 +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.
|
||||
* 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` 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.
|
||||
* The `l` command lists Unicode characters using the `\uXXXX` and `\UXXXXXXXX`
|
||||
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
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ members = ["cargo:."]
|
||||
# Config for 'dist'
|
||||
[dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.30.3"
|
||||
cargo-dist-version = "0.33.0"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
@@ -13,5 +13,5 @@ installers = []
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "plan"
|
||||
# Allow dirty workflows to ignore out-of-date contents
|
||||
# Skip checking whether the specified configuration files are up to date
|
||||
allow-dirty = ["ci"]
|
||||
@@ -0,0 +1 @@
|
||||
book
|
||||
@@ -0,0 +1,7 @@
|
||||
[book]
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "uutils sed Documentation"
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/uutils/sed/tree/main/docs/src"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](index.md)
|
||||
|
||||
* [Extensions and incompatibilities](extensions.md)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Extensions and incompatibilities
|
||||
|
||||
The main goal of the project is compatibility with GNU _sed_, but _sed_ also
|
||||
supports features that GNU _sed_ does not, and differs from it in a few places.
|
||||
Below is a list of these extensions and incompatibilities.
|
||||
|
||||
## Supported GNU extensions
|
||||
* Command-line arguments can be specified in long (`--`) form.
|
||||
* Spaces can precede a regular expression modifier.
|
||||
* `I` can be used in as a synonym for the `i` (case insensitive) substitution
|
||||
flag.
|
||||
* `M` and `m` substitution flags allow multi-line matching.
|
||||
* In addition to `\n`, other escape sequences (octal, hex, C) are supported
|
||||
in the strings of the `y` command.
|
||||
Under POSIX these yield undefined behavior.
|
||||
* The `a`, `c`, and `i` commands do not require an initial backslash,
|
||||
allow text to appear on the same line, and support escape sequences
|
||||
in the specified text.
|
||||
* The `a`, `i`, `=`, `l`, `q` and `r` commands support address range as an extension to POSIX.
|
||||
* The substitution command replacement group `\0` is a synonym for &.
|
||||
* An `F` command outputs the name of the file currently being processed.
|
||||
* A `Q` command (optionally followed by an exit code) quits immediately.
|
||||
* The `q` command can be optionally followed by an exit code.
|
||||
* A `W` command writes to a file the pattern's first line.
|
||||
* An `R` command reads one line at a time from a file.
|
||||
* The `l` command can be optionally followed by the output width.
|
||||
* The `--follow-symlinks` option for in-place editing.
|
||||
* The `--sandbox` option that limits potentially destructive commands.
|
||||
* Address 0 can be used to specify an address range that is already
|
||||
active on line 1 and can finish with the specified regular expression.
|
||||
* Address steps can be specified in the form of start~step and start,~step
|
||||
ranges.
|
||||
* Address 0 can be used in the `r` command to prepend a file.
|
||||
|
||||
## Supported BSD and GNU extensions
|
||||
* The second address in a range can be specified as a relative address with +N.
|
||||
* In-place editing of file with the `-i` flag.
|
||||
|
||||
## New extensions
|
||||
* Unicode characters can be specified in regular expression pattern, replacement
|
||||
and transliteration sequences using `\uXXXX` or `\UXXXXXXXX` sequences.
|
||||
|
||||
## Incompatible extensions
|
||||
The `-U` or `--uutil-extensions` option enables useful extensions or bug fixes
|
||||
that aren't compatible with GNU sed or POSIX.
|
||||
|
||||
* The `l` command lists Unicode characters using the `\uXXXX` and `\UXXXXXXXX`
|
||||
escapes rather than as octal UTF-8 byte sequences.
|
||||
|
||||
## Incompatibilities
|
||||
* Similarly to GNU _sed_, input is processed as raw bytes or as valid UTF-8
|
||||
(this includes 7-bit ASCII) based on the locale as specified by the
|
||||
`LC_ALL`, `LC_CTYPE`, and `LANG` environment variables,
|
||||
with the default being byte processing.
|
||||
However, in contrast with GNU _sed_, other locales (e.g. ISO-8859-1)
|
||||
are not supported. If the input is in another code page or encoding
|
||||
and requires locale-specific processing (e.g. ignore/map case,
|
||||
character classes), consider converting it through UTF-8 to ensure
|
||||
the correct handling of locale-specific regular expressions.
|
||||
This _sed_ program can also handle arbitrary byte sequences
|
||||
if no part of the input requires treating it as a Rust String.
|
||||
* Back-references aren't supported when input is processed as bytes
|
||||
(`LC_ALL=C`).
|
||||
* The command will report an error and fail if duplicate labels are found
|
||||
in the script.
|
||||
This matches the BSD behavior. The GNU version accepts duplicate labels.
|
||||
* The last line (`$`) address is interpreted as the last non-empty line of
|
||||
the last file. If files specified in subsequent arguments until the last
|
||||
one are empty, then the last line condition will never be triggered.
|
||||
This behavior is consistent with the
|
||||
[original implementation](https://github.com/dspinellis/unix-history-repo/blob/Research-V7/usr/src/cmd/sed/sed1.c#L665).
|
||||
* Labels are parsed for alphanumeric characters. The BSD version parses them
|
||||
until the end of the line, preventing ; to be used as a separator.
|
||||
@@ -0,0 +1,10 @@
|
||||
# uutils sed
|
||||
|
||||
_sed_ is a Rust reimplementation of the
|
||||
[sed utility](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/sed.html)
|
||||
with some [GNU sed](https://www.gnu.org/software/sed/manual/sed.html),
|
||||
[FreeBSD sed](https://man.freebsd.org/cgi/man.cgi?sed(1)),
|
||||
and other extensions.
|
||||
|
||||
It is part of the [uutils](https://uutils.github.io/) project. The source code
|
||||
is on [GitHub](https://github.com/uutils/sed).
|
||||
Generated
+11
-12
@@ -245,9 +245,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.4"
|
||||
version = "0.16.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
|
||||
checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
@@ -820,9 +820,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.2"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
@@ -1034,7 +1034,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
@@ -1152,9 +1152,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uucore"
|
||||
version = "0.10.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e97dd0fd8bb2ed12affba9f956c3b94b8ad3f9432746717475420e916ae0b8d4"
|
||||
checksum = "f24a5910ebbf2c0baf486a9125705f8e026a8f7741a3ea8cf5eca3c9257c9bdf"
|
||||
dependencies = [
|
||||
"bigdecimal",
|
||||
"clap",
|
||||
@@ -1167,7 +1167,6 @@ dependencies = [
|
||||
"os_display",
|
||||
"procfs",
|
||||
"rustc-hash",
|
||||
"rustix",
|
||||
"thiserror",
|
||||
"unic-langid",
|
||||
"unicode-width",
|
||||
@@ -1192,9 +1191,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uucore_procs"
|
||||
version = "0.10.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "907f1b828046ea42cdb4bd898f05b48ab873a41937bf75ebe12f09a5bf411f58"
|
||||
checksum = "50aa1f3bba55168888ae6f1f2714ce58897d8020d742d21f474f01e86468fedd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1202,9 +1201,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "uufuzz"
|
||||
version = "0.10.0"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72d4c5bac0f8ccabaa8e8ccbf2724446f8b007c06ad343fabfe1a1525a4ff4f2"
|
||||
checksum = "7147da7bd2de08e6647116e005b0992cc602e34c34972793844a45c4e8648dd9"
|
||||
dependencies = [
|
||||
"console",
|
||||
"rand",
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ libc = "0.2.153"
|
||||
tempfile = "3.15.0"
|
||||
rand = { version = "0.10.0" }
|
||||
similar = "3.0.0"
|
||||
uucore = { version = "0.10.0", features = ["libc"] }
|
||||
uufuzz = "0.10.0"
|
||||
uucore = { version = "0.12.0", features = ["libc"] }
|
||||
uufuzz = "0.12.0"
|
||||
|
||||
sed = { path = ".." }
|
||||
|
||||
|
||||
+10
-9
@@ -10,8 +10,8 @@
|
||||
|
||||
use crate::sed::error_handling::{ScriptLocation, runtime_error};
|
||||
use crate::sed::fast_regex::{Captures, Match, Regex};
|
||||
use crate::sed::named_io::{NamedReader, NamedWriter};
|
||||
use crate::sed::processor::RecordSeparatorState;
|
||||
use crate::sed::named_reader::NamedReader;
|
||||
use crate::sed::named_writer::NamedWriter;
|
||||
use crate::sed::script_char_provider::ScriptCharProvider;
|
||||
use crate::sed::script_line_provider::ScriptLineProvider;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct ProcessingContext {
|
||||
pub sandbox: bool,
|
||||
pub unbuffered: bool,
|
||||
pub null_data: bool,
|
||||
pub uutil_extensions: bool,
|
||||
|
||||
// Other context
|
||||
/// Currently processed input file name (not script)
|
||||
@@ -67,22 +68,22 @@ pub struct ProcessingContext {
|
||||
pub parsed_block_nesting: usize,
|
||||
/// 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
|
||||
/// the same file advance a single cursor, matching GNU sed.
|
||||
pub named_readers: HashMap<PathBuf, Rc<RefCell<NamedReader>>>,
|
||||
/// Commands with a (latchable and resetable) address range
|
||||
pub range_commands: Vec<Rc<RefCell<Command>>>,
|
||||
/// True if a substitution was made as specified in the t command
|
||||
pub substitution_made: bool,
|
||||
/// Elements to append at the end of each command processing cycle
|
||||
pub append_elements: Vec<AppendElement>,
|
||||
/// State used to separate consecutive output records.
|
||||
pub rss: RecordSeparatorState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Elements that shall be appended at the end of each command processing cycle
|
||||
pub enum AppendElement {
|
||||
Text(Rc<[u8]>), // Text from the a command
|
||||
ReaderText(Rc<[u8]>), // One line from the R command
|
||||
Path(PathBuf), // The contents of the specified file path
|
||||
Text(Rc<[u8]>), // The specified text bytes
|
||||
Path(PathBuf), // The contents of the specified file path
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -371,8 +372,8 @@ pub enum CommandData {
|
||||
BranchTarget(Option<Rc<RefCell<Command>>>), // Commands for 'b', 't', 'T', '{'
|
||||
Label(Option<String>), // Label name for 'b', 't', 'T', ':'
|
||||
Path(PathBuf), // File path for 'r'
|
||||
NamedReader(Rc<RefCell<NamedReader>>), // File input for 'R'
|
||||
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w', 'W'
|
||||
NamedReader(Rc<RefCell<NamedReader>>), // Successive file lines for 'R'
|
||||
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w'
|
||||
Number(usize), // Number for 'l', 'q', 'Q' (GNU)
|
||||
Substitution(Box<Substitution>), // Substitute command 's'
|
||||
Text(Rc<[u8]>), // Text for 'a', 'c', 'i'
|
||||
|
||||
+52
-11
@@ -18,7 +18,8 @@ use crate::sed::delimited_parser::{
|
||||
};
|
||||
use crate::sed::error_handling::{ScriptLocation, compilation_error, semantic_error};
|
||||
use crate::sed::fast_regex::Regex;
|
||||
use crate::sed::named_io::{NamedReader, NamedWriter};
|
||||
use crate::sed::named_reader::NamedReader;
|
||||
use crate::sed::named_writer::NamedWriter;
|
||||
use crate::sed::script_char_provider::ScriptCharProvider;
|
||||
use crate::sed::script_line_provider::{ScriptLineProvider, ScriptValue};
|
||||
|
||||
@@ -1107,7 +1108,7 @@ fn compile_empty_command(
|
||||
Ok(CommandHandling::Continue)
|
||||
}
|
||||
|
||||
// Handles r, R
|
||||
// Handles r
|
||||
fn compile_read_file_command(
|
||||
lines: &mut ScriptLineProvider,
|
||||
line: &mut ScriptCharProvider,
|
||||
@@ -1118,11 +1119,29 @@ fn compile_read_file_command(
|
||||
return compilation_error(lines, line, ERR_SANDBOX);
|
||||
}
|
||||
let path = read_file_path(lines, line)?;
|
||||
cmd.data = if cmd.code == 'R' {
|
||||
CommandData::NamedReader(NamedReader::new(path))
|
||||
} else {
|
||||
CommandData::Path(path)
|
||||
};
|
||||
cmd.data = CommandData::Path(path);
|
||||
Ok(CommandHandling::Continue)
|
||||
}
|
||||
|
||||
// Handles R
|
||||
fn compile_read_line_command(
|
||||
lines: &mut ScriptLineProvider,
|
||||
line: &mut ScriptCharProvider,
|
||||
cmd: &mut Command,
|
||||
context: &mut ProcessingContext,
|
||||
) -> UResult<CommandHandling> {
|
||||
if context.sandbox {
|
||||
return compilation_error(lines, line, ERR_SANDBOX);
|
||||
}
|
||||
let path = read_file_path(lines, line)?;
|
||||
// Reuse a reader keyed by path so all `R` commands on the same file share
|
||||
// one cursor (see `ProcessingContext::named_readers`).
|
||||
let reader = context
|
||||
.named_readers
|
||||
.entry(path.clone())
|
||||
.or_insert_with(|| NamedReader::new(path))
|
||||
.clone();
|
||||
cmd.data = CommandData::NamedReader(reader);
|
||||
Ok(CommandHandling::Continue)
|
||||
}
|
||||
|
||||
@@ -1636,15 +1655,13 @@ fn get_cmd_spec(
|
||||
n_addr: 2,
|
||||
handler: compile_execute_command,
|
||||
}),
|
||||
// F is a GNU extension
|
||||
'F' if !posix => Ok(CommandSpec {
|
||||
n_addr: 2,
|
||||
handler: compile_empty_command,
|
||||
}),
|
||||
// R is a GNU extension
|
||||
'R' if !posix => Ok(CommandSpec {
|
||||
n_addr: 2,
|
||||
handler: compile_read_file_command,
|
||||
handler: compile_read_line_command,
|
||||
}),
|
||||
'r' => Ok(CommandSpec {
|
||||
n_addr: if posix { 1 } else { 2 },
|
||||
@@ -1658,7 +1675,6 @@ fn get_cmd_spec(
|
||||
n_addr: 2,
|
||||
handler: compile_label_command,
|
||||
}),
|
||||
// W is a GNU extension
|
||||
'W' if !posix => Ok(CommandSpec {
|
||||
n_addr: 2,
|
||||
handler: compile_write_file_command,
|
||||
@@ -3032,6 +3048,31 @@ mod tests {
|
||||
assert!(err.to_string().contains(ERR_SANDBOX));
|
||||
}
|
||||
|
||||
// compile_read_line_command (R)
|
||||
#[test]
|
||||
fn test_compile_read_line_command_rejected_under_sandbox() {
|
||||
let (mut lines, mut chars) = make_providers("R input.txt");
|
||||
let mut cmd = Command::default();
|
||||
let mut context = ctx();
|
||||
context.sandbox = true;
|
||||
|
||||
let err =
|
||||
compile_read_line_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap_err();
|
||||
assert!(err.to_string().contains(ERR_SANDBOX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compile_read_line_command_sets_named_reader() {
|
||||
let (mut lines, mut chars) = make_providers("R input.txt");
|
||||
let mut cmd = Command::default();
|
||||
let mut context = ctx();
|
||||
|
||||
let handling =
|
||||
compile_read_line_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap();
|
||||
assert!(matches!(handling, CommandHandling::Continue));
|
||||
assert!(matches!(cmd.data, CommandData::NamedReader(_)));
|
||||
}
|
||||
|
||||
// compile_write_file_command
|
||||
#[test]
|
||||
fn test_compile_write_file_command_rejected_under_sandbox() {
|
||||
|
||||
+94
-1
@@ -592,8 +592,11 @@ pub struct OutputBuffer {
|
||||
max_pending_write: usize, // Max bytes to keep before flushing
|
||||
#[cfg(unix)]
|
||||
mmap_chunk: Option<MmapOutput>, // Chunk to write
|
||||
// True when the last write didn't end with \n; the \n is deferred so
|
||||
// that commands like `p` don't emit a spurious newline under -n.
|
||||
pending_newline: bool,
|
||||
#[cfg(test)]
|
||||
low_level_flushes: usize, // Number of system call flushes
|
||||
low_level_flushes: usize, // Number of system call flushes
|
||||
}
|
||||
|
||||
/// Threshold to use buffered writes for output
|
||||
@@ -624,6 +627,7 @@ impl OutputBuffer {
|
||||
pub fn new(w: Box<dyn OutputWrite + 'static>) -> Self {
|
||||
Self {
|
||||
out: BufWriter::new(w),
|
||||
pending_newline: false,
|
||||
#[cfg(test)]
|
||||
low_level_flushes: 0,
|
||||
}
|
||||
@@ -642,6 +646,7 @@ impl OutputBuffer {
|
||||
fast_copy,
|
||||
max_pending_write,
|
||||
mmap_chunk: None,
|
||||
pending_newline: false,
|
||||
#[cfg(test)]
|
||||
low_level_flushes: 0,
|
||||
}
|
||||
@@ -720,6 +725,12 @@ impl OutputBuffer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.pending_newline {
|
||||
self.flush_mmap(WriteRange::Complete)?;
|
||||
self.out.write_all(b"\n")?;
|
||||
self.pending_newline = false;
|
||||
}
|
||||
|
||||
match &new_chunk.content {
|
||||
IOChunkContent::MmapInput {
|
||||
full_span,
|
||||
@@ -765,6 +776,7 @@ impl OutputBuffer {
|
||||
len: new_len,
|
||||
});
|
||||
}
|
||||
self.pending_newline = !new_chunk.is_newline_terminated();
|
||||
}
|
||||
|
||||
IOChunkContent::Owned {
|
||||
@@ -777,6 +789,7 @@ impl OutputBuffer {
|
||||
if *has_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
}
|
||||
self.pending_newline = !has_newline;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -825,6 +838,16 @@ impl OutputBuffer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a deferred newline if the last output didn't end with one.
|
||||
pub fn flush_pending_newline(&mut self) -> io::Result<()> {
|
||||
if self.pending_newline {
|
||||
self.flush_mmap(WriteRange::Complete)?;
|
||||
self.out.write_all(b"\n")?;
|
||||
self.pending_newline = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush everything: pending mmap and buffered data.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
self.flush_mmap(WriteRange::Complete)?; // flush mmap if any
|
||||
@@ -840,6 +863,11 @@ impl OutputBuffer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.pending_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
self.pending_newline = false;
|
||||
}
|
||||
|
||||
match &chunk.content {
|
||||
IOChunkContent::Owned {
|
||||
content,
|
||||
@@ -850,11 +878,21 @@ impl OutputBuffer {
|
||||
if *has_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
}
|
||||
self.pending_newline = !has_newline;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a deferred newline if the last output didn't end with one.
|
||||
pub fn flush_pending_newline(&mut self) -> io::Result<()> {
|
||||
if self.pending_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
self.pending_newline = false;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Flush everything: pending mmap and buffered data.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
self.out.flush() // then flush buffered data
|
||||
@@ -1938,6 +1976,7 @@ mod tests {
|
||||
max_pending_write: 8,
|
||||
#[cfg(unix)]
|
||||
mmap_chunk: None,
|
||||
pending_newline: false,
|
||||
low_level_flushes: 0,
|
||||
};
|
||||
(buf, file)
|
||||
@@ -2058,4 +2097,58 @@ mod tests {
|
||||
|
||||
assert_eq!(out, "world\n");
|
||||
}
|
||||
|
||||
// pending_newline is injected between two no-newline chunks
|
||||
#[test]
|
||||
fn pending_newline_injected_between_chunks() {
|
||||
let (mut buf, mut file) = new_for_test();
|
||||
buf.write_chunk(&make_owned_chunk("first", false)).unwrap();
|
||||
buf.write_chunk(&make_owned_chunk("second", true)).unwrap();
|
||||
buf.out.flush().unwrap();
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut out = String::new();
|
||||
file.read_to_string(&mut out).unwrap();
|
||||
assert_eq!(out, "first\nsecond\n");
|
||||
}
|
||||
|
||||
// flush_pending_newline emits the deferred newline
|
||||
#[test]
|
||||
fn flush_pending_newline_emits_newline() {
|
||||
let (mut buf, mut file) = new_for_test();
|
||||
buf.write_chunk(&make_owned_chunk("foo", false)).unwrap();
|
||||
assert!(buf.pending_newline);
|
||||
buf.flush_pending_newline().unwrap();
|
||||
assert!(!buf.pending_newline);
|
||||
buf.out.flush().unwrap();
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut out = String::new();
|
||||
file.read_to_string(&mut out).unwrap();
|
||||
assert_eq!(out, "foo\n");
|
||||
}
|
||||
|
||||
// write_str strips trailing newline and sets pending_newline correctly
|
||||
#[test]
|
||||
fn write_str_with_trailing_newline() {
|
||||
let (mut buf, mut file) = new_for_test();
|
||||
buf.write_str("bar\n").unwrap();
|
||||
assert!(!buf.pending_newline);
|
||||
buf.out.flush().unwrap();
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut out = String::new();
|
||||
file.read_to_string(&mut out).unwrap();
|
||||
assert_eq!(out, "bar\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_str_without_trailing_newline() {
|
||||
let (mut buf, mut file) = new_for_test();
|
||||
buf.write_str("baz").unwrap();
|
||||
assert!(buf.pending_newline);
|
||||
buf.flush_pending_newline().unwrap();
|
||||
buf.out.flush().unwrap();
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut out = String::new();
|
||||
file.read_to_string(&mut out).unwrap();
|
||||
assert_eq!(out, "baz\n");
|
||||
}
|
||||
}
|
||||
+13
-3
@@ -15,7 +15,8 @@ pub mod error_handling;
|
||||
pub mod fast_io;
|
||||
pub mod fast_regex;
|
||||
pub mod in_place;
|
||||
pub mod named_io;
|
||||
pub mod named_reader;
|
||||
pub mod named_writer;
|
||||
pub mod processor;
|
||||
pub mod script_char_provider;
|
||||
pub mod script_line_provider;
|
||||
@@ -114,6 +115,11 @@ pub fn uu_app() -> Command {
|
||||
arg!(-s --separate "Consider files as separate rather than as a long stream."),
|
||||
arg!(--sandbox "Operate in a sandbox by disabling e/r/w commands."),
|
||||
arg!(-u --unbuffered "Load minimal input data and flush output buffers regularly."),
|
||||
Arg::new("uutil-extensions")
|
||||
.short('U')
|
||||
.long("uutil-extensions")
|
||||
.help("Enable incompatible extensions.")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
Arg::new("null-data")
|
||||
.short('z')
|
||||
.long("null-data")
|
||||
@@ -239,10 +245,11 @@ fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
|
||||
length: matches.get_one::<u32>("length").map_or(70, |v| *v as usize),
|
||||
quiet: matches.get_flag("quiet"),
|
||||
posix: matches.get_flag("posix"),
|
||||
separate: matches.get_flag("separate"),
|
||||
separate: matches.get_flag("separate") || matches.contains_id("in-place"),
|
||||
sandbox: matches.get_flag("sandbox"),
|
||||
unbuffered: matches.get_flag("unbuffered"),
|
||||
null_data: matches.get_flag("null-data"),
|
||||
uutil_extensions: matches.get_flag("uutil-extensions"),
|
||||
|
||||
// Environment
|
||||
character_mode: character_mode_for_locale(&locale)?,
|
||||
@@ -262,10 +269,10 @@ fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
|
||||
},
|
||||
parsed_block_nesting: 0,
|
||||
label_to_command_map: HashMap::new(),
|
||||
named_readers: HashMap::new(),
|
||||
range_commands: Vec::new(),
|
||||
substitution_made: false,
|
||||
append_elements: Vec::new(),
|
||||
rss: processor::RecordSeparatorState::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -390,6 +397,7 @@ mod tests {
|
||||
assert!(!ctx.sandbox);
|
||||
assert!(!ctx.unbuffered);
|
||||
assert!(!ctx.null_data);
|
||||
assert!(!ctx.uutil_extensions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -407,6 +415,7 @@ mod tests {
|
||||
"-s",
|
||||
"--sandbox",
|
||||
"-u",
|
||||
"-U",
|
||||
"-z",
|
||||
]);
|
||||
|
||||
@@ -425,6 +434,7 @@ mod tests {
|
||||
assert!(ctx.sandbox);
|
||||
assert!(ctx.unbuffered);
|
||||
assert!(ctx.null_data);
|
||||
assert!(ctx.uutil_extensions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,324 +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::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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// An abstraction for input files read one line at a time by the `R` command
|
||||
//
|
||||
// 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 std::cell::RefCell;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// State of the file backing an `R` command, opened lazily on first use.
|
||||
enum State {
|
||||
Unopened,
|
||||
Open(BufReader<File>),
|
||||
Exhausted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Reader that yields successive lines of a file for the GNU `R` command.
|
||||
/// The file is opened on first use; a file that cannot be opened or read is
|
||||
/// treated as having no more lines, matching GNU sed (no error is raised).
|
||||
pub struct NamedReader {
|
||||
path: PathBuf,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl NamedReader {
|
||||
/// Create a reader for `path` without opening it yet.
|
||||
pub fn new(path: PathBuf) -> Rc<RefCell<Self>> {
|
||||
Rc::new(RefCell::new(NamedReader {
|
||||
path,
|
||||
state: State::Unopened,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return the next line of the file, including its trailing newline if
|
||||
/// present, or `None` once the file is exhausted or could not be read.
|
||||
pub fn next_line(&mut self) -> Option<Vec<u8>> {
|
||||
if matches!(self.state, State::Unopened) {
|
||||
self.state = match File::open(&self.path) {
|
||||
Ok(file) => State::Open(BufReader::new(file)),
|
||||
Err(_) => State::Exhausted,
|
||||
};
|
||||
}
|
||||
|
||||
let State::Open(reader) = &mut self.state else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut line = Vec::new();
|
||||
match reader.read_until(b'\n', &mut line) {
|
||||
Ok(0) | Err(_) => {
|
||||
self.state = State::Exhausted;
|
||||
None
|
||||
}
|
||||
Ok(_) => Some(line),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn yields_successive_lines_then_none() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(b"one\ntwo\n").unwrap();
|
||||
let reader = NamedReader::new(file.path().to_path_buf());
|
||||
|
||||
assert_eq!(reader.borrow_mut().next_line(), Some(b"one\n".to_vec()));
|
||||
assert_eq!(reader.borrow_mut().next_line(), Some(b"two\n".to_vec()));
|
||||
assert_eq!(reader.borrow_mut().next_line(), None);
|
||||
assert_eq!(reader.borrow_mut().next_line(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_line_without_newline_is_preserved() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(b"abc").unwrap();
|
||||
let reader = NamedReader::new(file.path().to_path_buf());
|
||||
|
||||
assert_eq!(reader.borrow_mut().next_line(), Some(b"abc".to_vec()));
|
||||
assert_eq!(reader.borrow_mut().next_line(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_yields_no_lines() {
|
||||
let reader = NamedReader::new(PathBuf::from("/nonexistent/xyzzy-42-does-not-exist"));
|
||||
assert_eq!(reader.borrow_mut().next_line(), None);
|
||||
}
|
||||
}
|
||||
Loaded 20 of 37 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user