Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sed/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

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::script_char_provider::ScriptCharProvider;
use crate::sed::script_line_provider::ScriptLineProvider;
Expand Down Expand Up @@ -368,6 +369,7 @@ 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'
Number(usize), // Number for 'l', 'q', 'Q' (GNU)
Substitution(Box<Substitution>), // Substitute command 's'
Expand Down
45 changes: 45 additions & 0 deletions src/sed/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +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::script_char_provider::ScriptCharProvider;
use crate::sed::script_line_provider::{ScriptLineProvider, ScriptValue};
Expand Down Expand Up @@ -1122,6 +1123,21 @@ fn compile_read_file_command(
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)?;
cmd.data = CommandData::NamedReader(NamedReader::new(path));
Ok(CommandHandling::Continue)
}

// Handles w
fn compile_write_file_command(
lines: &mut ScriptLineProvider,
Expand Down Expand Up @@ -1636,6 +1652,10 @@ fn get_cmd_spec(
n_addr: 2,
handler: compile_empty_command,
}),
'R' if !posix => Ok(CommandSpec {
n_addr: 2,
handler: compile_read_line_command,
}),
'r' => Ok(CommandSpec {
n_addr: if posix { 1 } else { 2 },
handler: compile_read_file_command,
Expand Down Expand Up @@ -3021,6 +3041,31 @@ mod tests {
assert!(err.to_string().contains(ERR_SANDBOX));
}

// compile_read_line_command (R)
#[test]
fn test_compile_read_line_command_rejected_under_sandbox() {
let (mut lines, mut chars) = make_providers("R input.txt");
let mut cmd = Command::default();
let mut context = ctx();
context.sandbox = true;

let err =
compile_read_line_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap_err();
assert!(err.to_string().contains(ERR_SANDBOX));
}

#[test]
fn test_compile_read_line_command_sets_named_reader() {
let (mut lines, mut chars) = make_providers("R input.txt");
let mut cmd = Command::default();
let mut context = ctx();

let handling =
compile_read_line_command(&mut lines, &mut chars, &mut cmd, &mut context).unwrap();
assert!(matches!(handling, CommandHandling::Continue));
assert!(matches!(cmd.data, CommandData::NamedReader(_)));
}

// compile_write_file_command
#[test]
fn test_compile_write_file_command_rejected_under_sandbox() {
Expand Down
1 change: 1 addition & 0 deletions src/sed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +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 processor;
pub mod script_char_provider;
Expand Down
101 changes: 101 additions & 0 deletions src/sed/named_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// An abstraction for input files read one line at a time by the `R` command
//
// SPDX-License-Identifier: MIT
// Copyright (c) 2025 Diomidis Spinellis
//
// This file is part of the uutils sed package.
// It is licensed under the MIT License.
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.

use std::cell::RefCell;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::rc::Rc;

#[derive(Debug)]
/// State of the file backing an `R` command, opened lazily on first use.
enum State {
Unopened,
Open(BufReader<File>),
Exhausted,
}

#[derive(Debug)]
/// Reader that yields successive lines of a file for the GNU `R` command.
/// The file is opened on first use; a file that cannot be opened or read is
/// treated as having no more lines, matching GNU sed (no error is raised).
pub struct NamedReader {
path: PathBuf,
state: State,
}

impl NamedReader {
/// Create a reader for `path` without opening it yet.
pub fn new(path: PathBuf) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(NamedReader {
path,
state: State::Unopened,
}))
}

/// Return the next line of the file, including its trailing newline if
/// present, or `None` once the file is exhausted or could not be read.
pub fn next_line(&mut self) -> Option<Vec<u8>> {
if matches!(self.state, State::Unopened) {
self.state = match File::open(&self.path) {
Ok(file) => State::Open(BufReader::new(file)),
Err(_) => State::Exhausted,
};
}

let State::Open(reader) = &mut self.state else {
return None;
};

let mut line = Vec::new();
match reader.read_until(b'\n', &mut line) {
Ok(0) | Err(_) => {
self.state = State::Exhausted;
None
}
Ok(_) => Some(line),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;

#[test]
fn yields_successive_lines_then_none() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"one\ntwo\n").unwrap();
let reader = NamedReader::new(file.path().to_path_buf());

assert_eq!(reader.borrow_mut().next_line(), Some(b"one\n".to_vec()));
assert_eq!(reader.borrow_mut().next_line(), Some(b"two\n".to_vec()));
assert_eq!(reader.borrow_mut().next_line(), None);
assert_eq!(reader.borrow_mut().next_line(), None);
}

#[test]
fn last_line_without_newline_is_preserved() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"abc").unwrap();
let reader = NamedReader::new(file.path().to_path_buf());

assert_eq!(reader.borrow_mut().next_line(), Some(b"abc".to_vec()));
assert_eq!(reader.borrow_mut().next_line(), None);
}

