nu_protocol/
signature.rs

1use crate::{
2    engine::{Call, Command, CommandType, EngineState, Stack},
3    BlockId, Example, PipelineData, ShellError, SyntaxShape, Type, Value, VarId,
4};
5use nu_derive_value::FromValue;
6use serde::{Deserialize, Serialize};
7use std::fmt::Write;
8
9// Make nu_protocol available in this namespace, consumers of this crate will
10// have this without such an export.
11// The `FromValue` derive macro fully qualifies paths to "nu_protocol".
12use crate as nu_protocol;
13
14/// The signature definition of a named flag that either accepts a value or acts as a toggle flag
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct Flag {
17    pub long: String,
18    pub short: Option<char>,
19    pub arg: Option<SyntaxShape>,
20    pub required: bool,
21    pub desc: String,
22
23    // For custom commands
24    pub var_id: Option<VarId>,
25    pub default_value: Option<Value>,
26}
27
28/// The signature definition for a positional argument
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct PositionalArg {
31    pub name: String,
32    pub desc: String,
33    pub shape: SyntaxShape,
34
35    // For custom commands
36    pub var_id: Option<VarId>,
37    pub default_value: Option<Value>,
38}
39
40/// Command categories
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub enum Category {
43    Bits,
44    Bytes,
45    Chart,
46    Conversions,
47    Core,
48    Custom(String),
49    Database,
50    Date,
51    Debug,
52    Default,
53    Deprecated,
54    Removed,
55    Env,
56    Experimental,
57    FileSystem,
58    Filters,
59    Formats,
60    Generators,
61    Hash,
62    History,
63    Math,
64    Misc,
65    Network,
66    Path,
67    Platform,
68    Plugin,
69    Random,
70    Shells,
71    Strings,
72    System,
73    Viewers,
74}
75
76impl std::fmt::Display for Category {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        let msg = match self {
79            Category::Bits => "bits",
80            Category::Bytes => "bytes",
81            Category::Chart => "chart",
82            Category::Conversions => "conversions",
83            Category::Core => "core",
84            Category::Custom(name) => name,
85            Category::Database => "database",
86            Category::Date => "date",
87            Category::Debug => "debug",
88            Category::Default => "default",
89            Category::Deprecated => "deprecated",
90            Category::Removed => "removed",
91            Category::Env => "env",
92            Category::Experimental => "experimental",
93            Category::FileSystem => "filesystem",
94            Category::Filters => "filters",
95            Category::Formats => "formats",
96            Category::Generators => "generators",
97            Category::Hash => "hash",
98            Category::History => "history",
99            Category::Math => "math",
100            Category::Misc => "misc",
101            Category::Network => "network",
102            Category::Path => "path",
103            Category::Platform => "platform",
104            Category::Plugin => "plugin",
105            Category::Random => "random",
106            Category::Shells => "shells",
107            Category::Strings => "strings",
108            Category::System => "system",
109            Category::Viewers => "viewers",
110        };
111
112        write!(f, "{msg}")
113    }
114}
115
116pub fn category_from_string(category: &str) -> Category {
117    match category {
118        "bits" => Category::Bits,
119        "bytes" => Category::Bytes,
120        "chart" => Category::Chart,
121        "conversions" => Category::Conversions,
122        // Let's protect our own "core" commands by preventing scripts from having this category.
123        "core" => Category::Custom("custom_core".to_string()),
124        "database" => Category::Database,
125        "date" => Category::Date,
126        "debug" => Category::Debug,
127        "default" => Category::Default,
128        "deprecated" => Category::Deprecated,
129        "removed" => Category::Removed,
130        "env" => Category::Env,
131        "experimental" => Category::Experimental,
132        "filesystem" => Category::FileSystem,
133        "filter" => Category::Filters,
134        "formats" => Category::Formats,
135        "generators" => Category::Generators,
136        "hash" => Category::Hash,
137        "history" => Category::History,
138        "math" => Category::Math,
139        "misc" => Category::Misc,
140        "network" => Category::Network,
141        "path" => Category::Path,
142        "platform" => Category::Platform,
143        "plugin" => Category::Plugin,
144        "random" => Category::Random,
145        "shells" => Category::Shells,
146        "strings" => Category::Strings,
147        "system" => Category::System,
148        "viewers" => Category::Viewers,
149        _ => Category::Custom(category.to_string()),
150    }
151}
152
153/// Signature information of a [`Command`]
154#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct Signature {
156    pub name: String,
157    pub description: String,
158    pub extra_description: String,
159    pub search_terms: Vec<String>,
160    pub required_positional: Vec<PositionalArg>,
161    pub optional_positional: Vec<PositionalArg>,
162    pub rest_positional: Option<PositionalArg>,
163    pub named: Vec<Flag>,
164    pub input_output_types: Vec<(Type, Type)>,
165    pub allow_variants_without_examples: bool,
166    pub is_filter: bool,
167    pub creates_scope: bool,
168    pub allows_unknown_args: bool,
169    // Signature category used to classify commands stored in the list of declarations
170    pub category: Category,
171}
172
173impl PartialEq for Signature {
174    fn eq(&self, other: &Self) -> bool {
175        self.name == other.name
176            && self.description == other.description
177            && self.required_positional == other.required_positional
178            && self.optional_positional == other.optional_positional
179            && self.rest_positional == other.rest_positional
180            && self.is_filter == other.is_filter
181    }
182}
183
184impl Eq for Signature {}
185
186impl Signature {
187    pub fn new(name: impl Into<String>) -> Signature {
188        Signature {
189            name: name.into(),
190            description: String::new(),
191            extra_description: String::new(),
192            search_terms: vec![],
193            required_positional: vec![],
194            optional_positional: vec![],
195            rest_positional: None,
196            input_output_types: vec![],
197            allow_variants_without_examples: false,
198            named: vec![],
199            is_filter: false,
200            creates_scope: false,
201            category: Category::Default,
202            allows_unknown_args: false,
203        }
204    }
205
206    // Gets the input type from the signature
207    pub fn get_input_type(&self) -> Type {
208        match self.input_output_types.len() {
209            0 => Type::Any,
210            1 => self.input_output_types[0].0.clone(),
211            _ => {
212                let first = &self.input_output_types[0].0;
213                if self
214                    .input_output_types
215                    .iter()
216                    .all(|(input, _)| input == first)
217                {
218                    first.clone()
219                } else {
220                    Type::Any
221                }
222            }
223        }
224    }
225
226    // Gets the output type from the signature
227    pub fn get_output_type(&self) -> Type {
228        match self.input_output_types.len() {
229            0 => Type::Any,
230            1 => self.input_output_types[0].1.clone(),
231            _ => {
232                let first = &self.input_output_types[0].1;
233                if self
234                    .input_output_types
235                    .iter()
236                    .all(|(_, output)| output == first)
237                {
238                    first.clone()
239                } else {
240                    Type::Any
241                }
242            }
243        }
244    }
245
246    // Add a default help option to a signature
247    pub fn add_help(mut self) -> Signature {
248        // default help flag
249        let flag = Flag {
250            long: "help".into(),
251            short: Some('h'),
252            arg: None,
253            desc: "Display the help message for this command".into(),
254            required: false,
255            var_id: None,
256            default_value: None,
257        };
258        self.named.push(flag);
259        self
260    }
261
262    // Build an internal signature with default help option
263    pub fn build(name: impl Into<String>) -> Signature {
264        Signature::new(name.into()).add_help()
265    }
266
267    /// Add a description to the signature
268    ///
269    /// This should be a single sentence as it is the part shown for example in the completion
270    /// menu.
271    pub fn description(mut self, msg: impl Into<String>) -> Signature {
272        self.description = msg.into();
273        self
274    }
275
276    /// Add an extra description to the signature.
277    ///
278    /// Here additional documentation can be added
279    pub fn extra_description(mut self, msg: impl Into<String>) -> Signature {
280        self.extra_description = msg.into();
281        self
282    }
283
284    /// Add search terms to the signature
285    pub fn search_terms(mut self, terms: Vec<String>) -> Signature {
286        self.search_terms = terms;
287        self
288    }
289
290    /// Update signature's fields from a Command trait implementation
291    pub fn update_from_command(mut self, command: &dyn Command) -> Signature {
292        self.search_terms = command
293            .search_terms()
294            .into_iter()
295            .map(|term| term.to_string())
296            .collect();
297        self.extra_description = command.extra_description().to_string();
298        self.description = command.description().to_string();
299        self
300    }
301
302    /// Allow unknown signature parameters
303    pub fn allows_unknown_args(mut self) -> Signature {
304        self.allows_unknown_args = true;
305        self
306    }
307
308    /// Add a required positional argument to the signature
309    pub fn required(
310        mut self,
311        name: impl Into<String>,
312        shape: impl Into<SyntaxShape>,
313        desc: impl Into<String>,
314    ) -> Signature {
315        self.required_positional.push(PositionalArg {
316            name: name.into(),
317            desc: desc.into(),
318            shape: shape.into(),
319            var_id: None,
320            default_value: None,
321        });
322
323        self
324    }
325
326    /// Add an optional positional argument to the signature
327    pub fn optional(
328        mut self,
329        name: impl Into<String>,
330        shape: impl Into<SyntaxShape>,
331        desc: impl Into<String>,
332    ) -> Signature {
333        self.optional_positional.push(PositionalArg {
334            name: name.into(),
335            desc: desc.into(),
336            shape: shape.into(),
337            var_id: None,
338            default_value: None,
339        });
340
341        self
342    }
343
344    pub fn rest(
345        mut self,
346        name: &str,
347        shape: impl Into<SyntaxShape>,
348        desc: impl Into<String>,
349    ) -> Signature {
350        self.rest_positional = Some(PositionalArg {
351            name: name.into(),
352            desc: desc.into(),
353            shape: shape.into(),
354            var_id: None,
355            default_value: None,
356        });
357
358        self
359    }
360
361    /// Is this command capable of operating on its input via cell paths?
362    pub fn operates_on_cell_paths(&self) -> bool {
363        self.required_positional
364            .iter()
365            .chain(self.rest_positional.iter())
366            .any(|pos| {
367                matches!(
368                    pos,
369                    PositionalArg {
370                        shape: SyntaxShape::CellPath,
371                        ..
372                    }
373                )
374            })
375    }
376
377    /// Add an optional named flag argument to the signature
378    pub fn named(
379        mut self,
380        name: impl Into<String>,
381        shape: impl Into<SyntaxShape>,
382        desc: impl Into<String>,
383        short: Option<char>,
384    ) -> Signature {
385        let (name, s) = self.check_names(name, short);
386
387        self.named.push(Flag {
388            long: name,
389            short: s,
390            arg: Some(shape.into()),
391            required: false,
392            desc: desc.into(),
393            var_id: None,
394            default_value: None,
395        });
396
397        self
398    }
399
400    /// Add a required named flag argument to the signature
401    pub fn required_named(
402        mut self,
403        name: impl Into<String>,
404        shape: impl Into<SyntaxShape>,
405        desc: impl Into<String>,
406        short: Option<char>,
407    ) -> Signature {
408        let (name, s) = self.check_names(name, short);
409
410        self.named.push(Flag {
411            long: name,
412            short: s,
413            arg: Some(shape.into()),
414            required: true,
415            desc: desc.into(),
416            var_id: None,
417            default_value: None,
418        });
419
420        self
421    }
422
423    /// Add a switch to the signature
424    pub fn switch(
425        mut self,
426        name: impl Into<String>,
427        desc: impl Into<String>,
428        short: Option<char>,
429    ) -> Signature {
430        let (name, s) = self.check_names(name, short);
431
432        self.named.push(Flag {
433            long: name,
434            short: s,
435            arg: None,
436            required: false,
437            desc: desc.into(),
438            var_id: None,
439            default_value: None,
440        });
441
442        self
443    }
444
445    /// Changes the input type of the command signature
446    pub fn input_output_type(mut self, input_type: Type, output_type: Type) -> Signature {
447        self.input_output_types.push((input_type, output_type));
448        self
449    }
450
451    /// Set the input-output type signature variants of the command
452    pub fn input_output_types(mut self, input_output_types: Vec<(Type, Type)>) -> Signature {
453        self.input_output_types = input_output_types;
454        self
455    }
456
457    /// Changes the signature category
458    pub fn category(mut self, category: Category) -> Signature {
459        self.category = category;
460
461        self
462    }
463
464    /// Sets that signature will create a scope as it parses
465    pub fn creates_scope(mut self) -> Signature {
466        self.creates_scope = true;
467        self
468    }
469
470    // Is it allowed for the type signature to feature a variant that has no corresponding example?
471    pub fn allow_variants_without_examples(mut self, allow: bool) -> Signature {
472        self.allow_variants_without_examples = allow;
473        self
474    }
475
476    pub fn call_signature(&self) -> String {
477        let mut one_liner = String::new();
478        one_liner.push_str(&self.name);
479        one_liner.push(' ');
480
481        // Note: the call signature needs flags first because on the nu commandline,
482        // flags will precede the script file name. Flags for internal commands can come
483        // either before or after (or around) positional parameters, so there isn't a strong
484        // preference, so we default to the more constrained example.
485        if self.named.len() > 1 {
486            one_liner.push_str("{flags} ");
487        }
488
489        for positional in &self.required_positional {
490            one_liner.push_str(&get_positional_short_name(positional, true));
491        }
492        for positional in &self.optional_positional {
493            one_liner.push_str(&get_positional_short_name(positional, false));
494        }
495
496        if let Some(rest) = &self.rest_positional {
497            let _ = write!(one_liner, "...{}", get_positional_short_name(rest, false));
498        }
499
500        // if !self.subcommands.is_empty() {
501        //     one_liner.push_str("<subcommand> ");
502        // }
503
504        one_liner
505    }
506
507    /// Get list of the short-hand flags
508    pub fn get_shorts(&self) -> Vec<char> {
509        self.named.iter().filter_map(|f| f.short).collect()
510    }
511
512    /// Get list of the long-hand flags
513    pub fn get_names(&self) -> Vec<&str> {
514        self.named.iter().map(|f| f.long.as_str()).collect()
515    }
516
517    /// Checks if short or long are already present
518    /// Panics if one of them is found
519    fn check_names(&self, name: impl Into<String>, short: Option<char>) -> (String, Option<char>) {
520        let s = short.inspect(|c| {
521            assert!(
522                !self.get_shorts().contains(c),
523                "There may be duplicate short flags for '-{}'",
524                c
525            );
526        });
527
528        let name = {
529            let name: String = name.into();
530            assert!(
531                !self.get_names().contains(&name.as_str()),
532                "There may be duplicate name flags for '--{}'",
533                name
534            );
535            name
536        };
537
538        (name, s)
539    }
540
541    pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> {
542        if position < self.required_positional.len() {
543            self.required_positional.get(position)
544        } else if position < (self.required_positional.len() + self.optional_positional.len()) {
545            self.optional_positional
546                .get(position - self.required_positional.len())
547        } else {
548            self.rest_positional.as_ref()
549        }
550    }
551
552    pub fn num_positionals(&self) -> usize {
553        let mut total = self.required_positional.len() + self.optional_positional.len();
554
555        for positional in &self.required_positional {
556            if let SyntaxShape::Keyword(..) = positional.shape {
557                // Keywords have a required argument, so account for that
558                total += 1;
559            }
560        }
561        for positional in &self.optional_positional {
562            if let SyntaxShape::Keyword(..) = positional.shape {
563                // Keywords have a required argument, so account for that
564                total += 1;
565            }
566        }
567        total
568    }
569
570    /// Find the matching long flag
571    pub fn get_long_flag(&self, name: &str) -> Option<Flag> {
572        for flag in &self.named {
573            if flag.long == name {
574                return Some(flag.clone());
575            }
576        }
577        None
578    }
579
580    /// Find the matching long flag
581    pub fn get_short_flag(&self, short: char) -> Option<Flag> {
582        for flag in &self.named {
583            if let Some(short_flag) = &flag.short {
584                if *short_flag == short {
585                    return Some(flag.clone());
586                }
587            }
588        }
589        None
590    }
591
592    /// Set the filter flag for the signature
593    pub fn filter(mut self) -> Signature {
594        self.is_filter = true;
595        self
596    }
597
598    /// Create a placeholder implementation of Command as a way to predeclare a definition's
599    /// signature so other definitions can see it. This placeholder is later replaced with the
600    /// full definition in a second pass of the parser.
601    pub fn predeclare(self) -> Box<dyn Command> {
602        Box::new(Predeclaration { signature: self })
603    }
604
605    /// Combines a signature and a block into a runnable block
606    pub fn into_block_command(
607        self,
608        block_id: BlockId,
609        attributes: Vec<(String, Value)>,
610        examples: Vec<CustomExample>,
611    ) -> Box<dyn Command> {
612        Box::new(BlockCommand {
613            signature: self,
614            block_id,
615            attributes,
616            examples,
617        })
618    }
619
620    pub fn formatted_flags(self) -> String {
621        if self.named.len() < 11 {
622            let mut s = "Available flags:".to_string();
623            for flag in self.named {
624                if let Some(short) = flag.short {
625                    let _ = write!(s, " --{}(-{}),", flag.long, short);
626                } else {
627                    let _ = write!(s, " --{},", flag.long);
628                }
629            }
630            s.remove(s.len() - 1);
631            let _ = write!(s, ". Use `--help` for more information.");
632            s
633        } else {
634            let mut s = "Some available flags:".to_string();
635            for flag in self.named {
636                if let Some(short) = flag.short {
637                    let _ = write!(s, " --{}(-{}),", flag.long, short);
638                } else {
639                    let _ = write!(s, " --{},", flag.long);
640                }
641            }
642            s.remove(s.len() - 1);
643            let _ = write!(
644                s,
645                "... Use `--help` for a full list of flags and more information."
646            );
647            s
648        }
649    }
650}
651
652#[derive(Clone)]
653struct Predeclaration {
654    signature: Signature,
655}
656
657impl Command for Predeclaration {
658    fn name(&self) -> &str {
659        &self.signature.name
660    }
661
662    fn signature(&self) -> Signature {
663        self.signature.clone()
664    }
665
666    fn description(&self) -> &str {
667        &self.signature.description
668    }
669
670    fn extra_description(&self) -> &str {
671        &self.signature.extra_description
672    }
673
674    fn run(
675        &self,
676        _engine_state: &EngineState,
677        _stack: &mut Stack,
678        _call: &Call,
679        _input: PipelineData,
680    ) -> Result<PipelineData, crate::ShellError> {
681        panic!("Internal error: can't run a predeclaration without a body")
682    }
683}
684
685fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String {
686    match &arg.shape {
687        SyntaxShape::Keyword(name, ..) => {
688            if is_required {
689                format!("{} <{}> ", String::from_utf8_lossy(name), arg.name)
690            } else {
691                format!("({} <{}>) ", String::from_utf8_lossy(name), arg.name)
692            }
693        }
694        _ => {
695            if is_required {
696                format!("<{}> ", arg.name)
697            } else {
698                format!("({}) ", arg.name)
699            }
700        }
701    }
702}
703
704#[derive(Clone, FromValue)]
705pub struct CustomExample {
706    pub example: String,
707    pub description: String,
708    pub result: Option<Value>,
709}
710
711impl CustomExample {
712    pub fn to_example(&self) -> Example<'_> {
713        Example {
714            example: self.example.as_str(),
715            description: self.description.as_str(),
716            result: self.result.clone(),
717        }
718    }
719}
720
721#[derive(Clone)]
722struct BlockCommand {
723    signature: Signature,
724    block_id: BlockId,
725    attributes: Vec<(String, Value)>,
726    examples: Vec<CustomExample>,
727}
728
729impl Command for BlockCommand {
730    fn name(&self) -> &str {
731        &self.signature.name
732    }
733
734    fn signature(&self) -> Signature {
735        self.signature.clone()
736    }
737
738    fn description(&self) -> &str {
739        &self.signature.description
740    }
741
742    fn extra_description(&self) -> &str {
743        &self.signature.extra_description
744    }
745
746    fn run(
747        &self,
748        _engine_state: &EngineState,
749        _stack: &mut Stack,
750        _call: &Call,
751        _input: PipelineData,
752    ) -> Result<crate::PipelineData, crate::ShellError> {
753        Err(ShellError::GenericError {
754            error: "Internal error: can't run custom command with 'run', use block_id".into(),
755            msg: "".into(),
756            span: None,
757            help: None,
758            inner: vec![],
759        })
760    }
761
762    fn command_type(&self) -> CommandType {
763        CommandType::Custom
764    }
765
766    fn block_id(&self) -> Option<BlockId> {
767        Some(self.block_id)
768    }
769
770    fn attributes(&self) -> Vec<(String, Value)> {
771        self.attributes.clone()
772    }
773
774    fn examples(&self) -> Vec<Example> {
775        self.examples
776            .iter()
777            .map(CustomExample::to_example)
778            .collect()
779    }
780
781    fn search_terms(&self) -> Vec<&str> {
782        self.signature
783            .search_terms
784            .iter()
785            .map(String::as_str)
786            .collect()
787    }
788}