diff --git a/docs/src/extensions.md b/docs/src/extensions.md index 3b2a1adf..67360098 100644 --- a/docs/src/extensions.md +++ b/docs/src/extensions.md @@ -35,6 +35,8 @@ 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 diff --git a/src/sed/mod.rs b/src/sed/mod.rs index a4b80260..868b2bbe 100644 --- a/src/sed/mod.rs +++ b/src/sed/mod.rs @@ -28,6 +28,7 @@ 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; @@ -38,7 +39,7 @@ const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (uutils)"); #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let matches = parse_args(args)?; // Don't use arg_required_else_help when declaring command // as it exits with code 2 and we use it to check @@ -56,6 +57,120 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(()) } +fn parse_args(args: impl IntoIterator) -> clap::error::Result { + 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) -> Vec { + let mut args = args.into_iter(); + // The program name. + let mut out: Vec = 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)] @@ -87,7 +202,9 @@ 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