nu_protocol/
signature.rs

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