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 '-{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 '--{name}'"
533            );
534            name
535        };
536
537        (name, s)
538    }
539
540    pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> {
541        if position < self.required_positional.len() {
542            self.required_positional.get(position)
543        } else if position < (self.required_positional.len() + self.optional_positional.len()) {
544            self.optional_positional
545                .get(position - self.required_positional.len())
546        } else {
547            self.rest_positional.as_ref()
548        }
549    }
550
551    pub fn num_positionals(&self) -> usize {
552        let mut total = self.required_positional.len() + self.optional_positional.len();
553
554        for positional in &self.required_positional {
555            if let SyntaxShape::Keyword(..) = positional.shape {
556                // Keywords have a required argument, so account for that
557                total += 1;
558            }
559        }
560        for positional in &self.optional_positional {
561            if let SyntaxShape::Keyword(..) = positional.shape {
562                // Keywords have a required argument, so account for that
563                total += 1;
564            }
565        }
566        total
567    }
568
569    /// Find the matching long flag
570    pub fn get_long_flag(&self, name: &str) -> Option<Flag> {
571        for flag in &self.named {
572            if flag.long == name {
573                return Some(flag.clone());
574            }
575        }
576        None
577    }
578
579    /// Find the matching long flag
580    pub fn get_short_flag(&self, short: char) -> Option<Flag> {
581        for flag in &self.named {
582            if let Some(short_flag) = &flag.short {
583                if *short_flag == short {
584                    return Some(flag.clone());
585                }
586            }
587        }
588        None
589    }
590
591    /// Set the filter flag for the signature
592    pub fn filter(mut self) -> Signature {
593        self.is_filter = true;
594        self
595    }
596
597    /// Create a placeholder implementation of Command as a way to predeclare a definition's
598    /// signature so other definitions can see it. This placeholder is later replaced with the
599    /// full definition in a second pass of the parser.
600    pub fn predeclare(self) -> Box<dyn Command> {
601        Box::new(Predeclaration { signature: self })
602    }
603
604    /// Combines a signature and a block into a runnable block
605    pub fn into_block_command(
606        self,
607        block_id: BlockId,
608        attributes: Vec<(String, Value)>,
609        examples: Vec<CustomExample>,
610    ) -> Box<dyn Command> {
611        Box::new(BlockCommand {
612            signature: self,
613            block_id,
614            attributes,
615            examples,
616        })
617    }
618
619    pub fn formatted_flags(self) -> String {
620        if self.named.len() < 11 {
621            let mut s = "Available flags:".to_string();
622            for flag in self.named {
623                if let Some(short) = flag.short {
624                    let _ = write!(s, " --{}(-{}),", flag.long, short);
625                } else {
626                    let _ = write!(s, " --{},", flag.long);
627                }
628            }
629            s.remove(s.len() - 1);
630            let _ = write!(s, ". Use `--help` for more information.");
631            s
632        } else {
633            let mut s = "Some available flags:".to_string();
634            for flag in self.named {
635                if let Some(short) = flag.short {
636                    let _ = write!(s, " --{}(-{}),", flag.long, short);
637                } else {
638                    let _ = write!(s, " --{},", flag.long);
639                }
640            }
641            s.remove(s.len() - 1);
642            let _ = write!(
643                s,
644                "... Use `--help` for a full list of flags and more information."
645            );
646            s
647        }
648    }
649}
650
651#[derive(Clone)]
652struct Predeclaration {
653    signature: Signature,
654}
655
656impl Command for Predeclaration {
657    fn name(&self) -> &str {
658        &self.signature.name
659    }
660
661    fn signature(&self) -> Signature {
662        self.signature.clone()
663    }
664
665    fn description(&self) -> &str {
666        &self.signature.description
667    }
668
669    fn extra_description(&self) -> &str {
670        &self.signature.extra_description
671    }
672
673    fn run(
674        &self,
675        _engine_state: &EngineState,
676        _stack: &mut Stack,
677        _call: &Call,
678        _input: PipelineData,
679    ) -> Result<PipelineData, crate::ShellError> {
680        panic!("Internal error: can't run a predeclaration without a body")
681    }
682}
683
684fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String {
685    match &arg.shape {
686        SyntaxShape::Keyword(name, ..) => {
687            if is_required {
688                format!("{} <{}> ", String::from_utf8_lossy(name), arg.name)
689            } else {
690                format!("({} <{}>) ", String::from_utf8_lossy(name), arg.name)
691            }
692        }
693        _ => {
694            if is_required {
695                format!("<{}> ", arg.name)
696            } else {
697                format!("({}) ", arg.name)
698            }
699        }
700    }
701}
702
703#[derive(Clone, DeriveFromValue)]
704pub struct CustomExample {
705    pub example: String,
706    pub description: String,
707    pub result: Option<Value>,
708}
709
710impl CustomExample {
711    pub fn to_example(&self) -> Example<'_> {
712        Example {
713            example: self.example.as_str(),
714            description: self.description.as_str(),
715            result: self.result.clone(),
716        }
717    }
718}
719
720#[derive(Clone)]
721struct BlockCommand {
722    signature: Signature,
723    block_id: BlockId,
724    attributes: Vec<(String, Value)>,
725    examples: Vec<CustomExample>,
726}
727
728impl Command for BlockCommand {
729    fn name(&self) -> &str {
730        &self.signature.name
731    }
732
733    fn signature(&self) -> Signature {
734        self.signature.clone()
735    }
736
737    fn description(&self) -> &str {
738        &self.signature.description
739    }
740
741    fn extra_description(&self) -> &str {
742        &self.signature.extra_description
743    }
744
745    fn run(
746        &self,
747        _engine_state: &EngineState,
748        _stack: &mut Stack,
749        _call: &Call,
750        _input: PipelineData,
751    ) -> Result<crate::PipelineData, crate::ShellError> {
752        Err(ShellError::GenericError {
753            error: "Internal error: can't run custom command with 'run', use block_id".into(),
754            msg: "".into(),
755            span: None,
756            help: None,
757            inner: vec![],
758        })
759    }
760
761    fn command_type(&self) -> CommandType {
762        CommandType::Custom
763    }
764
765    fn block_id(&self) -> Option<BlockId> {
766        Some(self.block_id)
767    }
768
769    fn attributes(&self) -> Vec<(String, Value)> {
770        self.attributes.clone()
771    }
772
773    fn examples(&self) -> Vec<Example> {
774        self.examples
775            .iter()
776            .map(CustomExample::to_example)
777            .collect()
778    }
779
780    fn search_terms(&self) -> Vec<&str> {
781        self.signature
782            .search_terms
783            .iter()
784            .map(String::as_str)
785            .collect()
786    }
787
788    fn deprecation_info(&self) -> Vec<DeprecationEntry> {
789        self.attributes
790            .iter()
791            .filter_map(|(key, value)| {
792                (key == "deprecated")
793                    .then_some(value.clone())
794                    .map(DeprecationEntry::from_value)
795                    .and_then(Result::ok)
796            })
797            .collect()
798    }
799}