From 71b99edd481e733f243405292147e3c43794901a Mon Sep 17 00:00:00 2001 From: Leonhard Kipp Date: Fri, 22 Jan 2021 19:13:29 +0100 Subject: [PATCH] parser/add rest args to def (#2961) * Add rest arg to def This commit applied adds the ability to define the rest parameter of a def command. It does not implement the functionality to expand the rest argument in a user defined def function. The rest argument has to be exactly worded "...rest". Example after this PR is applied: file test.nu ```shell def my_command [ ...rest:int # My rest arg ] { echo 1 2 3 } ``` ```shell > source test.nu > my_command -h Usage: > my_command ...args {flags} Parameters: ...args: My rest arg Flags: -h, --help: Display this help message ``` * Fix space in help on wrong side --- crates/nu-engine/src/documentation.rs | 2 +- .../src/parse/def/param_flag_list.rs | 147 +++++++++++++++++- crates/nu-source/src/meta.rs | 8 +- 3 files changed, 152 insertions(+), 5 deletions(-) diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index 045acae614..8e98397e21 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -160,7 +160,7 @@ pub fn get_documentation( } if signature.rest_positional.is_some() { - one_liner.push_str(" ...args"); + one_liner.push_str("...args "); } if !subcommands.is_empty() { diff --git a/crates/nu-parser/src/parse/def/param_flag_list.rs b/crates/nu-parser/src/parse/def/param_flag_list.rs index 5144440e15..227b5a9e22 100644 --- a/crates/nu-parser/src/parse/def/param_flag_list.rs +++ b/crates/nu-parser/src/parse/def/param_flag_list.rs @@ -55,6 +55,7 @@ pub fn parse_signature( let mut parameters = vec![]; let mut flags = vec![]; + let mut rest = None; let mut i = 0; while i < tokens.len() { @@ -66,6 +67,11 @@ pub fn parse_signature( err = err.or(error); i += advanced_by; flags.push(flag); + } else if is_rest(&tokens[i]) { + let (rest_, advanced_by, error) = parse_rest(&tokens[i..], signature_vec); + err = err.or(error); + i += advanced_by; + rest = rest_; } else { let (parameter, advanced_by, error) = parse_parameter(&tokens[i..], signature_vec); err = err.or(error); @@ -74,7 +80,7 @@ pub fn parse_signature( } } - let signature = to_signature(name, parameters, flags); + let signature = to_signature(name, parameters, flags, rest); debug!("Signature: {:?}", signature); (signature, err) @@ -168,6 +174,68 @@ fn parse_flag( (flag, i, err) } +fn parse_rest( + tokens: &[Token], + tokens_as_str: &Spanned, +) -> ( + Option<(SyntaxShape, Description)>, + usize, + Option, +) { + if tokens.is_empty() { + return ( + None, + 0, + Some(ParseError::unexpected_eof( + "rest argument", + tokens_as_str.span, + )), + ); + } + + let mut err = None; + let mut i = 0; + + let error = parse_rest_name(&tokens[i]); + err = err.or(error); + i += 1; + + let (type_, advanced_by, error) = parse_optional_type(&tokens[i..]); + err = err.or(error); + i += advanced_by; + let type_ = type_.unwrap_or(SyntaxShape::Any); + + let (comment, advanced_by) = parse_optional_comment(&tokens[i..]); + i += advanced_by; + let comment = comment.unwrap_or_else(|| "".to_string()); + + return (Some((type_, comment)), i, err); + + fn parse_rest_name(name_token: &Token) -> Option { + return if let TokenContents::Baseline(name) = &name_token.contents { + if !name.starts_with("...") { + parse_rest_name_err(name_token) + } else if !name.starts_with("...rest") { + Some(ParseError::mismatch( + "rest argument name to be 'rest'", + token_to_spanned_string(name_token), + )) + } else { + None + } + } else { + parse_rest_name_err(name_token) + }; + + fn parse_rest_name_err(token: &Token) -> Option { + Some(ParseError::mismatch( + "...rest", + token_to_spanned_string(token), + )) + } + } +} + fn parse_type(type_: &Spanned) -> (SyntaxShape, Option) { debug!("Parsing type {:?}", type_); match type_.item.as_str() { @@ -396,6 +464,14 @@ fn parse_comma(tokens: &[Token]) -> (bool, usize) { } } +///Returns true if token potentially represents rest argument +fn is_rest(token: &Token) -> bool { + match &token.contents { + TokenContents::Baseline(item) => item.starts_with("..."), + _ => false, + } +} + ///True for short or longform flags. False otherwise fn is_flag(token: &Token) -> bool { match &token.contents { @@ -404,7 +480,12 @@ fn is_flag(token: &Token) -> bool { } } -fn to_signature(name: &str, params: Vec, flags: Vec) -> Signature { +fn to_signature( + name: &str, + params: Vec, + flags: Vec, + rest: Option<(SyntaxShape, Description)>, +) -> Signature { let mut sign = Signature::new(name); for param in params.into_iter() { @@ -420,6 +501,8 @@ fn to_signature(name: &str, params: Vec, flags: Vec) -> Signatu ); } + sign.rest_positional = rest; + sign } @@ -881,4 +964,64 @@ mod tests { "The all powerful x flag", ); } + + #[test] + fn simple_def_with_rest_arg() { + let name = "my_func"; + let sign = "[ ...rest]"; + let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); + assert!(err.is_none()); + assert_eq!( + sign.rest_positional, + Some((SyntaxShape::Any, "".to_string())) + ); + } + + #[test] + fn simple_def_with_rest_arg_with_type_and_comment() { + let name = "my_func"; + let sign = "[ ...rest:path # My super cool rest arg]"; + let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); + assert!(err.is_none()); + assert_eq!( + sign.rest_positional, + Some((SyntaxShape::FilePath, "My super cool rest arg".to_string())) + ); + } + + #[test] + fn simple_def_with_param_flag_and_rest() { + let name = "my_func"; + let sign = "[ + d:string # The required d parameter + --xxx(-x) # The all powerful x flag + --yyy (-y):int # The accompanying y flag + ...rest:table # Another rest + ]"; + let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); + assert!(err.is_none()); + assert_signature_has_flag( + &sign, + "xxx", + NamedType::Optional(Some('x'), SyntaxShape::Any), + "The all powerful x flag", + ); + assert_signature_has_flag( + &sign, + "yyy", + NamedType::Optional(Some('y'), SyntaxShape::Int), + "The accompanying y flag", + ); + assert_eq!( + sign.positional, + vec![( + PositionalType::Mandatory("d".into(), SyntaxShape::String), + "The required d parameter".into() + )] + ); + assert_eq!( + sign.rest_positional, + Some((SyntaxShape::Table, "Another rest".to_string())) + ); + } } diff --git a/crates/nu-source/src/meta.rs b/crates/nu-source/src/meta.rs index fab1deab9b..66210ac0c1 100644 --- a/crates/nu-source/src/meta.rs +++ b/crates/nu-source/src/meta.rs @@ -48,14 +48,18 @@ impl Spanned { ) -> impl Iterator { items.map(|item| &item.item[..]) } -} -impl Spanned { /// Borrows the contained String pub fn borrow_spanned(&self) -> Spanned<&str> { let span = self.span; self.item[..].spanned(span) } + + pub fn slice_spanned(&self, span: impl Into) -> Spanned<&str> { + let span = span.into(); + let item = &self.item[span.start()..span.end()]; + item.spanned(span) + } } pub trait SpannedItem: Sized {