Author SHA1 Message Date
Sylvestre Ledru 3bdf96d08d sed: keep script position for errors at end of script
An error found once the script is exhausted, like an unterminated
command, was reported as ":0:N" with no input name. Keep the last input
name and line number so the error points at the right line.
2026-09-27 18:50:16 +02:00
15 changed files with 811 additions and 1851 deletions

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ jobs:
persist-credentials: false
- name: Retrieve reference artifacts
uses: dawidd6/action-download-artifact@v27
uses: dawidd6/action-download-artifact@v25
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 -
Generated
+325 -411
View File
File diff suppressed because it is too large. Load diff
+6 -4
View File
@@ -1,7 +1,7 @@
# 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 (libs) mangen sed binstall chrono codspeed libc memchr memmap rlimit rustix textwrap uucore uutests xattr
# spell-checker:ignore (misc) clippy codegen msvc startswith uninlined uutils
[package]
@@ -42,6 +42,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"
@@ -60,8 +63,7 @@ sha2 = { version = "0.11.0", default-features = false, features = ["alloc"] }
tempfile = "3.10.1"
terminal_size = "0.4.2"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
unicode-width = "0.2.2"
uucore = { version = "0.12.0", features = ["diagnostics", "libc"] }
uucore = { version = "0.12.0", features = ["libc"] }
rustix = "1.1.4"
xattr = "1.3.1"
@@ -79,12 +81,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 }
-13
View File
@@ -35,23 +35,10 @@ Below is a list of these extensions and incompatibilities.
## 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
+390 -375
View File
File diff suppressed because it is too large. Load diff
+4 -17
View File
@@ -54,8 +54,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,8 +64,8 @@ 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
@@ -336,7 +334,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 +347,6 @@ impl Default for Command {
addr2: None,
non_select: false,
start_line: None,
range_started: false,
data: CommandData::None,
next: None,
location: ScriptLocation::default(),
@@ -396,17 +392,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)]
+7 -16
View File
@@ -29,7 +29,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 +77,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 +85,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)
@@ -843,10 +841,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 +907,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 +1074,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)?;
@@ -1185,7 +1176,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)
+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",
"───╯",
)
);
}
}
+6 -13
View File
@@ -678,20 +678,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.
+7 -193
View File
@@ -28,7 +28,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 +38,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 +56,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 +87,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,25 +95,20 @@ 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."),
@@ -383,14 +261,13 @@ 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(),
@@ -568,76 +445,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]
+27 -46
View File
@@ -9,8 +9,8 @@
// file that was distributed with this source code.
use crate::sed::command::{
Address, AppendElement, CharacterMode, Command, CommandData, InputAction, NextLine,
ProcessingContext, Transliteration,
Address, AppendElement, CharacterMode, Command, CommandData, InputAction, ProcessingContext,
Transliteration,
};
use crate::sed::delimited_parser::os_string_from_bytes;
use crate::sed::error_handling::{ScriptLocation, input_runtime_error};
@@ -124,15 +124,7 @@ fn applies(
}
} else if let Some(addr1) = &command.addr1 {
// See if latch must start.
let starts = match addr1 {
// Numeric start already passed (block, d, n, branch): start once here.
Address::Line(n) if linenum > *n => {
!command.range_started && !matches!(addr2, Address::Line(m) if *m < linenum)
}
_ => match_address(addr1, reader, pattern, context, &command.location)?,
};
if starts {
command.range_started = true;
if match_address(addr1, reader, pattern, context, &command.location)? {
match addr2 {
Address::Line(n) if linenum >= *n => {
context.last_address = true;
@@ -475,7 +467,7 @@ fn flush_appends(output: &mut OutputBuffer, context: &mut ProcessingContext) ->
for elem in &context.append_elements {
match elem {
AppendElement::Text(text) => {
output.write_raw(text.as_ref())?;
output.write_bytes(text.as_ref())?;
}
AppendElement::Path(path) => {
output.copy_file(path)?;
@@ -575,7 +567,9 @@ fn list(
) -> UResult<()> {
// Special case for an empty pattern space
if line.is_empty() {
output.write_str("$\n")?;
if line.is_newline_terminated() {
output.write_str("$\n")?;
}
return Ok(());
}
@@ -660,13 +654,12 @@ fn process_file(
// Set the script command from which to start.
let mut current: Option<Rc<RefCell<Command>>> =
if let Some(action) = context.input_action.take() {
// Continue processing the `N` or `n` command.
if let NextLine::Append(mut combined_lines) = action.next_line {
combined_lines.push(b'\n');
combined_lines.extend_from_slice(pattern.as_bytes());
// Continue processing the `N` command.
let mut combined_lines = action.prepend;
combined_lines.push(b'\n');
combined_lines.extend_from_slice(pattern.as_bytes());
pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated());
}
pattern.set_to_bytes(combined_lines, pattern.is_newline_terminated());
action.next_command
} else {
// Start from the script top.
@@ -744,7 +737,7 @@ fn process_file(
}
CommandData::Text(cmd_bytes) => {
let shell_out = shell_stdout(cmd_bytes.to_vec(), &command, context)?;
output.write_raw(&shell_out)?;
output.write_bytes(&shell_out)?;
}
_ => panic!("invalid 'e' command data"),
},
@@ -786,17 +779,7 @@ fn process_file(
list(output, &pattern, width, &command.location, context)?;
}
'n' => {
// Print the pattern space and continue with the next
// command once the next line is read, as `N` does.
if !context.quiet {
write_chunk(output, context, &pattern)?;
}
flush_appends(output, context)?;
context.input_action = Some(InputAction {
next_command: command.next.clone(),
next_line: NextLine::Replace,
});
continue 'lines;
break;
}
'N' => {
flush_appends(output, context)?;
@@ -806,7 +789,7 @@ fn process_file(
// to perform when the next line is read.
context.input_action = Some(InputAction {
next_command: command.next.clone(),
next_line: NextLine::Append(pattern.as_bytes().to_vec()),
prepend: pattern.as_bytes().to_vec(),
});
continue 'lines;
}
@@ -835,10 +818,7 @@ fn process_file(
i32::try_from(*extract_variant!(command, Number)).unwrap_or(i32::MAX),
);
context.stop_processing = true;
context.quit_silently = true;
context.quiet = true;
// Like GNU sed, discard text queued by `a`, `r` and `R`.
context.append_elements.clear();
break;
}
'R' => {
@@ -954,20 +934,18 @@ fn process_file(
flush_appends(output, context)?;
if context.stop_processing {
if !context.quit_silently {
output.flush_pending_newline()?;
}
output.flush_pending_newline()?;
break;
}
}
// Handle any n or N command remains.
// Take them even with -n, so that they do not carry over to the next file.
// Handle any N command remains.
// Take it even with -n, so that it does not carry over to the next file.
if context.separate
&& let Some(action) = context.input_action.take()
&& let NextLine::Append(mut pending) = action.next_line
&& !context.quiet
{
let mut pending = action.prepend;
pending.push(b'\n');
output.write_bytes(&pending)?;
if context.unbuffered {
@@ -983,10 +961,13 @@ fn reset_latched_address_ranges(range_commands: &mut [Rc<RefCell<Command>>]) {
for cmd_rc in range_commands.iter() {
let mut cmd = cmd_rc.borrow_mut();
// Check for address-spec line 0 pre-latch extension.
let pre_latched = matches!(cmd.addr1, Some(Address::Line(0)));
cmd.start_line = if pre_latched { Some(0) } else { None };
cmd.range_started = pre_latched;
cmd.start_line =
// Check for address-spec line 0 pre-latch extension.
if let Some(addr1) = &cmd.addr1 && matches!(addr1, Address::Line(0)) {
Some(0)
} else {
None
};
}
}
@@ -1024,8 +1005,8 @@ pub fn process_all_files(
&& !context.separate
&& !context.quiet
&& let Some(action) = context.input_action.take()
&& let NextLine::Append(mut pending) = action.next_line
{
let mut pending = action.prepend;
pending.push(b'\n');
output.write_bytes(&pending)?;
}
-18
View File
@@ -8,15 +8,10 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::cell::OnceCell;
use std::rc::Rc;
#[derive(Debug)]
pub struct ScriptCharProvider {
line: Vec<u8>,
pos: usize,
/// The line as text, shared by the commands compiled from it.
text: OnceCell<Option<Rc<str>>>,
}
impl ScriptCharProvider {
@@ -25,7 +20,6 @@ impl ScriptCharProvider {
Self {
line: line.as_ref().to_vec(),
pos: 0,
text: OnceCell::new(),
}
}
@@ -72,18 +66,6 @@ impl ScriptCharProvider {
pub fn get_pos(&self) -> usize {
self.pos
}
/// Return the whole line being scanned.
pub fn get_line(&self) -> &[u8] {
&self.line
}
/// Return the line as shared text, or `None` if it is not valid UTF-8.
pub fn line_text(&self) -> Option<Rc<str>> {
self.text
.get_or_init(|| str::from_utf8(&self.line).ok().map(Rc::from))
.clone()
}
}
#[cfg(test)]
+15 -2
View File
@@ -115,9 +115,22 @@ impl ScriptLineProvider {
// Move to the next available script source.
fn advance_source(&mut self, next_index: usize) -> UResult<()> {
if next_index >= self.sources.len() {
let (input_name, line_number) =
match std::mem::replace(&mut self.state, State::NotStarted) {
State::Active {
input_name,
line_number,
..
}
| State::Done {
input_name,
line_number,
} => (input_name, line_number),
State::NotStarted => (String::new(), 0),
};
self.state = State::Done {
input_name: self.get_input_name().to_string(),
line_number: self.get_line_number(),
input_name,
line_number,
};
return Ok(());
}
+14 -589
View File
File diff suppressed because it is too large. Load diff