Skip to main content

nu_protocol/
signature.rs

1use crate::{
2    BlockId, CompareTypes, DeclId, DeprecationEntry, Example, FromValue, IntoValue, PipelineData,
3    ShellError, Span, SyntaxShape, Type, TypeSet, Value, VarId,
4    engine::{Call, Command, CommandType, EngineState, Stack},
5    shell_error::generic::GenericError,
6};
7use nu_derive_value::FromValue as DeriveFromValue;
8use nu_utils::NuCow;
9use serde::{Deserialize, Serialize};
10use std::fmt::Write;
11
12// Make nu_protocol available in this namespace, consumers of this crate will
13// have this without such an export.
14// The `FromValue` derive macro fully qualifies paths to "nu_protocol".
15use crate as nu_protocol;
16
17pub enum Parameter {
18    Required(PositionalArg),
19    Optional(PositionalArg),
20    Rest(PositionalArg),
21    Flag(Flag),
22}
23
24impl From<Flag> for Parameter {
25    fn from(value: Flag) -> Self {
26        Self::Flag(value)
27    }
28}
29
30/// The signature definition of a named flag that either accepts a value or acts as a toggle flag
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Flag {
33    pub long: String,
34    pub short: Option<char>,
35    pub arg: Option<SyntaxShape>,
36    pub required: bool,
37    pub desc: String,
38    pub completion: Option<Completion>,
39
40    // For custom commands
41    pub var_id: Option<VarId>,
42    pub default_value: Option<Value>,
43}
44
45impl Flag {
46    /// The flag's long name, or `None` for a short-only flag (whose `long` is empty).
47    #[inline]
48    pub fn long_name(&self) -> Option<&str> {
49        (!self.long.is_empty()).then_some(self.long.as_str())
50    }
51
52    /// Whether this flag's value type accepts `nothing`/`null`.
53    ///
54    /// Used so `--flag=$null` can either pass `null` through (when the type allows it) or omit
55    /// the flag (when it does not). Switches (`arg: None`) never accept nothing — null means omit.
56    #[inline]
57    pub fn type_accepts_nothing(&self) -> bool {
58        match &self.arg {
59            Some(shape) => Type::Nothing.is_assignable_to(&shape.to_type()),
60            None => false,
61        }
62    }
63
64    #[inline]
65    pub fn new(long: impl Into<String>) -> Self {
66        Flag {
67            long: long.into(),
68            short: None,
69            arg: None,
70            required: false,
71            desc: String::new(),
72            completion: None,
73            var_id: None,
74            default_value: None,
75        }
76    }
77
78    #[inline]
79    pub fn short(self, short: char) -> Self {
80        Self {
81            short: Some(short),
82            ..self
83        }
84    }
85
86    #[inline]
87    pub fn arg(self, arg: SyntaxShape) -> Self {
88        Self {
89            arg: Some(arg),
90            ..self
91        }
92    }
93
94    #[inline]
95    pub fn required(self) -> Self {
96        Self {
97            required: true,
98            ..self
99        }
100    }
101
102    #[inline]
103    pub fn desc(self, desc: impl Into<String>) -> Self {
104        Self {
105            desc: desc.into(),
106            ..self
107        }
108    }
109
110    #[inline]
111    pub fn completion(self, completion: Completion) -> Self {
112        Self {
113            completion: Some(completion),
114            ..self
115        }
116    }
117}
118
119/// The signature definition for a positional argument
120#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct PositionalArg {
122    pub name: String,
123    pub desc: String,
124    pub shape: SyntaxShape,
125    pub completion: Option<Completion>,
126
127    // For custom commands
128    pub var_id: Option<VarId>,
129    pub default_value: Option<Value>,
130}
131
132impl PositionalArg {
133    #[inline]
134    pub fn new(name: impl Into<String>, shape: SyntaxShape) -> Self {
135        Self {
136            name: name.into(),
137            desc: String::new(),
138            shape,
139            completion: None,
140            var_id: None,
141            default_value: None,
142        }
143    }
144
145    #[inline]
146    pub fn desc(self, desc: impl Into<String>) -> Self {
147        Self {
148            desc: desc.into(),
149            ..self
150        }
151    }
152
153    #[inline]
154    pub fn completion(self, completion: Completion) -> Self {
155        Self {
156            completion: Some(completion),
157            ..self
158        }
159    }
160
161    #[inline]
162    pub fn required(self) -> Parameter {
163        Parameter::Required(self)
164    }
165
166    #[inline]
167    pub fn optional(self) -> Parameter {
168        Parameter::Optional(self)
169    }
170
171    #[inline]
172    pub fn rest(self) -> Parameter {
173        Parameter::Rest(self)
174    }
175}
176
177#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
178pub enum CommandWideCompleter {
179    External,
180    Command(DeclId),
181}
182
183/// A built-in completion a command declares for one of its arguments, dispatched on by the
184/// completer so renaming the command can't silently disable its argument completion.
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186pub enum BuiltinCompletion {
187    /// A `.nu` file or directory (`use`, `overlay use`, `source-env`); `std_virtual_path`
188    /// also offers the virtual `std/` modules (disabled for `source-env`).
189    NuFile { std_virtual_path: bool },
190    /// The exported members of an already-named module (`use spam <tab>`).
191    ModuleExports,
192    /// An environment variable name (`hide-env`).
193    EnvVar,
194    /// A command name; `internal_only` restricts to internal commands (`attr complete`).
195    Command { internal_only: bool },
196}
197
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub enum Completion {
200    Command(DeclId),
201    List(NuCow<&'static [&'static str], Vec<String>>),
202    /// A completion the engine provides for the argument (module/env/command names, …).
203    Builtin(BuiltinCompletion),
204}
205
206impl Completion {
207    pub const fn new_list(list: &'static [&'static str]) -> Self {
208        Self::List(NuCow::Borrowed(list))
209    }
210
211    pub fn to_value(&self, engine_state: &EngineState, span: Span) -> Value {
212        match self {
213            Completion::Command(id) => engine_state
214                .get_decl(*id)
215                .name()
216                .to_owned()
217                .into_value(span),
218            // No list to surface; name it so `scope commands` stays honest.
219            Completion::Builtin(kind) => Value::string(
220                match kind {
221                    BuiltinCompletion::NuFile { .. } => "<nu-file>",
222                    BuiltinCompletion::ModuleExports => "<module-exports>",
223                    BuiltinCompletion::EnvVar => "<env-var>",
224                    BuiltinCompletion::Command { .. } => "<command-name>",
225                },
226                span,
227            ),
228            Completion::List(list) => match list {
229                NuCow::Borrowed(list) => list
230                    .iter()
231                    .map(|&e| e.into_value(span))
232                    .collect::<Vec<Value>>()
233                    .into_value(span),
234                NuCow::Owned(list) => list
235                    .iter()
236                    .cloned()
237                    .map(|e| e.into_value(span))
238                    .collect::<Vec<Value>>()
239                    .into_value(span),
240            },
241        }
242    }
243}
244
245/// Command categories
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub enum Category {
248    Bits,
249    Bytes,
250    Chart,
251    Conversions,
252    Core,
253    Custom(String),
254    Database,
255    Date,
256    Debug,
257    Default,
258    Deprecated,
259    Removed,
260    Env,
261    Experimental,
262    FileSystem,
263    Filters,
264    Formats,
265    Generators,
266    Hash,
267    History,
268    Math,
269    Misc,
270    Network,
271    Path,
272    Platform,
273    Plugin,
274    Random,
275    Shells,
276    Strings,
277    System,
278    Viewers,
279}
280
281impl std::fmt::Display for Category {
282    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283        let msg = match self {
284            Category::Bits => "bits",
285            Category::Bytes => "bytes",
286            Category::Chart => "chart",
287            Category::Conversions => "conversions",
288            Category::Core => "core",
289            Category::Custom(name) => name,
290            Category::Database => "database",
291            Category::Date => "date",
292            Category::Debug => "debug",
293            Category::Default => "default",
294            Category::Deprecated => "deprecated",
295            Category::Removed => "removed",
296            Category::Env => "env",
297            Category::Experimental => "experimental",
298            Category::FileSystem => "filesystem",
299            Category::Filters => "filters",
300            Category::Formats => "formats",
301            Category::Generators => "generators",
302            Category::Hash => "hash",
303            Category::History => "history",
304            Category::Math => "math",
305            Category::Misc => "misc",
306            Category::Network => "network",
307            Category::Path => "path",
308            Category::Platform => "platform",
309            Category::Plugin => "plugin",
310            Category::Random => "random",
311            Category::Shells => "shells",
312            Category::Strings => "strings",
313            Category::System => "system",
314            Category::Viewers => "viewers",
315        };
316
317        write!(f, "{msg}")
318    }
319}
320
321pub fn category_from_string(category: &str) -> Category {
322    match category {
323        "bits" => Category::Bits,
324        "bytes" => Category::Bytes,
325        "chart" => Category::Chart,
326        "conversions" => Category::Conversions,
327        // Let's protect our own "core" commands by preventing scripts from having this category.
328        "core" => Category::Custom("custom_core".to_string()),
329        "database" => Category::Database,
330        "date" => Category::Date,
331        "debug" => Category::Debug,
332        "default" => Category::Default,
333        "deprecated" => Category::Deprecated,
334        "removed" => Category::Removed,
335        "env" => Category::Env,
336        "experimental" => Category::Experimental,
337        "filesystem" => Category::FileSystem,
338        "filter" => Category::Filters,
339        "formats" => Category::Formats,
340        "generators" => Category::Generators,
341        "hash" => Category::Hash,
342        "history" => Category::History,
343        "math" => Category::Math,
344        "misc" => Category::Misc,
345        "network" => Category::Network,
346        "path" => Category::Path,
347        "platform" => Category::Platform,
348        "plugin" => Category::Plugin,
349        "random" => Category::Random,
350        "shells" => Category::Shells,
351        "strings" => Category::Strings,
352        "system" => Category::System,
353        "viewers" => Category::Viewers,
354        _ => Category::Custom(category.to_string()),
355    }
356}
357
358/// Signature information of a [`Command`]
359#[derive(Clone, Debug, Serialize, Deserialize)]
360pub struct Signature {
361    pub name: String,
362    pub description: String,
363    pub extra_description: String,
364    pub search_terms: Vec<String>,
365    pub required_positional: Vec<PositionalArg>,
366    pub optional_positional: Vec<PositionalArg>,
367    pub rest_positional: Option<PositionalArg>,
368    pub named: Vec<Flag>,
369    pub input_output_types: Vec<(Type, Type)>,
370    pub allow_variants_without_examples: bool,
371    pub is_filter: bool,
372    pub creates_scope: bool,
373    pub allows_unknown_args: bool,
374    pub complete: Option<CommandWideCompleter>,
375    // Signature category used to classify commands stored in the list of declarations
376    pub category: Category,
377}
378
379impl PartialEq for Signature {
380    fn eq(&self, other: &Self) -> bool {
381        self.name == other.name
382            && self.description == other.description
383            && self.required_positional == other.required_positional
384            && self.optional_positional == other.optional_positional
385            && self.rest_positional == other.rest_positional
386            && self.is_filter == other.is_filter
387    }
388}
389
390impl Eq for Signature {}
391
392fn type_involves_custom(ty: &Type) -> bool {
393    match ty {
394        Type::Custom(_) => true,
395        Type::OneOf(types) => types.iter().any(type_involves_custom),
396        Type::List(inner) => type_involves_custom(inner),
397        _ => false,
398    }
399}
400
401fn is_structured_type(ty: &Type) -> bool {
402    matches!(ty, Type::List(_) | Type::Table(_) | Type::Record(_))
403}
404
405/// Custom values are assignable to list/table/record so `get` / `into record` type-check.
406/// That special case must not steal the output type of a real custom IO pair
407/// (`semver | into string` is a string, not `oneof<list, table, record>`).
408fn is_custom_structured_fallback(input: &Type, declared: &Type) -> bool {
409    type_involves_custom(input)
410        && is_structured_type(declared)
411        && input.compare_types(declared).is_none()
412}
413
414impl Signature {
415    /// Creates a new signature for a command with `name`
416    pub fn new(name: impl Into<String>) -> Signature {
417        Signature {
418            name: name.into(),
419            description: String::new(),
420            extra_description: String::new(),
421            search_terms: vec![],
422            required_positional: vec![],
423            optional_positional: vec![],
424            rest_positional: None,
425            input_output_types: vec![],
426            allow_variants_without_examples: false,
427            named: vec![],
428            is_filter: false,
429            creates_scope: false,
430            category: Category::Default,
431            allows_unknown_args: false,
432            complete: None,
433        }
434    }
435
436    /// Gets the input type from the signature
437    ///
438    /// - If the input was unspecified  [`Type::Any`] is returned.
439    /// - If the signature has a single input type, it is returned.
440    /// - If there are multiple input types, a [union](Type::union) of them is returned.
441    pub fn get_input_type(&self) -> Type {
442        match self.input_output_types.as_slice() {
443            [] => Type::Any,
444            [(input, _output)] => input.clone(),
445            multiple => Type::one_of(multiple.iter().map(|(input, _)| input.clone())),
446        }
447    }
448
449    /// Gets the output type from the signature based on `input`
450    ///
451    /// - If the signature's output was unspecified [`Type::Any`] is returned.
452    /// - If `input` is [`None`], it's treated as [`Type::Any`]. i.e. all IO pairs are considered.
453    /// - IO pairs where the given `input` is [assignable to](crate::CompareTypes::is_assignable_to)
454    ///   the input type are considered valid.
455    /// - Custom values are assignable to list/table/record. Those fallback pairs are ignored
456    ///   when a lattice match exists (so `semver | into string` is `string`).
457    /// - [Union](TypeSet::union) of remaining outputs is returned.
458    /// - If there are no valid IO pairs for the given `input`, [`None`] is returned.
459    // XXX: remove?
460    pub fn get_output_type(&self, input_type: Option<&Type>) -> Option<Type> {
461        if self.input_output_types.is_empty() {
462            return Some(Type::Any);
463        }
464        let input = input_type.unwrap_or(&Type::Any);
465        // Entries whose input type accepts `input` are the candidates; among those, the ones
466        // that are not merely a structured-type fallback for a custom value are preferred. Two
467        // passes without temporary collections: this runs for every call the parser
468        // type-checks.
469        let mut any_match = false;
470        let mut any_strict_match = false;
471        for (in_ty, _) in &self.input_output_types {
472            if input.is_assignable_to(in_ty) {
473                any_match = true;
474                if !is_custom_structured_fallback(input, in_ty) {
475                    any_strict_match = true;
476                    break;
477                }
478            }
479        }
480        if !any_match {
481            return None;
482        }
483
484        self.input_output_types
485            .iter()
486            .filter(|(in_ty, _)| {
487                input.is_assignable_to(in_ty)
488                    && (!any_strict_match || !is_custom_structured_fallback(input, in_ty))
489            })
490            .map(|(_, out)| out.clone())
491            .reduce(Type::union)
492    }
493
494    /// Add a default help option to a signature
495    pub fn add_help(mut self) -> Signature {
496        // default help flag
497        let flag = Flag {
498            long: "help".into(),
499            short: Some('h'),
500            arg: None,
501            desc: "Display the help message for this command".into(),
502            required: false,
503            var_id: None,
504            default_value: None,
505            completion: None,
506        };
507        self.named.push(flag);
508        self
509    }
510
511    /// Build an internal signature with default help option
512    ///
513    /// This is equivalent to `Signature::new(name).add_help()`.
514    pub fn build(name: impl Into<String>) -> Signature {
515        Signature::new(name.into()).add_help()
516    }
517
518    /// Add a description to the signature
519    ///
520    /// This should be a single sentence as it is the part shown for example in the completion
521    /// menu.
522    pub fn description(mut self, msg: impl Into<String>) -> Signature {
523        self.description = msg.into();
524        self
525    }
526
527    /// Add an extra description to the signature.
528    ///
529    /// Here additional documentation can be added
530    pub fn extra_description(mut self, msg: impl Into<String>) -> Signature {
531        self.extra_description = msg.into();
532        self
533    }
534
535    /// Add search terms to the signature
536    pub fn search_terms(mut self, terms: Vec<String>) -> Signature {
537        self.search_terms = terms;
538        self
539    }
540
541    /// Update signature's fields from a Command trait implementation
542    pub fn update_from_command(mut self, command: &dyn Command) -> Signature {
543        self.search_terms = command
544            .search_terms()
545            .into_iter()
546            .map(|term| term.to_string())
547            .collect();
548        self.extra_description = command.extra_description().to_string();
549        self.description = command.description().to_string();
550        self
551    }
552
553    /// Allow unknown signature parameters
554    pub fn allows_unknown_args(mut self) -> Signature {
555        self.allows_unknown_args = true;
556        self
557    }
558
559    pub fn param(mut self, param: impl Into<Parameter>) -> Self {
560        let param: Parameter = param.into();
561        match param {
562            Parameter::Flag(flag) => {
563                if let Some(s) = flag.short {
564                    assert!(
565                        !self.get_shorts().contains(&s),
566                        "There may be duplicate short flags for '-{s}'"
567                    );
568                }
569
570                let name = flag.long.as_str();
571                assert!(
572                    !self.get_names().contains(&name),
573                    "There may be duplicate name flags for '--{name}'"
574                );
575
576                self.named.push(flag);
577            }
578            Parameter::Required(positional_arg) => {
579                self.required_positional.push(positional_arg);
580            }
581            Parameter::Optional(positional_arg) => {
582                self.optional_positional.push(positional_arg);
583            }
584            Parameter::Rest(positional_arg) => {
585                assert!(
586                    self.rest_positional.is_none(),
587                    "Tried to set rest arguments more than once"
588                );
589                self.rest_positional = Some(positional_arg);
590            }
591        }
592        self
593    }
594
595    /// Add a required positional argument to the signature
596    pub fn required(
597        mut self,
598        name: impl Into<String>,
599        shape: impl Into<SyntaxShape>,
600        desc: impl Into<String>,
601    ) -> Signature {
602        self.required_positional.push(PositionalArg {
603            name: name.into(),
604            desc: desc.into(),
605            shape: shape.into(),
606            var_id: None,
607            default_value: None,
608            completion: None,
609        });
610
611        self
612    }
613
614    /// Add an optional positional argument to the signature
615    pub fn optional(
616        mut self,
617        name: impl Into<String>,
618        shape: impl Into<SyntaxShape>,
619        desc: impl Into<String>,
620    ) -> Signature {
621        self.optional_positional.push(PositionalArg {
622            name: name.into(),
623            desc: desc.into(),
624            shape: shape.into(),
625            var_id: None,
626            default_value: None,
627            completion: None,
628        });
629
630        self
631    }
632
633    /// Add a rest positional parameter
634    ///
635    /// Rest positionals (also called [rest parameters][rp]) are treated as
636    /// optional: passing 0 arguments is a valid call.  If the command requires
637    /// at least one argument, it must be checked by the implementation.
638    ///
639    /// [rp]: https://www.nushell.sh/book/custom_commands.html#rest-parameters
640    pub fn rest(
641        mut self,
642        name: &str,
643        shape: impl Into<SyntaxShape>,
644        desc: impl Into<String>,
645    ) -> Signature {
646        self.rest_positional = Some(PositionalArg {
647            name: name.into(),
648            desc: desc.into(),
649            shape: shape.into(),
650            var_id: None,
651            default_value: None,
652            completion: None,
653        });
654
655        self
656    }
657
658    /// Is this command capable of operating on its input via cell paths?
659    pub fn operates_on_cell_paths(&self) -> bool {
660        self.required_positional
661            .iter()
662            .chain(self.rest_positional.iter())
663            .any(|pos| {
664                matches!(
665                    pos,
666                    PositionalArg {
667                        shape: SyntaxShape::CellPath,
668                        ..
669                    }
670                )
671            })
672    }
673
674    /// Add an optional named flag argument to the signature
675    pub fn named(
676        mut self,
677        name: impl Into<String>,
678        shape: impl Into<SyntaxShape>,
679        desc: impl Into<String>,
680        short: Option<char>,
681    ) -> Signature {
682        let (name, s) = self.check_names(name, short);
683
684        self.named.push(Flag {
685            long: name,
686            short: s,
687            arg: Some(shape.into()),
688            required: false,
689            desc: desc.into(),
690            var_id: None,
691            default_value: None,
692            completion: None,
693        });
694
695        self
696    }
697
698    /// Add a required named flag argument to the signature
699    pub fn required_named(
700        mut self,
701        name: impl Into<String>,
702        shape: impl Into<SyntaxShape>,
703        desc: impl Into<String>,
704        short: Option<char>,
705    ) -> Signature {
706        let (name, s) = self.check_names(name, short);
707
708        self.named.push(Flag {
709            long: name,
710            short: s,
711            arg: Some(shape.into()),
712            required: true,
713            desc: desc.into(),
714            var_id: None,
715            default_value: None,
716            completion: None,
717        });
718
719        self
720    }
721
722    /// Add a switch to the signature
723    pub fn switch(
724        mut self,
725        name: impl Into<String>,
726        desc: impl Into<String>,
727        short: Option<char>,
728    ) -> Signature {
729        let (name, s) = self.check_names(name, short);
730
731        self.named.push(Flag {
732            long: name,
733            short: s,
734            arg: None,
735            required: false,
736            desc: desc.into(),
737            var_id: None,
738            default_value: None,
739            completion: None,
740        });
741
742        self
743    }
744
745    /// Changes the input type of the command signature
746    pub fn input_output_type(mut self, input_type: Type, output_type: Type) -> Signature {
747        self.input_output_types.push((input_type, output_type));
748        self
749    }
750
751    /// Set the input-output type signature variants of the command
752    pub fn input_output_types(mut self, input_output_types: Vec<(Type, Type)>) -> Signature {
753        self.input_output_types = input_output_types;
754        self
755    }
756
757    /// Changes the signature category
758    pub fn category(mut self, category: Category) -> Signature {
759        self.category = category;
760
761        self
762    }
763
764    /// Sets that signature will create a scope as it parses
765    pub fn creates_scope(mut self) -> Signature {
766        self.creates_scope = true;
767        self
768    }
769
770    // Is it allowed for the type signature to feature a variant that has no corresponding example?
771    pub fn allow_variants_without_examples(mut self, allow: bool) -> Signature {
772        self.allow_variants_without_examples = allow;
773        self
774    }
775
776    /// A string rendering of the command signature
777    ///
778    /// If the command has flags, all of them will be shown together as
779    /// `{flags}`.
780    pub fn call_signature(&self) -> String {
781        let mut one_liner = String::new();
782        one_liner.push_str(&self.name);
783        one_liner.push(' ');
784
785        // Note: the call signature needs flags first because on the nu commandline,
786        // flags will precede the script file name. Flags for internal commands can come
787        // either before or after (or around) positional parameters, so there isn't a strong
788        // preference, so we default to the more constrained example.
789        if self.named.len() > 1 {
790            one_liner.push_str("{flags} ");
791        }
792
793        for positional in &self.required_positional {
794            one_liner.push_str(&get_positional_short_name(positional, true));
795        }
796        for positional in &self.optional_positional {
797            one_liner.push_str(&get_positional_short_name(positional, false));
798        }
799
800        if let Some(rest) = &self.rest_positional {
801            let _ = write!(one_liner, "...{}", get_positional_short_name(rest, false));
802        }
803
804        // if !self.subcommands.is_empty() {
805        //     one_liner.push_str("<subcommand> ");
806        // }
807
808        one_liner
809    }
810
811    /// Get list of the short-hand flags
812    pub fn get_shorts(&self) -> Vec<char> {
813        self.named.iter().filter_map(|f| f.short).collect()
814    }
815
816    /// Get list of the long-hand flags
817    pub fn get_names(&self) -> Vec<&str> {
818        self.named.iter().map(|f| f.long.as_str()).collect()
819    }
820
821    /// Checks if short or long options are already present
822    ///
823    /// ## Panics
824    ///
825    /// Panics if one of them is found.
826    // XXX: return result instead of a panic
827    fn check_names(&self, name: impl Into<String>, short: Option<char>) -> (String, Option<char>) {
828        let s = short.inspect(|c| {
829            assert!(
830                !self.get_shorts().contains(c),
831                "There may be duplicate short flags for '-{c}'"
832            );
833        });
834
835        let name = {
836            let name: String = name.into();
837            assert!(
838                !self.get_names().contains(&name.as_str()),
839                "There may be duplicate name flags for '--{name}'"
840            );
841            name
842        };
843
844        (name, s)
845    }
846
847    /// Returns an argument with the index `position`
848    ///
849    /// It will index, in order, required arguments, then optional, then the
850    /// trailing `...rest` argument. Note that the `...rest` argument must be
851    /// a [`Value::List`], therefore this method may not work as intended when
852    /// the closure uses a rest argument.
853    pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> {
854        if position < self.required_positional.len() {
855            self.required_positional.get(position)
856        } else if position < (self.required_positional.len() + self.optional_positional.len()) {
857            self.optional_positional
858                .get(position - self.required_positional.len())
859        } else {
860            self.rest_positional.as_ref()
861        }
862    }
863
864    /// Returns the number of (optional) positional parameters in a signature
865    ///
866    /// This does _not_ include the `...rest` parameter, even if it's present.
867    pub fn num_positionals(&self) -> usize {
868        let mut total = self.required_positional.len() + self.optional_positional.len();
869
870        for positional in &self.required_positional {
871            if let SyntaxShape::Keyword(..) = positional.shape {
872                // Keywords have a required argument, so account for that
873                total += 1;
874            }
875        }
876        for positional in &self.optional_positional {
877            if let SyntaxShape::Keyword(..) = positional.shape {
878                // Keywords have a required argument, so account for that
879                total += 1;
880            }
881        }
882        total
883    }
884
885    /// Find the matching long flag
886    pub fn get_long_flag(&self, name: &str) -> Option<Flag> {
887        if name.is_empty() {
888            return None;
889        }
890        for flag in &self.named {
891            if flag.long == name {
892                return Some(flag.clone());
893            }
894        }
895        None
896    }
897
898    /// Find the matching long flag
899    pub fn get_short_flag(&self, short: char) -> Option<Flag> {
900        for flag in &self.named {
901            if let Some(short_flag) = &flag.short
902                && *short_flag == short
903            {
904                return Some(flag.clone());
905            }
906        }
907        None
908    }
909
910    /// Set the filter flag for the signature
911    pub fn filter(mut self) -> Signature {
912        self.is_filter = true;
913        self
914    }
915
916    /// Create a placeholder implementation of Command as a way to predeclare a definition's
917    /// signature so other definitions can see it. This placeholder is later replaced with the
918    /// full definition in a second pass of the parser.
919    pub fn predeclare(self) -> Box<dyn Command> {
920        self.predeclare_with_command_type(CommandType::Builtin)
921    }
922
923    /// Create a placeholder implementation of Command as a way to predeclare a definition's
924    /// signature with an explicit command type.
925    pub fn predeclare_with_command_type(self, command_type: CommandType) -> Box<dyn Command> {
926        Box::new(Predeclaration {
927            signature: self,
928            command_type,
929        })
930    }
931
932    /// Combines a signature and a block into a runnable block
933    pub fn into_block_command(
934        self,
935        block_id: BlockId,
936        attributes: Vec<(String, Value)>,
937        examples: Vec<CustomExample>,
938    ) -> Box<dyn Command> {
939        Box::new(BlockCommand {
940            signature: self,
941            block_id,
942            attributes,
943            examples,
944        })
945    }
946}
947
948#[derive(Clone)]
949struct Predeclaration {
950    signature: Signature,
951    command_type: CommandType,
952}
953
954impl Command for Predeclaration {
955    fn name(&self) -> &str {
956        &self.signature.name
957    }
958
959    fn signature(&self) -> Signature {
960        self.signature.clone()
961    }
962
963    fn description(&self) -> &str {
964        &self.signature.description
965    }
966
967    fn extra_description(&self) -> &str {
968        &self.signature.extra_description
969    }
970
971    fn run(
972        &self,
973        _engine_state: &EngineState,
974        _stack: &mut Stack,
975        _call: &Call,
976        _input: PipelineData,
977    ) -> Result<PipelineData, crate::ShellError> {
978        panic!("Internal error: can't run a predeclaration without a body")
979    }
980
981    fn command_type(&self) -> CommandType {
982        self.command_type
983    }
984}
985
986fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String {
987    match &arg.shape {
988        SyntaxShape::Keyword(name, ..) => {
989            if is_required {
990                format!("{} <{}> ", String::from_utf8_lossy(name), arg.name)
991            } else {
992                format!("({} <{}>) ", String::from_utf8_lossy(name), arg.name)
993            }
994        }
995        _ => {
996            if is_required {
997                format!("<{}> ", arg.name)
998            } else {
999                format!("({}) ", arg.name)
1000            }
1001        }
1002    }
1003}
1004
1005#[derive(Clone, DeriveFromValue)]
1006pub struct CustomExample {
1007    pub example: String,
1008    pub description: String,
1009    pub result: Option<Value>,
1010}
1011
1012impl CustomExample {
1013    pub fn to_example(&self) -> Example<'_> {
1014        Example {
1015            example: self.example.as_str(),
1016            description: self.description.as_str(),
1017            result: self.result.clone(),
1018        }
1019    }
1020}
1021
1022#[derive(Clone)]
1023struct BlockCommand {
1024    signature: Signature,
1025    block_id: BlockId,
1026    attributes: Vec<(String, Value)>,
1027    examples: Vec<CustomExample>,
1028}
1029
1030impl Command for BlockCommand {
1031    fn name(&self) -> &str {
1032        &self.signature.name
1033    }
1034
1035    fn signature(&self) -> Signature {
1036        self.signature.clone()
1037    }
1038
1039    fn description(&self) -> &str {
1040        &self.signature.description
1041    }
1042
1043    fn extra_description(&self) -> &str {
1044        &self.signature.extra_description
1045    }
1046
1047    fn run(
1048        &self,
1049        _engine_state: &EngineState,
1050        _stack: &mut Stack,
1051        _call: &Call,
1052        _input: PipelineData,
1053    ) -> Result<crate::PipelineData, crate::ShellError> {
1054        Err(ShellError::Generic(GenericError::new_internal(
1055            "Internal error: can't run custom command with 'run', use block_id",
1056            "",
1057        )))
1058    }
1059
1060    fn command_type(&self) -> CommandType {
1061        CommandType::Custom
1062    }
1063
1064    fn block_id(&self) -> Option<BlockId> {
1065        Some(self.block_id)
1066    }
1067
1068    fn attributes(&self) -> Vec<(String, Value)> {
1069        self.attributes.clone()
1070    }
1071
1072    fn examples(&self) -> Vec<Example<'_>> {
1073        self.examples
1074            .iter()
1075            .map(CustomExample::to_example)
1076            .collect()
1077    }
1078
1079    fn search_terms(&self) -> Vec<&str> {
1080        self.signature
1081            .search_terms
1082            .iter()
1083            .map(String::as_str)
1084            .collect()
1085    }
1086
1087    fn deprecation_info(&self) -> Vec<DeprecationEntry> {
1088        self.attributes
1089            .iter()
1090            .filter_map(|(key, value)| {
1091                (key == "deprecated")
1092                    .then_some(value.clone())
1093                    .map(DeprecationEntry::from_value)
1094                    .and_then(Result::ok)
1095            })
1096            .collect()
1097    }
1098}