larpa/
desc.rs

1//! Introspectable command description.
2//!
3//! This module contains [`CommandDesc`], which is automatically generated for every
4//! [`Command`][crate::Command] and allows generating `--help` output, a command usage string,
5//! man pages, shell completions, etc. by simply inspecting its contents.
6
7use std::{
8    ffi::{OsStr, OsString},
9    fmt,
10    path::{Path, PathBuf},
11};
12
13use crate::private::{ArgumentDescImpl, ArgumentNameImpl, CommandDescImpl, CommandDescInner};
14
15/// Introspectable command description.
16#[derive(Debug, Clone)]
17pub struct CommandDesc(pub(crate) CommandDescImpl);
18
19impl CommandDesc {
20    fn inner(&self) -> &'static CommandDescInner {
21        self.0.0
22    }
23
24    /// Returns the canonical name of the command.
25    ///
26    /// This is derived from the name of the crate that defines the command.
27    /// The *invoked name* of a command is `argv[0]`, which is typically the name of the executable,
28    /// and may differ from the canonical name.
29    #[inline]
30    pub fn canonical_name(&self) -> &str {
31        self.inner().canonical_name
32    }
33
34    #[inline]
35    pub fn description(&self) -> Option<&str> {
36        self.inner().description
37    }
38
39    /// Returns the version string of the command.
40    ///
41    /// This defaults to the Cargo package version (via `CARGO_PKG_VERSION`), but can be customized
42    /// via [`#[larpa(version = "custom-version")]`][version].
43    ///
44    /// Returns [`None`] when no version was specified manually and `CARGO_PKG_VERSION` was not set
45    /// (likely because the package wasn't built by Cargo).
46    ///
47    /// [version]: crate::attrs::top_level::version
48    #[inline]
49    pub fn version(&self) -> Option<&str> {
50        self.inner().version
51    }
52
53    #[inline]
54    pub fn authors(&self) -> Option<&str> {
55        self.inner().authors
56    }
57
58    /// Returns the program license identifier.
59    ///
60    /// By default, this is taken from the package's `Cargo.toml`, but it can be overridden with
61    /// [`#[larpa(license)]`][license].
62    ///
63    /// [license]: crate::attrs::top_level::license
64    #[inline]
65    pub fn license(&self) -> Option<&str> {
66        self.inner().license
67    }
68
69    #[inline]
70    pub fn homepage(&self) -> Option<&str> {
71        self.inner().homepage
72    }
73
74    #[inline]
75    pub fn repository(&self) -> Option<&str> {
76        self.inner().repository
77    }
78
79    #[inline]
80    pub fn args(&self) -> &[ArgumentDesc] {
81        self.inner().args
82    }
83
84    #[inline]
85    pub fn is_subcommand_optional(&self) -> bool {
86        self.inner().subcommand_optional
87    }
88
89    /// Returns `true` if there may be invokable subcommands beyond the ones in
90    /// [`CommandDesc::subcommands`].
91    ///
92    /// This is `true` when the command is an `enum` with a variant annotated with
93    /// [`#[larpa(fallback)]`][fallback].
94    ///
95    /// [fallback]: crate::attrs::variant::fallback
96    #[inline]
97    pub fn has_subcommand_fallback(&self) -> bool {
98        self.inner()
99            .subcommands
100            .as_ref()
101            .is_some_and(|d| d.has_fallback)
102    }
103
104    /// Invokes the dynamic subcommand discovery function.
105    ///
106    /// For `enum`s with a [`#[larpa(fallback)]`][fallback] variant, a dynamic subcommand discovery
107    /// function may be provided by the user that finds additional applicable subcommands at
108    /// runtime.
109    ///
110    /// If [`#[larpa(discover)]`][discover] was not used, this will return an empty [`Vec`].
111    ///
112    /// [fallback]: crate::attrs::variant::fallback
113    /// [discover]: crate::attrs::variant::discover
114    pub fn discover_subcommands(&self) -> Vec<DiscoveredSubcommand> {
115        match self
116            .inner()
117            .subcommands
118            .as_ref()
119            .and_then(|d| d.discover_subcommands)
120        {
121            Some(desc) => desc(self),
122            None => Vec::new(),
123        }
124    }
125
126    /// Returns the list of built-in subcommands, if this command has any.
127    #[inline]
128    pub fn subcommands(&self) -> &[SubcommandDesc] {
129        match self.inner().subcommands.as_ref() {
130            Some(desc) => desc.subcommands,
131            None => &[],
132        }
133    }
134}
135
136/// Description of a built-in subcommand.
137///
138/// Returned by [`CommandDesc::subcommands`].
139#[derive(Debug)]
140pub struct SubcommandDesc(pub(crate) CommandDesc);
141
142impl SubcommandDesc {
143    #[inline]
144    pub fn canonical_name(&self) -> &str {
145        self.0.canonical_name()
146    }
147
148    #[inline]
149    pub fn description(&self) -> Option<&str> {
150        self.0.description()
151    }
152
153    #[inline]
154    pub fn args(&self) -> &[ArgumentDesc] {
155        self.0.args()
156    }
157
158    #[inline]
159    pub fn subcommands(&self) -> &[SubcommandDesc] {
160        self.0.subcommands()
161    }
162}
163
164/// A dynamically discovered subcommand (for example, from searching `$PATH`).
165///
166/// This type can be returned by a user-defined external subcommand discovery function.
167/// For more information on how to set this up, please refer to the documentation for the
168/// [`#[larpa(discover)]`][discover] attribute.
169///
170/// [discover]: crate::attrs::variant::discover
171#[derive(Debug)]
172pub struct DiscoveredSubcommand {
173    name: OsString,
174    description: Option<String>,
175    path: Option<PathBuf>,
176}
177
178impl DiscoveredSubcommand {
179    /// Creates a [`DiscoveredSubcommand`] with the given subcommand name.
180    ///
181    /// It is assumed that `$cmd $name` (where `$cmd` is the canonical name of the command,
182    /// and `$name` is the `name` argument of this function) will invoke this subcommand.
183    pub fn new(name: impl Into<OsString>) -> Self {
184        Self {
185            name: name.into(),
186            description: None,
187            path: None,
188        }
189    }
190
191    /// Sets the description of this subcommand.
192    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
193        self.description = Some(desc.into());
194        self
195    }
196
197    /// Sets the path to the external subcommand.
198    pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
199        self.path = Some(path.into());
200        self
201    }
202
203    #[inline]
204    pub fn name(&self) -> &OsStr {
205        &self.name
206    }
207
208    #[inline]
209    pub fn description(&self) -> Option<&str> {
210        self.description.as_deref()
211    }
212
213    #[inline]
214    pub fn path(&self) -> Option<&Path> {
215        self.path.as_deref()
216    }
217}
218
219/// Description of a command-line argument.
220///
221/// Returned by [`CommandDesc::args`] and [`SubcommandDesc::args`].
222#[derive(Debug)]
223pub struct ArgumentDesc(pub(crate) ArgumentDescImpl);
224
225impl ArgumentDesc {
226    /// Returns whether the argument is positional.
227    ///
228    /// Positional arguments have neither a short nor a long flag associated with them, and are
229    /// matched purely by their position in the command line.
230    #[inline]
231    pub fn is_positional(&self) -> bool {
232        self.short().is_none() && self.long().is_none()
233    }
234
235    /// Returns whether the argument is optional.
236    #[inline]
237    pub fn is_optional(&self) -> bool {
238        self.0.optional
239    }
240
241    /// Returns whether the argument is required.
242    ///
243    /// This is the inverse of [`is_optional`][Self::is_optional].
244    ///
245    /// If a required argument is not provided, parsing will fail.
246    #[inline]
247    pub fn is_required(&self) -> bool {
248        !self.is_optional()
249    }
250
251    /// Returns whether the argument can be provided multiple times.
252    #[inline]
253    pub fn is_repeating(&self) -> bool {
254        self.0.repeating
255    }
256
257    /// Returns whether this argument takes a value.
258    ///
259    /// If `false`, the argument is a flag that only cares about whether (or how many times) it is
260    /// present.
261    #[inline]
262    pub fn takes_value(&self) -> bool {
263        self.value_name().is_some()
264    }
265
266    #[inline]
267    pub fn value_name(&self) -> Option<&str> {
268        self.0.name.0.value_name
269    }
270
271    /// Returns the description of this argument.
272    #[inline]
273    pub fn description(&self) -> Option<&str> {
274        self.0.description
275    }
276
277    /// Returns the custom default value (if any).
278    ///
279    /// Note that this will only return [`Some`] when using
280    /// [`#[larpa(default = "custom value")]`][dfl].
281    /// Using [`#[larpa(default)]`][dfl] alone will use the field type's [`Default`] implementation
282    /// and this method will return [`None`].
283    ///
284    /// [dfl]: crate::attrs::field::default
285    #[inline]
286    pub fn custom_default(&self) -> Option<&str> {
287        self.0.custom_default
288    }
289
290    /// Returns the argument's short form (eg. `-s`), without the leading `-`.
291    #[inline]
292    pub fn short(&self) -> Option<char> {
293        self.0.name.0.short
294    }
295
296    /// Returns the argument's long form (eg. `--long`), without the leading `--`.
297    #[inline]
298    pub fn long(&self) -> Option<&str> {
299        self.0.name.0.long
300    }
301
302    pub fn name(&self) -> &ArgumentName {
303        &self.0.name
304    }
305}
306
307/// A [`Display`][fmt::Display]able name of a command-line argument.
308#[derive(Debug)]
309pub struct ArgumentName(pub(crate) ArgumentNameImpl);
310
311impl fmt::Display for ArgumentName {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        match (self.0.short, self.0.long) {
314            (Some(short), None) => write!(f, "-{short}"),
315            (None, Some(long)) => write!(f, "--{long}"),
316            (Some(short), Some(long)) => write!(f, "-{short}/--{long}"),
317            (None, None) => f.write_str(self.0.value_name.unwrap()),
318        }
319    }
320}