Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95cbd302fe | ||
|
|
1e390a63c0 | ||
|
|
452e249296 | ||
|
|
c8b5dc53e3 | ||
|
|
714ec8a7bf |
No files matched your search
@@ -49,19 +49,6 @@ 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
|
||||
@@ -147,7 +134,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Retrieve reference artifacts
|
||||
uses: dawidd6/action-download-artifact@v27
|
||||
uses: dawidd6/action-download-artifact@v23
|
||||
continue-on-error: true
|
||||
with:
|
||||
workflow: GnuTests.yml
|
||||
|
||||
@@ -97,12 +97,12 @@ jobs:
|
||||
RUSTC_WRAPPER: ""
|
||||
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||
LLVM_PROFILE_FILE: "${{ github.workspace }}/sed-%p-%m.profraw"
|
||||
LLVM_PROFILE_FILE: "sed-%p-%m.profraw"
|
||||
- name: "`grcov` ~ install"
|
||||
id: build_grcov
|
||||
shell: bash
|
||||
run: |
|
||||
git clone --depth=1 https://github.com/mozilla/grcov.git ~/grcov/
|
||||
git clone https://github.com/mozilla/grcov.git ~/grcov/
|
||||
cd ~/grcov
|
||||
cargo install --path .
|
||||
cd -
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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.33.0/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
Generated
+640
-439
File diff suppressed because it is too large.
Load diff
+9
-12
@@ -1,12 +1,11 @@
|
||||
# sed (uutils)
|
||||
# * see the repository LICENSE, README, and CONTRIBUTING files for more information
|
||||
|
||||
# spell-checker:ignore (libs) mangen sed binstall codspeed libc memchr memmap rlimit rustix textwrap uucore uutests xattr
|
||||
# spell-checker:ignore (misc) clippy codegen msvc startswith uninlined uutils
|
||||
# spell-checker:ignore (libs) mangen sed
|
||||
|
||||
[package]
|
||||
name = "sed"
|
||||
version = "0.3.0"
|
||||
version = "0.2.0"
|
||||
license = "MIT"
|
||||
description = "sed ~ implemented as universal (cross-platform) utils, written in Rust"
|
||||
default-run = "sed"
|
||||
@@ -42,6 +41,9 @@ sed = []
|
||||
|
||||
[workspace.dependencies]
|
||||
assert_fs = "1.1.3"
|
||||
chrono = { version = "0.4.37", default-features = false, features = [
|
||||
"clock",
|
||||
] }
|
||||
clap = { version = "4.4", features = ["wrap_help", "cargo"] }
|
||||
clap_complete = "4.5"
|
||||
clap_mangen = "0.3"
|
||||
@@ -56,12 +58,11 @@ phf_codegen = "0.14.0"
|
||||
predicates = "3.1.3"
|
||||
rand = { version = "0.10.0" }
|
||||
regex = "1.10.4"
|
||||
sha2 = { version = "0.11.0", default-features = false, features = ["alloc"] }
|
||||
sha2 = "0.11"
|
||||
tempfile = "3.10.1"
|
||||
terminal_size = "0.4.2"
|
||||
textwrap = { version = "0.16.1", features = ["terminal_size"] }
|
||||
unicode-width = "0.2.2"
|
||||
uucore = { version = "0.12.0", features = ["diagnostics", "libc"] }
|
||||
uucore = { version = "0.10.0", features = ["libc"] }
|
||||
rustix = "1.1.4"
|
||||
xattr = "1.3.1"
|
||||
|
||||
@@ -79,12 +80,12 @@ regex = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
terminal_size = { workspace = true }
|
||||
textwrap = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
uucore = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_fs = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
ctor = "1.0.0"
|
||||
divan = { workspace = true }
|
||||
hex = { workspace = true }
|
||||
@@ -95,7 +96,7 @@ regex = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
uucore = { workspace = true, features = ["entries", "process", "signals", "benchmark"] }
|
||||
uutests = "0.12.0"
|
||||
uutests = "0.10.0"
|
||||
|
||||
[target.'cfg(unix)'.dev-dependencies]
|
||||
xattr = { workspace = true }
|
||||
@@ -129,10 +130,6 @@ 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,17 +15,16 @@ and other extensions.
|
||||
## Status
|
||||
|
||||
At this state _sed_ implements all [POSIX features](https://pubs.opengroup.org/onlinepubs/9799919799/)
|
||||
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
|
||||
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
|
||||
[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:
|
||||
* improve GNU _sed_ compatibility, especially on the regular expression front,
|
||||
* implement more GNU extensions, and
|
||||
* improve performance where possible.
|
||||
* Adjust buffering on terminal output to match current implementations,
|
||||
* Implement more GNU extensions,
|
||||
* Improve performance where possible.
|
||||
|
||||
## Installation and Use
|
||||
|
||||
@@ -88,8 +87,71 @@ cargo test
|
||||
```
|
||||
|
||||
## Extensions and incompatibilities
|
||||
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).
|
||||
### 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.
|
||||
|
||||
## 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.33.0"
|
||||
cargo-dist-version = "0.30.3"
|
||||
# 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"
|
||||
# Skip checking whether the specified configuration files are up to date
|
||||
# Allow dirty workflows to ignore out-of-date contents
|
||||
allow-dirty = ["ci"]
|
||||
@@ -1 +0,0 @@
|
||||
book
|
||||
@@ -1,7 +0,0 @@
|
||||
[book]
|
||||
language = "en"
|
||||
src = "src"
|
||||
title = "uutils sed Documentation"
|
||||
|
||||
[output.html]
|
||||
git-repository-url = "https://github.com/uutils/sed/tree/main/docs/src"
|
||||
@@ -1,5 +0,0 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](index.md)
|
||||
|
||||
* [Extensions and incompatibilities](extensions.md)
|
||||
@@ -1,86 +0,0 @@
|
||||
# Extensions and incompatibilities
|
||||
|
||||
The main goal of the project is compatibility with GNU _sed_, but _sed_ also
|
||||
supports features that GNU _sed_ does not, and differs from it in a few places.
|
||||
Below is a list of these extensions and incompatibilities.
|
||||
|
||||
## Supported GNU extensions
|
||||
* Command-line arguments can be specified in long (`--`) form.
|
||||
* Spaces can precede a regular expression modifier.
|
||||
* `I` can be used in as a synonym for the `i` (case insensitive) substitution
|
||||
flag.
|
||||
* `M` and `m` substitution flags allow multi-line matching.
|
||||
* In addition to `\n`, other escape sequences (octal, hex, C) are supported
|
||||
in the strings of the `y` command.
|
||||
Under POSIX these yield undefined behavior.
|
||||
* The `a`, `c`, and `i` commands do not require an initial backslash,
|
||||
allow text to appear on the same line, and support escape sequences
|
||||
in the specified text.
|
||||
* The `a`, `i`, `=`, `l`, `q` and `r` commands support address range as an extension to POSIX.
|
||||
* The substitution command replacement group `\0` is a synonym for &.
|
||||
* An `F` command outputs the name of the file currently being processed.
|
||||
* A `Q` command (optionally followed by an exit code) quits immediately.
|
||||
* The `q` command can be optionally followed by an exit code.
|
||||
* A `W` command writes to a file the pattern's first line.
|
||||
* An `R` command reads one line at a time from a file.
|
||||
* The `l` command can be optionally followed by the output width.
|
||||
* The `--follow-symlinks` option for in-place editing.
|
||||
* The `--sandbox` option that limits potentially destructive commands.
|
||||
* Address 0 can be used to specify an address range that is already
|
||||
active on line 1 and can finish with the specified regular expression.
|
||||
* Address steps can be specified in the form of start~step and start,~step
|
||||
ranges.
|
||||
* Address 0 can be used in the `r` command to prepend a file.
|
||||
|
||||
## Supported BSD and GNU extensions
|
||||
* The second address in a range can be specified as a relative address with +N.
|
||||
* In-place editing of file with the `-i` flag.
|
||||
As in GNU _sed_, a backup suffix must be attached (`-i.bak`,
|
||||
`--in-place=.bak`); BSD's `-i .bak` and `-i ''` are not supported.
|
||||
|
||||
## New extensions
|
||||
* Unicode characters can be specified in regular expression pattern, replacement
|
||||
and transliteration sequences using `\uXXXX` or `\UXXXXXXXX` sequences.
|
||||
* On a terminal, script errors also quote the script line and underline the
|
||||
character at fault (`UUTILS_DIAG=always` or `never` overrides this):
|
||||
```
|
||||
$ sed 's/a/b/q'
|
||||
sed: <script argument 1>:1:7: error: invalid substitute flag: 'q'
|
||||
â•─[ <script argument 1>:1:7 ]
|
||||
│
|
||||
1 │ s/a/b/q
|
||||
│ ─
|
||||
───╯
|
||||
```
|
||||
|
||||
## Incompatible extensions
|
||||
The `-U` or `--uutil-extensions` option enables useful extensions or bug fixes
|
||||
that aren't compatible with GNU sed or POSIX.
|
||||
|
||||
* The `l` command lists Unicode characters using the `\uXXXX` and `\UXXXXXXXX`
|
||||
escapes rather than as octal UTF-8 byte sequences.
|
||||
|
||||
## Incompatibilities
|
||||
* Similarly to GNU _sed_, input is processed as raw bytes or as valid UTF-8
|
||||
(this includes 7-bit ASCII) based on the locale as specified by the
|
||||
`LC_ALL`, `LC_CTYPE`, and `LANG` environment variables,
|
||||
with the default being byte processing.
|
||||
However, in contrast with GNU _sed_, other locales (e.g. ISO-8859-1)
|
||||
are not supported. If the input is in another code page or encoding
|
||||
and requires locale-specific processing (e.g. ignore/map case,
|
||||
character classes), consider converting it through UTF-8 to ensure
|
||||
the correct handling of locale-specific regular expressions.
|
||||
This _sed_ program can also handle arbitrary byte sequences
|
||||
if no part of the input requires treating it as a Rust String.
|
||||
* Back-references aren't supported when input is processed as bytes
|
||||
(`LC_ALL=C`).
|
||||
* The command will report an error and fail if duplicate labels are found
|
||||
in the script.
|
||||
This matches the BSD behavior. The GNU version accepts duplicate labels.
|
||||
* The last line (`$`) address is interpreted as the last non-empty line of
|
||||
the last file. If files specified in subsequent arguments until the last
|
||||
one are empty, then the last line condition will never be triggered.
|
||||
This behavior is consistent with the
|
||||
[original implementation](https://github.com/dspinellis/unix-history-repo/blob/Research-V7/usr/src/cmd/sed/sed1.c#L665).
|
||||
* Labels are parsed for alphanumeric characters. The BSD version parses them
|
||||
until the end of the line, preventing ; to be used as a separator.
|
||||
@@ -1,10 +0,0 @@
|
||||
# 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
+402
-386
File diff suppressed because it is too large.
Load diff
+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.12.0", features = ["libc"] }
|
||||
uufuzz = "0.12.0"
|
||||
uucore = { version = "0.10.0", features = ["libc"] }
|
||||
uufuzz = "0.10.0"
|
||||
|
||||
sed = { path = ".." }
|
||||
|
||||
|
||||
+13
-27
@@ -10,8 +10,8 @@
|
||||
|
||||
use crate::sed::error_handling::{ScriptLocation, runtime_error};
|
||||
use crate::sed::fast_regex::{Captures, Match, Regex};
|
||||
use crate::sed::named_reader::NamedReader;
|
||||
use crate::sed::named_writer::NamedWriter;
|
||||
use crate::sed::named_io::{NamedReader, NamedWriter};
|
||||
use crate::sed::processor::RecordSeparatorState;
|
||||
use crate::sed::script_char_provider::ScriptCharProvider;
|
||||
use crate::sed::script_line_provider::ScriptLineProvider;
|
||||
|
||||
@@ -39,7 +39,6 @@ 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)
|
||||
@@ -54,8 +53,6 @@ pub struct ProcessingContext {
|
||||
pub last_file: bool,
|
||||
/// Stop processing further input.
|
||||
pub stop_processing: bool,
|
||||
/// Stop without writing a missing final newline (`Q`).
|
||||
pub quit_silently: bool,
|
||||
/// Whether sed operates on bytes or UTF-8 characters
|
||||
pub character_mode: CharacterMode,
|
||||
/// Previously compiled RE, saved for reuse when specifying an empty RE
|
||||
@@ -66,26 +63,26 @@ pub struct ProcessingContext {
|
||||
pub input_action: Option<InputAction>,
|
||||
/// Hold space
|
||||
pub hold: ByteSpace,
|
||||
/// Location of each `{` still open at compile time, innermost last
|
||||
pub open_blocks: Vec<ScriptLocation>,
|
||||
/// Nesting of { } at compile time
|
||||
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]>), // The specified text bytes
|
||||
Path(PathBuf), // The contents of the specified file path
|
||||
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
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
@@ -336,7 +333,6 @@ pub struct Command {
|
||||
pub addr2: Option<Address>, // End address
|
||||
pub non_select: bool, // True if '!'
|
||||
pub start_line: Option<usize>, // Start line number (or None if unlatched)
|
||||
pub range_started: bool, // True once the range has been entered
|
||||
pub data: CommandData, // Command-specific data
|
||||
pub next: Option<Rc<RefCell<Command>>>, // Pointer to next command
|
||||
pub location: ScriptLocation, // Command's definition location
|
||||
@@ -350,7 +346,6 @@ impl Default for Command {
|
||||
addr2: None,
|
||||
non_select: false,
|
||||
start_line: None,
|
||||
range_started: false,
|
||||
data: CommandData::None,
|
||||
next: None,
|
||||
location: ScriptLocation::default(),
|
||||
@@ -376,8 +371,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>>), // Successive file lines for 'R'
|
||||
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w'
|
||||
NamedReader(Rc<RefCell<NamedReader>>), // File input for 'R'
|
||||
NamedWriter(Rc<RefCell<NamedWriter>>), // File output for 'w', 'W'
|
||||
Number(usize), // Number for 'l', 'q', 'Q' (GNU)
|
||||
Substitution(Box<Substitution>), // Substitute command 's'
|
||||
Text(Rc<[u8]>), // Text for 'a', 'c', 'i'
|
||||
@@ -396,17 +391,8 @@ pub enum SpaceFlag {
|
||||
pub struct InputAction {
|
||||
/// Next command to execute (rather than commands from start)
|
||||
pub next_command: Option<Rc<RefCell<Command>>>,
|
||||
/// How the read line becomes the pattern space
|
||||
pub next_line: NextLine,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// How a line read by `n` or `N` becomes the pattern space
|
||||
pub enum NextLine {
|
||||
/// Replace the pattern space with the line (`n`)
|
||||
Replace,
|
||||
/// Append the line, after a newline, to these contents (`N`)
|
||||
Append(Vec<u8>),
|
||||
/// Data to prepend to the read contents
|
||||
pub prepend: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+49
-106
@@ -18,8 +18,7 @@ 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_reader::NamedReader;
|
||||
use crate::sed::named_writer::NamedWriter;
|
||||
use crate::sed::named_io::{NamedReader, NamedWriter};
|
||||
use crate::sed::script_char_provider::ScriptCharProvider;
|
||||
use crate::sed::script_line_provider::{ScriptLineProvider, ScriptValue};
|
||||
|
||||
@@ -29,7 +28,7 @@ use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use terminal_size::{Width, terminal_size};
|
||||
use uucore::error::UResult;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
|
||||
const DEFAULT_OUTPUT_WIDTH: usize = 60;
|
||||
|
||||
@@ -77,11 +76,6 @@ pub fn compile(
|
||||
#[cfg(any())]
|
||||
dbg!(&result);
|
||||
|
||||
// Like GNU sed, point at the innermost `{` left open.
|
||||
if let Some(location) = context.open_blocks.last() {
|
||||
return semantic_error(location, "unmatched `{'");
|
||||
}
|
||||
|
||||
// Link branch commands to the target label commands.
|
||||
populate_label_map(result.clone(), context)?;
|
||||
populate_range_commands(result.clone(), context);
|
||||
@@ -90,6 +84,9 @@ pub fn compile(
|
||||
// Link the ends of command blocks to their following commands.
|
||||
// This converts the tree into a graph, so it must be the last
|
||||
// conversion that traverses the structure as a tree.
|
||||
if context.parsed_block_nesting > 0 {
|
||||
return Err(USimpleError::new(1, "unmatched `{'"));
|
||||
}
|
||||
patch_block_endings(result.clone());
|
||||
|
||||
Ok(result)
|
||||
@@ -361,7 +358,7 @@ fn compile_address_range(
|
||||
let is_step_match = line.current() == '~'; // E.g. 0~2: Pick even-numbered lines
|
||||
line.advance();
|
||||
line.eat_spaces();
|
||||
let is_step_end = if !line.eol() && line.current() == '~' {
|
||||
let is_step_end = if line.current() == '~' {
|
||||
// E.g. /foo/,~10: Start at foo, include all lines until multiple of 10 is reached.
|
||||
line.advance();
|
||||
line.eat_spaces();
|
||||
@@ -375,46 +372,39 @@ fn compile_address_range(
|
||||
return compilation_error(lines, line, "~step is invalid in POSIX mode");
|
||||
}
|
||||
|
||||
// Look for second address; a missing ~step counts as 0.
|
||||
let has_addr2 = !line.eol() && (is_address_char(line.current()) || line.current() == '+');
|
||||
if !(has_addr2 || is_step_match || is_step_end) {
|
||||
return compilation_error(lines, line, "unexpected `,'");
|
||||
}
|
||||
let addr2 = if has_addr2 {
|
||||
compile_address(lines, line, context)?
|
||||
} else {
|
||||
Address::Line(0)
|
||||
};
|
||||
// Set step_n to the number specified in the (required numeric) address.
|
||||
let step_n = if is_step_match || is_step_end {
|
||||
match addr2 {
|
||||
Address::Line(n) => n,
|
||||
_ => {
|
||||
return compilation_error(
|
||||
lines,
|
||||
line,
|
||||
"~step can only be specified through numeric values",
|
||||
);
|
||||
// Look for second address.
|
||||
if !line.eol() {
|
||||
let addr2 = compile_address(lines, line, context)?;
|
||||
// Set step_n to the number specified in the (required numeric) address.
|
||||
let step_n = if is_step_match || is_step_end {
|
||||
match addr2 {
|
||||
Address::Line(n) => n,
|
||||
_ => {
|
||||
return compilation_error(
|
||||
lines,
|
||||
line,
|
||||
"~step can only be specified through numeric values",
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0 // dummy, not used
|
||||
};
|
||||
|
||||
if is_line0 && !matches!(addr2, Address::Re(_)) && !is_step_match {
|
||||
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
|
||||
}
|
||||
} else {
|
||||
0 // dummy, not used
|
||||
};
|
||||
|
||||
// Address 0 needs a regex end or a positive ~step (0~0 selects no line).
|
||||
if is_line0 && !matches!(addr2, Address::Re(_)) && !(is_step_match && step_n > 0) {
|
||||
return compilation_error(lines, line, ERR_ADDRESS_0_USAGE);
|
||||
// If needed, transform Address::Line into Address::Step*.
|
||||
cmd.addr2 = if is_step_match {
|
||||
Some(Address::StepMatch(step_n))
|
||||
} else if is_step_end {
|
||||
Some(Address::StepEnd(step_n))
|
||||
} else {
|
||||
Some(addr2)
|
||||
};
|
||||
n_addr += 1;
|
||||
}
|
||||
|
||||
// If needed, transform Address::Line into Address::Step*.
|
||||
cmd.addr2 = if is_step_match {
|
||||
Some(Address::StepMatch(step_n))
|
||||
} else if is_step_end {
|
||||
Some(Address::StepEnd(step_n))
|
||||
} else {
|
||||
Some(addr2)
|
||||
};
|
||||
n_addr += 1;
|
||||
}
|
||||
|
||||
// Zero-address read command check
|
||||
@@ -505,7 +495,7 @@ fn compile_address(
|
||||
let number = parse_number(lines, line, true)?.unwrap();
|
||||
Ok(Address::Line(number))
|
||||
}
|
||||
_ => compilation_error(lines, line, "expected context address"),
|
||||
_ => panic!("invalid context address"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,10 +833,6 @@ fn compile_subst_command(
|
||||
) -> UResult<CommandHandling> {
|
||||
line.advance(); // move past 's'
|
||||
|
||||
if line.eol() {
|
||||
return compilation_error(lines, line, "unterminated `s' command");
|
||||
}
|
||||
|
||||
let delimiter = line.current();
|
||||
if delimiter == '\0' || delimiter == '\\' {
|
||||
return compilation_error(
|
||||
@@ -913,10 +899,6 @@ fn compile_trans_command(
|
||||
) -> UResult<CommandHandling> {
|
||||
line.advance(); // move past 'y'
|
||||
|
||||
if line.eol() {
|
||||
return compilation_error(lines, line, "unterminated `y' command");
|
||||
}
|
||||
|
||||
let delimiter = line.current();
|
||||
if delimiter == '\0' || delimiter == '\\' {
|
||||
return compilation_error(
|
||||
@@ -1084,9 +1066,10 @@ fn compile_end_group_command(
|
||||
cmd: &mut Command,
|
||||
context: &mut ProcessingContext,
|
||||
) -> UResult<CommandHandling> {
|
||||
if context.open_blocks.pop().is_none() {
|
||||
if context.parsed_block_nesting == 0 {
|
||||
return compilation_error(lines, line, "unexpected `}'");
|
||||
}
|
||||
context.parsed_block_nesting -= 1;
|
||||
line.advance();
|
||||
line.eat_spaces();
|
||||
parse_command_ending(lines, line, cmd)?;
|
||||
@@ -1124,7 +1107,7 @@ fn compile_empty_command(
|
||||
Ok(CommandHandling::Continue)
|
||||
}
|
||||
|
||||
// Handles r
|
||||
// Handles r, R
|
||||
fn compile_read_file_command(
|
||||
lines: &mut ScriptLineProvider,
|
||||
line: &mut ScriptCharProvider,
|
||||
@@ -1135,29 +1118,11 @@ fn compile_read_file_command(
|
||||
return compilation_error(lines, line, ERR_SANDBOX);
|
||||
}
|
||||
let path = read_file_path(lines, line)?;
|
||||
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);
|
||||
cmd.data = if cmd.code == 'R' {
|
||||
CommandData::NamedReader(NamedReader::new(path))
|
||||
} else {
|
||||
CommandData::Path(path)
|
||||
};
|
||||
Ok(CommandHandling::Continue)
|
||||
}
|
||||
|
||||
@@ -1185,7 +1150,7 @@ fn compile_block_command(
|
||||
context: &mut ProcessingContext,
|
||||
) -> UResult<CommandHandling> {
|
||||
line.advance(); // move past '{'
|
||||
context.open_blocks.push(cmd.location.clone());
|
||||
context.parsed_block_nesting += 1;
|
||||
let block_body = compile_sequence(lines, line, context)?;
|
||||
cmd.data = CommandData::BranchTarget(block_body);
|
||||
Ok(CommandHandling::Continue)
|
||||
@@ -1671,13 +1636,15 @@ 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_line_command,
|
||||
handler: compile_read_file_command,
|
||||
}),
|
||||
'r' => Ok(CommandSpec {
|
||||
n_addr: if posix { 1 } else { 2 },
|
||||
@@ -1691,6 +1658,7 @@ 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,
|
||||
@@ -3064,31 +3032,6 @@ 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() {
|
||||
|
||||
+7
-151
@@ -12,13 +12,8 @@ use crate::sed::command::ProcessingContext;
|
||||
use crate::sed::script_char_provider::ScriptCharProvider;
|
||||
use crate::sed::script_line_provider::ScriptLineProvider;
|
||||
|
||||
use std::env;
|
||||
use std::io::{IsTerminal, stderr};
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
use uucore::diagnostics;
|
||||
use uucore::display::Quotable;
|
||||
use uucore::error::{UResult, USimpleError};
|
||||
|
||||
@@ -28,9 +23,6 @@ pub struct ScriptLocation {
|
||||
pub input_name: Rc<str>, // Shared input name
|
||||
pub line_number: usize, // 1-based line number
|
||||
pub column_number: usize, // 1-based column number
|
||||
/// The script line, for errors raised after it was consumed.
|
||||
/// `None` when diagnostics are off.
|
||||
pub line_text: Option<Rc<str>>,
|
||||
}
|
||||
|
||||
impl Default for ScriptLocation {
|
||||
@@ -39,7 +31,6 @@ impl Default for ScriptLocation {
|
||||
input_name: Rc::from("<unknown>"),
|
||||
line_number: 1,
|
||||
column_number: 1,
|
||||
line_text: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,123 +42,10 @@ impl ScriptLocation {
|
||||
line_number: lines.get_line_number(),
|
||||
column_number: line.get_pos() + 1,
|
||||
input_name: Rc::from(lines.get_input_name()),
|
||||
line_text: if diagnostics_enabled() {
|
||||
line.line_text()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether script errors get a snippet. Evaluated once: a location is
|
||||
/// recorded for every compiled command, and the terminal check is a syscall.
|
||||
fn diagnostics_enabled() -> bool {
|
||||
static ENABLED: OnceLock<bool> = OnceLock::new();
|
||||
*ENABLED.get_or_init(diagnostics::enabled)
|
||||
}
|
||||
|
||||
/// Format `msg` as an error at the given script position, followed by the
|
||||
/// offending script line, underlined, when diagnostics are enabled.
|
||||
fn script_error(
|
||||
input_name: &str,
|
||||
line_number: usize,
|
||||
column_number: usize,
|
||||
line_text: Option<&str>,
|
||||
msg: impl ToString,
|
||||
) -> String {
|
||||
let message = format!(
|
||||
"{input_name}:{line_number}:{column_number}: error: {}",
|
||||
msg.to_string()
|
||||
);
|
||||
// Line 0: the script was exhausted, so there is no line to show.
|
||||
if !diagnostics_enabled() || line_number == 0 {
|
||||
return message;
|
||||
}
|
||||
// A script line that is not valid UTF-8 cannot be drawn.
|
||||
let Some(text) = line_text.filter(|text| !text.is_empty()) else {
|
||||
return message;
|
||||
};
|
||||
|
||||
// `column_number` is a 1-based byte column; errors at end of line point
|
||||
// one past the last character.
|
||||
let Some(prefix) = text.get(..(column_number - 1).min(text.len())) else {
|
||||
return message;
|
||||
};
|
||||
let drawn = render_snippet(
|
||||
input_name,
|
||||
line_number,
|
||||
column_number,
|
||||
text,
|
||||
prefix,
|
||||
use_color(),
|
||||
);
|
||||
format!("{message}\n{drawn}")
|
||||
}
|
||||
|
||||
/// Whether the underline is colored. uucore's diagnostics make the same
|
||||
/// check but keep it private.
|
||||
fn use_color() -> bool {
|
||||
env::var_os("NO_COLOR").is_none() && stderr().is_terminal()
|
||||
}
|
||||
|
||||
/// Draw `text` as line `line_number` of `input_name`, underlining the
|
||||
/// character that follows `prefix`, in the layout uucore's diagnostics use:
|
||||
///
|
||||
/// ```text
|
||||
/// â•─[ <script argument 1>:1:7 ]
|
||||
/// │
|
||||
/// 1 │ s/a/b/q
|
||||
/// │ ─
|
||||
/// ───╯
|
||||
/// ```
|
||||
fn render_snippet(
|
||||
input_name: &str,
|
||||
line_number: usize,
|
||||
column_number: usize,
|
||||
text: &str,
|
||||
prefix: &str,
|
||||
color: bool,
|
||||
) -> String {
|
||||
let number = line_number.to_string();
|
||||
let margin = " ".repeat(number.len() + 2);
|
||||
// Pad by display width so the underline lines up with the character
|
||||
// above it; tabs are kept as they are, since their width depends on the
|
||||
// terminal.
|
||||
let indent: String = prefix
|
||||
.chars()
|
||||
.flat_map(|c| {
|
||||
let (fill, width) = if c == '\t' {
|
||||
('\t', 1)
|
||||
} else {
|
||||
(' ', c.width().unwrap_or(0))
|
||||
};
|
||||
std::iter::repeat_n(fill, width)
|
||||
})
|
||||
.collect();
|
||||
// As wide as the offending character; one column past the end of line.
|
||||
let width = text[prefix.len()..]
|
||||
.chars()
|
||||
.next()
|
||||
.and_then(UnicodeWidthChar::width)
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
let underline = "─".repeat(width);
|
||||
let underline = if color {
|
||||
format!("\x1b[31m{underline}\x1b[0m")
|
||||
} else {
|
||||
underline
|
||||
};
|
||||
format!(
|
||||
"{margin}â•─[ {input_name}:{line_number}:{column_number} ]\n\
|
||||
{margin}│\n\
|
||||
\x20{number} │ {text}\n\
|
||||
{margin}│ {indent}{underline}\n\
|
||||
{rule}╯",
|
||||
rule = "─".repeat(margin.len()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Fail with msg as a compile error at the provider location.
|
||||
/// The error's exit code is 1 (compilation phase).
|
||||
pub fn compilation_error<T>(
|
||||
@@ -177,12 +55,12 @@ pub fn compilation_error<T>(
|
||||
) -> UResult<T> {
|
||||
Err(USimpleError::new(
|
||||
1,
|
||||
script_error(
|
||||
format!(
|
||||
"{}:{}:{}: error: {}",
|
||||
lines.get_input_name(),
|
||||
lines.get_line_number(),
|
||||
line.get_pos() + 1,
|
||||
str::from_utf8(line.get_line()).ok(),
|
||||
msg,
|
||||
msg.to_string()
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -192,12 +70,12 @@ pub fn compilation_error<T>(
|
||||
fn location_error<T>(location: &ScriptLocation, msg: impl ToString, exit_code: i32) -> UResult<T> {
|
||||
Err(USimpleError::new(
|
||||
exit_code,
|
||||
script_error(
|
||||
&location.input_name,
|
||||
format!(
|
||||
"{}:{}:{}: error: {}",
|
||||
location.input_name,
|
||||
location.line_number,
|
||||
location.column_number,
|
||||
location.line_text.as_deref(),
|
||||
msg,
|
||||
msg.to_string()
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -237,25 +115,3 @@ pub fn input_runtime_error<T>(
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// The integration tests check the layout; they cannot get color, as
|
||||
// their stderr is not a terminal.
|
||||
#[test]
|
||||
fn the_underline_is_red_when_colored() {
|
||||
let drawn = render_snippet("f.sed", 1, 7, "s/a/b/q", "s/a/b/", true);
|
||||
assert_eq!(
|
||||
drawn,
|
||||
concat!(
|
||||
" â•─[ f.sed:1:7 ]\n",
|
||||
" │\n",
|
||||
" 1 │ s/a/b/q\n",
|
||||
" │ \x1b[31m─\x1b[0m\n",
|
||||
"───╯",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-107
@@ -592,11 +592,8 @@ 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
|
||||
@@ -627,7 +624,6 @@ 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,
|
||||
}
|
||||
@@ -646,7 +642,6 @@ impl OutputBuffer {
|
||||
fast_copy,
|
||||
max_pending_write,
|
||||
mmap_chunk: None,
|
||||
pending_newline: false,
|
||||
#[cfg(test)]
|
||||
low_level_flushes: 0,
|
||||
}
|
||||
@@ -678,20 +673,13 @@ impl OutputBuffer {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Schedule the specified text for output unchanged, after any
|
||||
/// deferred newline.
|
||||
pub fn write_raw(&mut self, bytes: &[u8]) -> io::Result<()> {
|
||||
self.flush_pending_newline()?;
|
||||
#[cfg(unix)]
|
||||
self.flush_mmap(WriteRange::Complete)?;
|
||||
self.out.write_all(bytes)
|
||||
}
|
||||
|
||||
/// Copy the specified file to the output, after any deferred newline.
|
||||
/// Copy the specified file to the output.
|
||||
pub fn copy_file(&mut self, path: &PathBuf) -> io::Result<()> {
|
||||
// As in GNU sed, write the deferred newline even when the file is
|
||||
// empty or cannot be read.
|
||||
self.write_raw(b"")?;
|
||||
// Flush mmap writes, if any.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
self.flush_mmap(WriteRange::Complete)?;
|
||||
}
|
||||
|
||||
let Ok(file) = File::open(path) else {
|
||||
// Per POSIX, if the file can't be read treat it as empty.
|
||||
@@ -732,12 +720,6 @@ 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,
|
||||
@@ -783,7 +765,6 @@ impl OutputBuffer {
|
||||
len: new_len,
|
||||
});
|
||||
}
|
||||
self.pending_newline = !new_chunk.is_newline_terminated();
|
||||
}
|
||||
|
||||
IOChunkContent::Owned {
|
||||
@@ -796,7 +777,6 @@ impl OutputBuffer {
|
||||
if *has_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
}
|
||||
self.pending_newline = !has_newline;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -845,16 +825,6 @@ 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
|
||||
@@ -870,11 +840,6 @@ impl OutputBuffer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.pending_newline {
|
||||
self.out.write_all(b"\n")?;
|
||||
self.pending_newline = false;
|
||||
}
|
||||
|
||||
match &chunk.content {
|
||||
IOChunkContent::Owned {
|
||||
content,
|
||||
@@ -885,21 +850,11 @@ 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
|
||||
@@ -1983,7 +1938,6 @@ mod tests {
|
||||
max_pending_write: 8,
|
||||
#[cfg(unix)]
|
||||
mmap_chunk: None,
|
||||
pending_newline: false,
|
||||
low_level_flushes: 0,
|
||||
};
|
||||
(buf, file)
|
||||
@@ -2104,58 +2058,4 @@ 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");
|
||||
}
|
||||
}
|
||||
+10
-206
@@ -15,8 +15,7 @@ pub mod error_handling;
|
||||
pub mod fast_io;
|
||||
pub mod fast_regex;
|
||||
pub mod in_place;
|
||||
pub mod named_reader;
|
||||
pub mod named_writer;
|
||||
pub mod named_io;
|
||||
pub mod processor;
|
||||
pub mod script_char_provider;
|
||||
pub mod script_line_provider;
|
||||
@@ -28,7 +27,6 @@ use crate::sed::script_line_provider::ScriptValue;
|
||||
use clap::{Arg, ArgMatches, Command, arg};
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use uucore::error::{UResult, USimpleError, UUsageError};
|
||||
use uucore::format_usage;
|
||||
@@ -39,7 +37,7 @@ const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (uutils)");
|
||||
|
||||
#[uucore::main]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let matches = parse_args(args)?;
|
||||
let matches = uu_app().try_get_matches_from(args)?;
|
||||
|
||||
// Don't use arg_required_else_help when declaring command
|
||||
// as it exits with code 2 and we use it to check
|
||||
@@ -57,120 +55,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_args(args: impl IntoIterator<Item = OsString>) -> clap::error::Result<ArgMatches> {
|
||||
let mut cmd = uu_app();
|
||||
cmd.build();
|
||||
let args = gnu_in_place_args(&cmd, args);
|
||||
cmd.try_get_matches_from(args)
|
||||
}
|
||||
|
||||
/// Rewrite GNU's `-iSUFFIX`, which clap cannot parse, as `--in-place=SUFFIX`,
|
||||
/// splitting it from flags clustered before it: `-ni.bak` becomes
|
||||
/// `-n --in-place=.bak`. The values of other options are left as they are.
|
||||
fn gnu_in_place_args(cmd: &Command, args: impl IntoIterator<Item = OsString>) -> Vec<OsString> {
|
||||
let mut args = args.into_iter();
|
||||
// The program name.
|
||||
let mut out: Vec<OsString> = args.next().into_iter().collect();
|
||||
while let Some(arg) = args.next() {
|
||||
// Pass non-UTF-8 arguments to clap unchanged.
|
||||
let arg = match arg.into_string() {
|
||||
Ok(arg) => arg,
|
||||
Err(arg) => {
|
||||
out.push(arg);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if arg == "--" {
|
||||
out.push(arg.into());
|
||||
break;
|
||||
}
|
||||
match option_kind(cmd, &arg) {
|
||||
OptionKind::InPlace(pos) => {
|
||||
if pos > 1 {
|
||||
out.push(arg[..pos].into());
|
||||
}
|
||||
out.push(format!("--in-place={}", &arg[pos + 1..]).into());
|
||||
}
|
||||
OptionKind::ValueFollows => {
|
||||
out.push(arg.into());
|
||||
// Keep the value as is, even if it looks like `-iSUFFIX`.
|
||||
out.extend(args.next());
|
||||
}
|
||||
OptionKind::Other => out.push(arg.into()),
|
||||
}
|
||||
}
|
||||
out.extend(args);
|
||||
out
|
||||
}
|
||||
|
||||
enum OptionKind {
|
||||
/// A cluster like `-ni.bak`, with the position of its `i`.
|
||||
InPlace(usize),
|
||||
/// An option whose value is the next argument, like `-f FILE`.
|
||||
ValueFollows,
|
||||
Other,
|
||||
}
|
||||
|
||||
fn option_kind(cmd: &Command, arg: &str) -> OptionKind {
|
||||
if let Some(name) = arg.strip_prefix("--") {
|
||||
return match long_option(cmd, name) {
|
||||
Some(opt) if opt.get_action().takes_values() && !opt.is_require_equals_set() => {
|
||||
OptionKind::ValueFollows
|
||||
}
|
||||
_ => OptionKind::Other,
|
||||
};
|
||||
}
|
||||
let Some(cluster) = arg.strip_prefix('-') else {
|
||||
return OptionKind::Other;
|
||||
};
|
||||
for (pos, c) in cluster.char_indices() {
|
||||
// Leave unknown options for clap to report.
|
||||
let Some(opt) = cmd.get_arguments().find(|a| {
|
||||
a.get_short() == Some(c) || a.get_all_short_aliases().is_some_and(|s| s.contains(&c))
|
||||
}) else {
|
||||
return OptionKind::Other;
|
||||
};
|
||||
let attached = pos + c.len_utf8() < cluster.len();
|
||||
if opt.get_id() == "in-place" {
|
||||
return if attached {
|
||||
OptionKind::InPlace(pos + 1)
|
||||
} else {
|
||||
OptionKind::Other
|
||||
};
|
||||
}
|
||||
if opt.get_action().takes_values() {
|
||||
// The rest of the cluster, or else the next argument, is the value.
|
||||
return if attached {
|
||||
OptionKind::Other
|
||||
} else {
|
||||
OptionKind::ValueFollows
|
||||
};
|
||||
}
|
||||
}
|
||||
OptionKind::Other
|
||||
}
|
||||
|
||||
/// Find a long option by its name or, as `infer_long_args` allows, a unique
|
||||
/// prefix of it. `None` if `name` includes a value.
|
||||
fn long_option<'a>(cmd: &'a Command, name: &str) -> Option<&'a Arg> {
|
||||
if name.contains('=') {
|
||||
return None;
|
||||
}
|
||||
let names = |a: &'a Arg| {
|
||||
a.get_long()
|
||||
.into_iter()
|
||||
.chain(a.get_all_aliases().into_iter().flatten())
|
||||
};
|
||||
if let Some(opt) = cmd.get_arguments().find(|a| names(a).any(|n| n == name)) {
|
||||
return Some(opt);
|
||||
}
|
||||
let mut found = cmd
|
||||
.get_arguments()
|
||||
.filter(|a| names(a).any(|n| n.starts_with(name)));
|
||||
let opt = found.next()?;
|
||||
found.next().is_none().then_some(opt)
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
pub fn uu_app() -> Command {
|
||||
#[cfg(windows)]
|
||||
@@ -202,9 +86,7 @@ pub fn uu_app() -> Command {
|
||||
.short_alias('r')
|
||||
.help("Use extended regular expressions.")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
// As in GNU sed, a value may begin with `-`.
|
||||
arg!(-e --expression <SCRIPT> "Add script to executed commands.")
|
||||
.allow_hyphen_values(true)
|
||||
.action(clap::ArgAction::Append),
|
||||
// Access with .get_many::<PathBuf>("file")
|
||||
Arg::new("script-file")
|
||||
@@ -212,36 +94,26 @@ pub fn uu_app() -> Command {
|
||||
.long("script-file")
|
||||
.help("Specify script file.")
|
||||
.value_parser(clap::value_parser!(PathBuf))
|
||||
.allow_hyphen_values(true)
|
||||
.action(clap::ArgAction::Append),
|
||||
Arg::new("follow-symlinks")
|
||||
.long("follow-symlinks")
|
||||
.help("Follow symlinks when processing in place.")
|
||||
.action(clap::ArgAction::SetTrue),
|
||||
// Access with .get_one::<String>("in-place")
|
||||
// The SUFFIX must be attached, as in GNU sed.
|
||||
Arg::new("in-place")
|
||||
.short('i')
|
||||
.long("in-place")
|
||||
.help("Edit files in place, making a backup if SUFFIX is supplied.")
|
||||
.value_name("SUFFIX")
|
||||
.num_args(0..=1)
|
||||
.require_equals(true)
|
||||
.default_missing_value(""),
|
||||
// Access with .get_one::<u32>("line-length")
|
||||
arg!(-l --length <NUM> "Specify the 'l' command line-wrap length.")
|
||||
.allow_hyphen_values(true)
|
||||
.value_parser(clap::value_parser!(u32)),
|
||||
arg!(-n --quiet "Suppress automatic printing of pattern space.").aliases(["silent"]),
|
||||
arg!(--posix "Disable non-POSIX extensions."),
|
||||
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")
|
||||
@@ -367,11 +239,10 @@ 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") || matches.contains_id("in-place"),
|
||||
separate: matches.get_flag("separate"),
|
||||
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)?,
|
||||
@@ -383,19 +254,18 @@ fn build_context(matches: &ArgMatches) -> UResult<ProcessingContext> {
|
||||
last_line: false,
|
||||
last_file: false,
|
||||
stop_processing: false,
|
||||
quit_silently: false,
|
||||
saved_regex: None,
|
||||
input_action: None,
|
||||
hold: ByteSpace {
|
||||
content: Vec::new(),
|
||||
has_newline: true,
|
||||
},
|
||||
open_blocks: Vec::new(),
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -520,7 +390,6 @@ mod tests {
|
||||
assert!(!ctx.sandbox);
|
||||
assert!(!ctx.unbuffered);
|
||||
assert!(!ctx.null_data);
|
||||
assert!(!ctx.uutil_extensions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -538,7 +407,6 @@ mod tests {
|
||||
"-s",
|
||||
"--sandbox",
|
||||
"-u",
|
||||
"-U",
|
||||
"-z",
|
||||
]);
|
||||
|
||||
@@ -557,7 +425,6 @@ mod tests {
|
||||
assert!(ctx.sandbox);
|
||||
assert!(ctx.unbuffered);
|
||||
assert!(ctx.null_data);
|
||||
assert!(ctx.uutil_extensions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -568,76 +435,13 @@ mod tests {
|
||||
assert!(ctx.regex_extended);
|
||||
}
|
||||
|
||||
// In-place argument rewriting
|
||||
fn in_place_args(args: &[&str]) -> Vec<String> {
|
||||
let mut cmd = uu_app();
|
||||
cmd.build();
|
||||
gnu_in_place_args(&cmd, ["sed"].iter().chain(args).map(OsString::from))
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
.map(|arg| arg.into_string().unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_place_args_rewrite_attached_suffix() {
|
||||
let cases: &[(&[&str], &[&str])] = &[
|
||||
(&["-i.bak"], &["--in-place=.bak"]),
|
||||
// Everything after the `i` is the suffix, as in GNU sed.
|
||||
(&["-iE"], &["--in-place=E"]),
|
||||
(&["-i=.bak"], &["--in-place==.bak"]),
|
||||
// Flags clustered before `-i` are kept.
|
||||
(&["-ni.bak"], &["-n", "--in-place=.bak"]),
|
||||
(&["-Esi.bak"], &["-Es", "--in-place=.bak"]),
|
||||
(&["-ri.bak"], &["-r", "--in-place=.bak"]),
|
||||
(
|
||||
&["-i.bak", "--", "-i.keep"],
|
||||
&["--in-place=.bak", "--", "-i.keep"],
|
||||
),
|
||||
// Only the next argument is another option's value.
|
||||
(&["-e", "p", "-i.bak"], &["-e", "p", "--in-place=.bak"]),
|
||||
(
|
||||
&["--expression=p", "-i.bak"],
|
||||
&["--expression=p", "--in-place=.bak"],
|
||||
),
|
||||
(&["--quiet", "-i.bak"], &["--quiet", "--in-place=.bak"]),
|
||||
(
|
||||
&["--in-place", "-i.bak"],
|
||||
&["--in-place", "--in-place=.bak"],
|
||||
),
|
||||
// A `--` that is an option's value does not end the options.
|
||||
(&["-e", "--", "-i.bak"], &["-e", "--", "--in-place=.bak"]),
|
||||
];
|
||||
for (args, expected) in cases {
|
||||
assert_eq!(in_place_args(args), *expected, "args: {args:?}");
|
||||
}
|
||||
}
|
||||
fn test_in_place_with_suffix() {
|
||||
let matches = test_matches(&["-i", ".bak"]);
|
||||
let ctx = build_context(&matches).unwrap();
|
||||
|
||||
#[test]
|
||||
fn test_in_place_args_leave_other_arguments_alone() {
|
||||
let cases: &[&[&str]] = &[
|
||||
&["-i", "s/a/b/", "file"],
|
||||
&["-Ei", "s/a/b/", "file"],
|
||||
&["-En", "s/a/b/", "file"],
|
||||
&["--in-place=.bak", "s/a/b/", "file"],
|
||||
&["--in=.bak", "s/a/b/", "file"],
|
||||
// Values attached to other options.
|
||||
&["-fi.sed", "file"],
|
||||
&["-nfi.sed", "file"],
|
||||
&["-ei", "file"],
|
||||
// Values that follow other options.
|
||||
&["-f", "-ifoo.sed", "file"],
|
||||
&["-nf", "-ifoo.sed", "file"],
|
||||
&["-l", "-i5"],
|
||||
&["--expression", "-i.bak"],
|
||||
&["--expr", "-i.bak"],
|
||||
// Unknown options are left for clap to report.
|
||||
&["-xi.bak"],
|
||||
&["--", "-i.bak"],
|
||||
];
|
||||
for args in cases {
|
||||
assert_eq!(in_place_args(args), *args, "args: {args:?}");
|
||||
}
|
||||
assert!(ctx.in_place);
|
||||
assert_eq!(ctx.in_place_suffix, Some(".bak".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Loaded 20 of 41 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user