diff --git a/Cargo.lock b/Cargo.lock index a63465e..f7f86c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,9 +335,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "syn" diff --git a/Cargo.toml b/Cargo.toml index 4e117dc..1bf3c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,3 +96,4 @@ memchr = "2.4.1" lazy_format = "1.10.0" bitflags = {version = "1.3.2", optional = true } lazy_static = { version = "1.4.0", optional = true } +#smallvec = { version = "1.9.0", features = ["write", "const_generics", "const_new", "may_dangle", "union"] } diff --git a/src/args.rs b/src/args.rs index 9743fcd..fa82477 100644 --- a/src/args.rs +++ b/src/args.rs @@ -340,8 +340,8 @@ pub fn program_name() -> Cow<'static, str> /// Parse the program's arguments into an `Options` array. /// If parsing fails, an `ArgParseError` is returned detailing why it failed. -#[inline] -#[cfg_attr(feature="logging", instrument(err))] +#[inline] +#[cfg_attr(feature="logging", instrument(err(Display)))] pub fn parse_args() -> Result { parse_from(std::env::args_os().skip(1)) @@ -351,51 +351,295 @@ pub fn parse_args() -> Result fn parse_from(args: I) -> Result where I: IntoIterator, T: Into -{ - mod warnings { - use super::*; - /// Issue a warning when `-exec{}` is provided as an argument, but no positional arguments (`{}`) are specified in the argument list to the command. - #[cold] - #[cfg_attr(feature="logging", inline(never), instrument(level="trace"))] - #[cfg_attr(not(feature="logging"), inline(always))] - pub fn execp_no_positional_replacements() - { - if_trace!(warn!("-exec{{}} provided with no positional arguments ({}), there will be no replacement with the data. Did you mean `-exec`?", POSITIONAL_ARG_STRING)); - } - /// Issue a warning if the user apparently meant to specify two `-exec/{}` arguments to `collect`, but seemingly is accidentally is passing the `-exec/{}` string as an argument to the first. - #[cold] - #[cfg_attr(feature="logging", inline(never), instrument(level="trace"))] - #[cfg_attr(not(feature="logging"), inline(always))] - pub fn exec_apparent_missing_terminator(first_is_positional: bool, second_is_positional: bool, command: &str, argument_number: usize) - { - if_trace! { - warn!("{} provided, but argument to command {command:?} number {argument_number} is {}. Are you missing the terminator before '{}' before this argument?", if first_is_positional {"-exec{{}}"} else {"-exec"}, if second_is_positional {"-exec{{}}"} else {"-exec"}, EXEC_MODE_STRING_TERMINATOR) - } - } - } - +{ let mut args = args.into_iter().map(Into::into); + let mut output = Options::default(); + let mut idx = 0; //XXX: When `-exec{}` is provided, but no `{}` arguments are found, maybe issue a warning with `if_trace!(warning!())`? There are valid situations to do this in, but they are rare... - todo!("//TODO: Parse `args` into `Options`") + let mut parser = || -> Result<_, ArgParseError> { + while let Some(mut arg) = args.next() { + idx += 1; + macro_rules! try_parse_for { + (@ assert_parser_okay $parser:path) => { + const _:() = { + const fn _assert_is_parser() {} + const fn _assert_is_result(res: P::Output) -> P::Output { res } + + _assert_is_parser::<$parser>(); + }; + }; + (try $parser:path => $then:expr$(, $or:expr)?) => { + { + try_parse_for!(@ assert_parser_okay $parser); + //_assert_is_closure(&$then); //XXX: There isn't a good way to tell without having to specify some bound on return type... + if let Some(result) = parsers::try_parse_with::<$parser>(&mut arg, &mut args) { + $(let result = result.map_err($or);)? + result.map($then) + } else { + continue + } + } + }; + ($parser:path => $then:expr) => { + $then(try_parse_for!(try $parser => std::convert::identity)?) + } + } + + try_parse_for!(try parsers::ExecMode => |result| output.exec.push(result))?; + } + Ok(()) + }; + parser() + .with_index(idx) + .map(move |_| output) } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug)] pub enum ArgParseError { + /// With an added argument index. + WithIndex(usize, Box), /// Returned when an invalid or unknown argument is found UnknownOption(OsString), /// Returned when the argument, `argument`, is passed in an invalid context by the user. - InvalidUsage { argument: String, message: String }, + InvalidUsage { argument: String, message: String, inner: Option> }, +} + +trait ArgParseErrorExt: Sized +{ + fn with_index(self, idx: usize) -> Result; +} +impl ArgParseError +{ + #[inline] + pub fn wrap_index(self, idx: usize) -> Self { + Self::WithIndex(idx, Box::new(self)) + } +} +impl> ArgParseErrorExt for Result +{ + #[inline(always)] + fn with_index(self, idx: usize) -> Result { + self.map_err(Into::into) + .map_err(move |e| e.wrap_index(idx)) + } } -impl error::Error for ArgParseError{} +impl error::Error for ArgParseError +{ + #[inline] + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::InvalidUsage { inner, .. } => inner.as_ref().map(|x| -> &(dyn error::Error + 'static) { x.as_ref() }), + Self::WithIndex(_, inner) => inner.source(), + _ => None, + } + } +} impl fmt::Display for ArgParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::WithIndex(index, inner) => write!(f, "Argument #{index}: {inner}"), Self::UnknownOption(opt) => f.write_str(String::from_utf8_lossy(opt.as_bytes()).as_ref()), - Self::InvalidUsage { argument, message } => write!(f, "Invalid usage for argument `{argument}`: {message}") + Self::InvalidUsage { argument, message, .. } => write!(f, "Invalid usage for argument `{argument}`: {message}") + } + } +} + +trait ArgError: error::Error + Send + Sync + 'static +{ + fn into_invalid_usage(self) -> (String, String, Box) + where Self: Sized; +} + +trait TryParse: Sized +{ + type Error: ArgError; + type Output; + + #[inline(always)] + fn visit(argument: &OsStr) -> Option { let _ = argument; None } + fn parse(self, argument: OsString, rest: &mut I) -> Result + where I: Iterator; +} + +impl From<(String, String, E)> for ArgParseError +{ + #[inline] + fn from((argument, message, inner): (String, String, E)) -> Self + { + Self::InvalidUsage { argument, message, inner: Some(Box::new(inner)) } + } +} + +impl From for ArgParseError +{ + #[inline(always)] + fn from(from: E) -> Self + { + let (argument, message, inner) = from.into_invalid_usage(); + Self::InvalidUsage { argument, message, inner: Some(inner) } + } +} + +mod parsers { + use super::*; + + #[inline(always)] + #[cfg_attr(feature="logging", instrument(level="trace", skip(rest), fields(parser = ?std::any::type_name::