#[test]
fn missing_file_yields_no_lines() {
let reader = NamedReader::new(PathBuf::from("/nonexistent/xyzzy-42-does-not-exist"));
assert_eq!(reader.borrow_mut().next_line(), None);
}
}
9 changes: 9 additions & 0 deletions src/sed/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,15 @@ fn process_file(
context.quiet = true;
break;
}
'R' => {
// Queue the file's next line for output at end of cycle.
let reader = extract_variant!(command, NamedReader);
if let Some(line) = reader.borrow_mut().next_line() {
context
.append_elements
.push(AppendElement::Text(line.into()));
}
}
'r' => {
// Copy the file to standard output at a later point.
let path = extract_variant!(command, Path);
Expand Down
75 changes: 75 additions & 0 deletions tests/by-util/test_sed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,81 @@ fn write_first_line_with_w_command_is_non_posix() {
.stderr_is("sed: <script argument 1>:1:1: error: invalid command code `W'\n");
}

#[test]
fn read_one_line_reads_successive_lines() -> std::io::Result<()> {
let temp = NamedTempFile::new()?;
fs::write(temp.path(), "one\ntwo\n")?;
let cmd = format!("R {}", temp.path().display());

// One line of the file is queued per cycle; the third cycle finds EOF.
new_ucmd!()
.args(&["-e", &cmd])
.pipe_in("a\nb\nc\n")
.succeeds()
.stdout_is("a\none\nb\ntwo\nc\n");

Ok(())
}

#[test]
fn read_one_line_missing_file_is_silent() {
new_ucmd!()
.args(&["-e", "R /nonexistent/xyzzy-42-does-not-exist"])
.pipe_in("a\nb\n")
.succeeds()
.stdout_is("a\nb\n");
}

#[test]
fn sandbox_rejects_read_one_line_command() {
new_ucmd!()
.args(&["--sandbox", "R /tmp/out", LINES1])
.fails()
.stderr_contains("command not allowed with --sandbox");
}

#[test]
fn read_one_line_with_r_command_is_non_posix() {
new_ucmd!()
.args(&["--posix", "R /tmp/out"])
.fails()
.code_is(1)
.stderr_is("sed: <script argument 1>:1:1: error: invalid command code `R'\n");
}

#[test]
fn read_one_line_appends_even_with_suppressed_autoprint() -> std::io::Result<()> {
let temp = NamedTempFile::new()?;
fs::write(temp.path(), "x1\nx2\n")?;
let cmd = format!("R {}", temp.path().display());

// `-n` suppresses the pattern space auto-print, but `R` still queues the
// file's lines to the output stream (like `a`/`r`).
new_ucmd!()
.args(&["-n", "-e", &cmd])
.pipe_in("a\nb\n")
.succeeds()
.stdout_is("x1\nx2\n");

Ok(())
}

#[test]
fn read_one_line_stops_when_file_shorter_than_input() -> std::io::Result<()> {
let temp = NamedTempFile::new()?;
fs::write(temp.path(), "only\n")?;
let cmd = format!("R {}", temp.path().display());

// The file has one line; later cycles read nothing and emit no extra text.
new_ucmd!()
.args(&["-e", &cmd])
.pipe_in("a\nb\nc\n")
.succeeds()
.stdout_is("a\nonly\nb\nc\n");

Ok(())
}

////////////////////////////////////////////////////////////
// =, l, F commands
check_output!(number_continuous, ["/l2_/=", LINES1, LINES2]);
Expand Down
Loading