39 changed files with 1377 additions and 3672 deletions

No files matched your search

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