())))] + pub(super) fn try_parse_with

(arg: &mut OsString, rest: &mut impl Iterator) -> Option> + where P: TryParse + { + #[cfg(feature="logging")] + let _span = tracing::debug_span!("parse"); + P::visit(arg.as_os_str()).map(move |parser| { + #[cfg(feature="logging")] + let _in = _span.enter(); + parser.parse(std::mem::replace(arg, OsString::default()), rest).map_err(Into::into) + }).map(|res| { + #[cfg(feature="logging")] + match res.as_ref() { + Err(err) => { + ::tracing::event!(::tracing::Level::ERROR, ?err, "Attempted parse failed with error") + }, + _ => () + } + res + }).or_else(|| { + #[cfg(feature="logging")] + ::tracing::event!(::tracing::Level::TRACE, "no match"); + None + }) + } + + /// Parser for `ExecMode` + /// + /// Parses `-exec` / `-exec{}` modes. + #[derive(Debug, Clone, Copy)] + pub enum ExecMode { + Stdin, + Postional, + } + impl ExecMode { + #[inline(always)] + fn is_positional(&self) -> bool + { + match self { + Self::Postional => true, + _ => false + } + } + #[inline(always)] + fn command_string(&self) -> &'static str + { + if self.is_positional() { + "-exec{}" + } else { + "-exec" + } + } + + } + + #[derive(Debug)] + pub struct ExecModeParseError(ExecMode); + impl error::Error for ExecModeParseError{} + impl fmt::Display for ExecModeParseError + { + #[inline(always)] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + { + write!(f, "{} needs at least a command", self.0.command_string()) + } + } + + impl ArgError for ExecModeParseError + { + fn into_invalid_usage(self) -> (String, String, Box) + where Self: Sized { + (self.0.command_string().to_owned(), "Expected a command file-path to execute.".to_owned(), Box::new(self)) + } + } + + impl TryParse for ExecMode + { + type Error = ExecModeParseError; + type Output = super::ExecMode; + #[inline(always)] + fn visit(argument: &OsStr) -> Option { + + if argument == OsStr::from_bytes(b"-exec") { + Some(Self::Stdin) + } else if argument == OsStr::from_bytes(b"-exec{}") { + Some(Self::Postional) + } else { + None + } + } + + #[inline] + fn parse(self, _argument: OsString, rest: &mut I) -> Result + where I: Iterator { + mod warnings { + use super::*; + /// Issue a warning when `-exec{}` is provided as an argument, but no positional arguments (`{}`) are specified in the argument list to the command. + #[cold] + #[cfg_attr(feature="logging", inline(never), instrument(level="trace"))] + #[cfg_attr(not(feature="logging"), inline(always))] + pub fn execp_no_positional_replacements() + { + if_trace!(warn!("-exec{{}} provided with no positional arguments ({}), there will be no replacement with the data. Did you mean `-exec`?", POSITIONAL_ARG_STRING)); + } + /// Issue a warning if the user apparently meant to specify two `-exec/{}` arguments to `collect`, but seemingly is accidentally is passing the `-exec/{}` string as an argument to the first. + #[cold] + #[cfg_attr(feature="logging", inline(never), instrument(level="trace"))] + #[cfg_attr(not(feature="logging"), inline(always))] + pub fn exec_apparent_missing_terminator(first_is_positional: bool, second_is_positional: bool, command: &OsStr, argument_number: usize) + { + if_trace! { + warn!("{} provided, but argument to command {command:?} number #{argument_number} is `{}`. Are you missing the terminator '{}' before this argument?", if first_is_positional {"-exec{}"} else {"-exec"}, if second_is_positional {"-exec{}"} else {"-exec"}, EXEC_MODE_STRING_TERMINATOR) + } + } + } + + let command = rest.next().ok_or_else(|| ExecModeParseError(self))?; + let test_warn_missing_term = |(idx , string) : (usize, OsString)| { + if let Some(val) = Self::visit(&string) { + warnings::exec_apparent_missing_terminator(self.is_positional(), val.is_positional(), &command, idx); + } + string + }; + Ok(match self { + Self::Stdin => { + super::ExecMode::Stdin { + args: rest + .take_while(|argument| argument.as_bytes() != EXEC_MODE_STRING_TERMINATOR.as_bytes()) + .enumerate().map(&test_warn_missing_term) + .collect(), + command, + } + }, + Self::Postional => { + let mut repl_warn = true; + let res = super::ExecMode::Positional { + args: rest + .take_while(|argument| argument.as_bytes() != EXEC_MODE_STRING_TERMINATOR.as_bytes()) + .enumerate().map(&test_warn_missing_term) + .map(|x| if x.as_bytes() == POSITIONAL_ARG_STRING.as_bytes() { + repl_warn = false; + None + } else { + Some(x) + }) + .collect(), + command, + }; + if repl_warn { warnings::execp_no_positional_replacements(); } + res + }, + }) } } } diff --git a/src/ext.rs b/src/ext.rs index 6e1730f..5972715 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -7,6 +7,7 @@ use std::{ }, marker::PhantomData, ops, + iter, }; /// Essentially equivelant bound as `eyre::StdError` (private trait) @@ -16,6 +17,112 @@ pub trait EyreError: std::error::Error + Send + Sync + 'static{} impl EyreError for T where T: std::error::Error + Send + Sync + 'static{} +#[derive(Debug, Clone)] +pub struct Joiner(I, F, bool); + +#[derive(Debug, Clone, Copy)] +pub struct CloneJoiner(T); + +impl Joiner +{ + #[inline(always)] + fn size_calc(low: usize) -> usize + { + match low { + 0 | 1 => low, + 2 => 4, + x if x % 2 == 0 => x * 2, + odd => (odd * 2) - 1 + } + } +} +type JoinerExt = Joiner; + +impl Iterator for Joiner +where I: Iterator, F: FnMut() -> I::Item +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + let val = match self.2 { + false => self.0.next(), + true => Some(self.1()) + }; + if val.is_some() { + self.2 ^= true; + } + val + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (low, high) = self.0.size_hint(); + (Self::size_calc(low), high.map(Self::size_calc)) + } +} + +impl Iterator for Joiner> +where I: Iterator, T: Clone +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + let val = match self.2 { + false => self.0.next(), + true => Some(self.1.0.clone()) + }; + if val.is_some() { + self.2 ^= true; + } + val + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let (low, high) = self.0.size_hint(); + (Self::size_calc(low), high.map(Self::size_calc)) + } +} + +impl iter::FusedIterator for Joiner +where Joiner: Iterator, + I: iter::FusedIterator{} +impl ExactSizeIterator for Joiner +where Joiner: Iterator, + I: ExactSizeIterator {} + +pub trait IterJoinExt: Sized +{ + fn join_by T>(self, joiner: F) -> Joiner; + fn join_by_default(self) -> Joiner T> + where T: Default; + fn join_by_clone(self, value: T) -> Joiner> + where T: Clone; + +} + +impl IterJoinExt for I +where I: Iterator +{ + #[inline] + fn join_by T>(self, joiner: F) -> Joiner { + Joiner(self, joiner, false) + } + #[inline] + fn join_by_default(self) -> Joiner T> + where T: Default + { + Joiner(self, T::default, false) + } + #[inline] + fn join_by_clone(self, value: T) -> Joiner> + where T: Clone { + Joiner(self, CloneJoiner(value), false) + } +} + pub trait IntoEyre { fn into_eyre(self) -> eyre::Result; diff --git a/src/main.rs b/src/main.rs index 6026455..70c9cb6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ struct Options { /// Execution of commands (if passed) **always** happens *after* the copy to `stdout`, but *before* the **close** of `stdout`. If the copy to `stdout` fails, the exec will not be executed regardless of if the mode required is actually using `stdout`. /// The process shall always wait for the child to terminate before exiting. If the child daemon forks, that fork is not followed, and the process exists anyway. /// Ideally, A `SIGHUP` handler should be registered, which tells the parent to stop waiting on the child and exit now. TODO: The behaviour of the child is unspecified if this happens. It may be killed, or re-attached to `init`. But the return code of the parent should always be `0` in this case. - exec: Option<(OSString, Vec>)> +exec: Option<(OSString, Vec>)> } trait ModeReturn: Send { fn get_fd_path(&self) -> &Path; @@ -499,12 +499,27 @@ fn close_fileno(fd: T) -> eyre::Result<()> } } +fn parse_args() -> eyre::Result +{ + args::parse_args() + .wrap_err("Parsing arguments failed") + .with_section(|| std::env::args_os().skip(1) + .map(|x| std::borrow::Cow::Owned(String::from_utf8_lossy(&x.into_vec()).into_owned())) + .join_by_clone(std::borrow::Cow::Borrowed(" ")) //XXX: this can be replaced by `flat_map() -> [x, " "]` really... Dunno which will be faster... + .collect::() + .header("The program arguments were")) + .with_suggestion(|| "Try passing `--help`") +} + #[cfg_attr(feature="logging", instrument(err))] fn main() -> eyre::Result<()> { init()?; feature_check()?; if_trace!(debug!("initialised")); + //TODO: How to cleanly feature-gate `args`? + let opt = parse_args()?; + //TODO: maybe look into fd SEALing? Maybe we can prevent a consumer process from reading from stdout until we've finished the transfer. The name SEAL sounds like it might have something to do with that? cfg_if!{ if #[cfg(feature="memfile")] {