From d0a2a02eeaeb07737702d2a93289417d6b8df3f0 Mon Sep 17 00:00:00 2001 From: Leonhard Kipp Date: Wed, 27 Jan 2021 18:31:29 +0100 Subject: [PATCH] Add possibility to declare optional parameters and switch flags (#2966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add possibility to declare optional parameters and switch flags With this commit applied it is now possible to specify optional parameters and flags as switches. This PR **only** makes guarantees about **parsing** optional flags and switches correctly. This PR **does not guarantee flawless functionality** of optional parameters / switches within scripts. functionality within scripts. Example: test.nu ```shell def my_command [ opt_param? opt_param2?: int --switch ] {echo hi nushell} ``` ```shell > source test.nu > my_command -h ───┬───────── 0 │ hi 1 │ nushell ───┴───────── Usage: > my_command (opt_param) (opt_param2) {flags} Parameters: (opt_param) (opt_param2) Flags: -h, --help: Display this help message --switch --opt_flag ``` * Update def docs --- .../src/parse/def/param_flag_list.rs | 216 +++++++++++------- docs/commands/def.md | 60 ++++- 2 files changed, 193 insertions(+), 83 deletions(-) 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 227b5a9e2..ccdfc883f 100644 --- a/crates/nu-parser/src/parse/def/param_flag_list.rs +++ b/crates/nu-parser/src/parse/def/param_flag_list.rs @@ -1,12 +1,15 @@ -///This module contains functions to parse the parameter and flag list (signature) of a -///definition +///This module contains functions to parse the parameter and flag list (signature) ///Such a signature can be of the following format: -/// [ (parameter | flag | )* ] +/// [ (parameter | flag | rest_param | )* ] ///Where ///parameter is: -/// name (<:> type)? (<,> | | (#Comment ))? +/// name (<:> type)? ()? item_end ///flag is: -/// --name (-shortform)? (<:> type)? (<,> | | (#Comment ))? +/// --name (-shortform)? (<:> type)? item_end +///rest is: +/// ...rest (<:> type)? item_end +///item_end: +/// (<,>)? (#Comment)? ()? /// use log::debug; @@ -49,7 +52,7 @@ pub fn parse_signature( //After normal lexing, tokens also need to be split on ',' and ':' //TODO this could probably be all done in a specialized lexing function - let tokens = lex_split_baseline_tokens_on(tokens, &[',', ':']); + let tokens = lex_split_baseline_tokens_on(tokens, &[',', ':', '?']); let tokens = lex_split_shortflag_from_longflag(tokens); debug!("Tokens are {:?}", tokens); @@ -100,26 +103,43 @@ fn parse_parameter( } let mut err: Option = None; - //1 because name = tokens[0] - let mut i = 1; + let mut i = 0; + let mut type_ = SyntaxShape::Any; + let mut comment = None; + let mut optional = false; let (name, error) = parse_param_name(&tokens[0]); + i += 1; err = err.or(error); - let (type_, advanced_by, error) = parse_optional_type(&tokens[i..]); - let type_ = type_.unwrap_or(SyntaxShape::Any); - err = err.or(error); - i += advanced_by; + if i < tokens.len() { + let (parsed_opt_modifier, advanced_by) = + parse_optional_parameter_optional_modifier(&tokens[i]); + optional = parsed_opt_modifier; + i += advanced_by; + } - let (comment_text, advanced_by, error) = parse_signature_item_end(&tokens[i..]); - i += advanced_by; - err = err.or(error); + if i < tokens.len() { + let (parsed_type_, advanced_by, error) = parse_optional_type(&tokens[i..]); + type_ = parsed_type_.unwrap_or(SyntaxShape::Any); + err = err.or(error); + i += advanced_by; + } - let parameter = Parameter::new( - PositionalType::mandatory(&name.item, type_), - comment_text, - name.span, - ); + if i < tokens.len() { + let (comment_text, advanced_by, error) = parse_signature_item_end(&tokens[i..]); + comment = comment_text; + i += advanced_by; + err = err.or(error); + } + + let pos_type = if optional { + PositionalType::optional(&name.item, type_) + } else { + PositionalType::mandatory(&name.item, type_) + }; + + let parameter = Parameter::new(pos_type, comment, name.span); debug!( "Parsed parameter: {} with shape {:?}", @@ -143,32 +163,47 @@ fn parse_flag( } let mut err: Option = None; - //1 because name = tokens[0] - let mut i = 1; + let mut i = 0; + let mut shortform = None; + let mut type_ = None; + let mut comment = None; let (name, error) = parse_flag_name(&tokens[0]); err = err.or(error); + i += 1; - let (shortform, advanced_by, error) = parse_flag_optional_shortform(&tokens[i..]); - i += advanced_by; - err = err.or(error); + if i < tokens.len() { + let (parsed_shortform, advanced_by, error) = parse_flag_optional_shortform(&tokens[i..]); + shortform = parsed_shortform; + i += advanced_by; + err = err.or(error); + } - let (type_, advanced_by, error) = parse_optional_type(&tokens[i..]); - let type_ = type_.unwrap_or(SyntaxShape::Any); - err = err.or(error); - i += advanced_by; + if i < tokens.len() { + let (parsed_type, advanced_by, error) = parse_optional_type(&tokens[i..]); + type_ = parsed_type; + i += advanced_by; + err = err.or(error); + } - let (comment, advanced_by, error) = parse_signature_item_end(&tokens[i..]); - i += advanced_by; - err = err.or(error); + if i < tokens.len() { + let (parsed_comment, advanced_by, error) = parse_signature_item_end(&tokens[i..]); + comment = parsed_comment; + i += advanced_by; + err = err.or(error); + } - //TODO Fixup span - let flag = Flag::new( - name.item.clone(), - NamedType::Optional(shortform, type_), - comment, - name.span, - ); + //If no type is given, the flag is a switch. Otherwise its optional + //Example: + //--verbose(-v) # Switch + //--output(-o): path # Optional flag + let named_type = if let Some(shape) = type_ { + NamedType::Optional(shortform, shape) + } else { + NamedType::Switch(shortform) + }; + + let flag = Flag::new(name.item.clone(), named_type, comment, name.span); debug!("Parsed flag: {:?}", flag); (flag, i, err) @@ -195,19 +230,25 @@ fn parse_rest( let mut err = None; let mut i = 0; + let mut type_ = SyntaxShape::Any; + let mut comment = "".to_string(); 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); + if i < tokens.len() { + let (parsed_type, advanced_by, error) = parse_optional_type(&tokens[i..]); + err = err.or(error); + i += advanced_by; + type_ = parsed_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()); + if i < tokens.len() { + let (parsed_comment, advanced_by) = parse_optional_comment(&tokens[i..]); + i += advanced_by; + comment = parsed_comment.unwrap_or_else(|| "".to_string()); + } return (Some((type_, comment)), i, err); @@ -320,8 +361,20 @@ fn parse_optional_type(tokens: &[Token]) -> (Option, usize, Option< (type_, i, err) } +///Parse token if it is a modifier to make +fn parse_optional_parameter_optional_modifier(token: &Token) -> (bool, usize) { + fn is_questionmark(token: &Token) -> bool { + is_baseline_token_matching(token, "?") + } + if is_questionmark(token) { + (true, 1) + } else { + (false, 0) + } +} + ///Parses the end of a flag or a parameter -/// ((<,> | ) | (#Comment ) +/// (<,>)? (#Comment)? ()? fn parse_signature_item_end(tokens: &[Token]) -> (Option, usize, Option) { if tokens.is_empty() { //If no more tokens, parameter/flag doesn't need ',' or comment to be properly finished @@ -546,7 +599,7 @@ fn lex_split_shortflag_from_longflag(tokens: Vec) -> Vec { } result } -//Currently the lexer does not split baselines on ',' ':' +//Currently the lexer does not split baselines on ',' ':' '?' //The parameter list requires this. Therefore here is a hacky method doing this. fn lex_split_baseline_tokens_on( tokens: Vec, @@ -672,14 +725,14 @@ mod tests { #[test] fn simple_def_with_params() { let name = "my_func"; - let sign = "[param1:int, param2:string]"; + let sign = "[param1?: int, param2: string]"; let (sign, err) = parse_signature(name, &sign.to_string().spanned(Span::new(0, 27))); assert!(err.is_none()); assert_eq!( sign.positional, vec![ ( - PositionalType::Mandatory("param1".into(), SyntaxShape::Int), + PositionalType::Optional("param1".into(), SyntaxShape::Int), "".into() ), ( @@ -690,6 +743,27 @@ mod tests { ); } + #[test] + fn simple_def_with_optional_param_without_type() { + let name = "my_func"; + let sign = "[param1 ?, param2?]"; + let (sign, err) = parse_signature(name, &sign.to_string().spanned(Span::new(0, 27))); + assert!(err.is_none()); + assert_eq!( + sign.positional, + vec![ + ( + PositionalType::Optional("param1".into(), SyntaxShape::Any), + "".into() + ), + ( + PositionalType::Optional("param2".into(), SyntaxShape::Any), + "".into() + ), + ] + ); + } + #[test] fn simple_def_with_params_with_comment() { let name = "my_func"; @@ -802,8 +876,8 @@ mod tests { let sign = "[ --list (-l) : path # First flag --verbose : number # Second flag + --all(-a) # My switch ]"; - // --all(-a) # My switch let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); assert!(err.is_none()); assert_signature_has_flag( @@ -818,12 +892,7 @@ mod tests { NamedType::Optional(None, SyntaxShape::Number), "Second flag", ); - // assert_signature_has_flag( - // &sign, - // "verbose", - // NamedType::Switch(Some('a')), - // "Second flag", - // ); + assert_signature_has_flag(&sign, "all", NamedType::Switch(Some('a')), "My switch"); } #[test] @@ -835,6 +904,7 @@ mod tests { --verbose # Second flag param3 : number, --flag3 # Third flag + param4 ?: table # Optional Param ]"; let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); assert!(err.is_none()); @@ -844,25 +914,10 @@ mod tests { NamedType::Optional(Some('l'), SyntaxShape::FilePath), "First flag", ); - assert_signature_has_flag( - &sign, - "verbose", - NamedType::Optional(None, SyntaxShape::Any), - "Second flag", - ); - assert_signature_has_flag( - &sign, - "flag3", - NamedType::Optional(None, SyntaxShape::Any), - "Third flag", - ); + assert_signature_has_flag(&sign, "verbose", NamedType::Switch(None), "Second flag"); + assert_signature_has_flag(&sign, "flag3", NamedType::Switch(None), "Third flag"); assert_eq!( sign.positional, - // --list (-l) : path # First flag - // param1, param2:table # Param2 Doc - // --verbose # Second flag - // param3 : number, - // --flag3 # Third flag vec![ ( PositionalType::Mandatory("param1".into(), SyntaxShape::Any), @@ -876,6 +931,10 @@ mod tests { PositionalType::Mandatory("param3".into(), SyntaxShape::Number), "".into() ), + ( + PositionalType::Optional("param4".into(), SyntaxShape::Table), + "Optional Param".into() + ), ] ); } @@ -888,12 +947,7 @@ mod tests { ]"; let (sign, err) = parse_signature(name, &sign.to_string().spanned_unknown()); assert!(err.is_none()); - assert_signature_has_flag( - &sign, - "force", - NamedType::Optional(Some('f'), SyntaxShape::Any), - "", - ); + assert_signature_has_flag(&sign, "force", NamedType::Switch(Some('f')), ""); assert_eq!( sign.positional, // --list (-l) : path # First flag @@ -1003,7 +1057,7 @@ mod tests { assert_signature_has_flag( &sign, "xxx", - NamedType::Optional(Some('x'), SyntaxShape::Any), + NamedType::Switch(Some('x')), "The all powerful x flag", ); assert_signature_has_flag( diff --git a/docs/commands/def.md b/docs/commands/def.md index 8570272d8..227c99a6c 100644 --- a/docs/commands/def.md +++ b/docs/commands/def.md @@ -4,14 +4,70 @@ Use `def` to create a custom command. ## Examples -``` +```shell > def my_command [] { echo hi nu } > my_command hi nu ``` -``` +```shell > def my_command [adjective: string, num: int] { echo $adjective $num meet nu } > my_command nice 2 nice 2 meet nu ``` + +```shell +def my_cookie_daemon [ + in: path # Specify where the cookie daemon shall look for cookies :p + ...rest: path # Other places to consider for cookie supplies + --output (-o): path # Where to store leftovers + --verbose +] { + echo $in $rest | each { eat $it } + ... +} +my_cookie_daemon /home/bob /home/alice --output /home/mallory +``` + +Further (and non trivial) examples can be found in our [nushell scripts repo](https://github.com/nushell/nu_scripts) + +## Syntax + +The syntax of the def command is as follows. +`def ` + +The signature is a list of parameters flags and at maximum one rest argument. You can specify the type of each of them by appending `: `. +Example: +```shell +def cmd [ +parameter: string +--flag: int +...rest: path +] { ... } +``` + +It is possible to comment them by appending `# Comment text`! +Example +```shell +def cmd [ +parameter # Paramter Comment +--flag: int # Flag comment +...rest: path # Rest comment +] { ... } +``` + +Flags can have a single character shorthand form. For example `--output` is often abbreviated by `-o`. You can declare a shorthand by writing `(-)` after the flag name. +Example +```shell +def cmd [ +--flag(-f): int # Flag comment +] { ... } +``` + +You can make a parameter optional by adding `?` to its name. Optional parameters do not need to be passed. +(TODO Handling optional parameters in scripts is WIP. Please don't expect it to work seamlessly) +```shell +def cmd [ +parameter?: path # Optional parameter +] { ... } +```