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

Co-authored-by: Mukunda Katta <mukunda.vjcs6@gmail.com>
2026-08-19 20:22:54 +03:00
41 changed files with 2009 additions and 3051 deletions

No files matched your search

+1 -14
View File
@@ -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
+2 -2
View File
@@ -97,12 +97,12 @@ jobs:
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
LLVM_PROFILE_FILE: "${{ 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 -
-41
View File
@@ -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
+1 -1
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+9 -12
View File
@@ -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 }
+70 -8
View File
@@ -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
View File
@@ -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
View File
@@ -1 +0,0 @@
book
-7
View File
@@ -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"
-5
View File
@@ -1,5 +0,0 @@
# Summary
[Introduction](index.md)
* [Extensions and incompatibilities](extensions.md)
-86
View File
@@ -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.
-10
View File
@@ -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).
+402 -386
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -14,8 +14,8 @@ libc = "0.2.153"
tempfile = "3.15.0"
rand = { version = "0.10.0" }
similar = "3.0.0"
uucore = { version = "0.12.0", features = ["libc"] }
uufuzz = "0.12.0"
uucore = { version = "0.10.0", features = ["libc"] }
uufuzz = "0.10.0"
sed = { path = ".." }
+13 -27
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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