Skip to main content

nu_protocol/plugin/
identity.rs

1use std::path::{Path, PathBuf};
2
3use crate::{ParseError, ShellError, Spanned, shell_error::generic::GenericError};
4
5/// Error when an invalid plugin filename was encountered.
6#[derive(Debug, Clone)]
7pub struct InvalidPluginFilename(PathBuf);
8
9impl std::fmt::Display for InvalidPluginFilename {
10    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11        f.write_str("invalid plugin filename")
12    }
13}
14
15impl From<Spanned<InvalidPluginFilename>> for ParseError {
16    fn from(error: Spanned<InvalidPluginFilename>) -> ParseError {
17        ParseError::LabeledError(
18            "Invalid plugin filename".into(),
19            "must start with `nu_plugin_`".into(),
20            error.span,
21        )
22    }
23}
24
25impl From<Spanned<InvalidPluginFilename>> for ShellError {
26    fn from(error: Spanned<InvalidPluginFilename>) -> ShellError {
27        ShellError::Generic(
28            GenericError::new(
29                format!("Invalid plugin filename: {}", error.item.0.display()),
30                "not a valid plugin filename",
31                error.span,
32            )
33            .with_help("valid Nushell plugin filenames must start with `nu_plugin_`"),
34        )
35    }
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub struct PluginIdentity {
40    /// The filename used to start the plugin
41    filename: PathBuf,
42    /// The shell used to start the plugin, if required
43    shell: Option<PathBuf>,
44    /// The friendly name of the plugin (e.g. `inc` for `C:\nu_plugin_inc.exe`)
45    name: String,
46}
47
48impl PluginIdentity {
49    /// Create a new plugin identity from a path to plugin executable and shell option.
50    ///
51    /// The `filename` must be an absolute path. Canonicalize before trying to construct the
52    /// [`PluginIdentity`].
53    pub fn new(
54        filename: impl Into<PathBuf>,
55        shell: Option<PathBuf>,
56    ) -> Result<PluginIdentity, InvalidPluginFilename> {
57        let filename: PathBuf = filename.into();
58
59        // Must pass absolute path.
60        if filename.is_relative() {
61            return Err(InvalidPluginFilename(filename));
62        }
63
64        let name = filename
65            .file_stem()
66            .map(|stem| stem.to_string_lossy().into_owned())
67            .and_then(|stem| stem.strip_prefix("nu_plugin_").map(|s| s.to_owned()))
68            .ok_or_else(|| InvalidPluginFilename(filename.clone()))?;
69
70        Ok(PluginIdentity {
71            filename,
72            shell,
73            name,
74        })
75    }
76
77    /// The filename of the plugin executable.
78    pub fn filename(&self) -> &Path {
79        &self.filename
80    }
81
82    /// The shell command used by the plugin.
83    pub fn shell(&self) -> Option<&Path> {
84        self.shell.as_deref()
85    }
86
87    /// The name of the plugin, determined by the part of the filename after `nu_plugin_` excluding
88    /// the extension.
89    ///
90    /// - `C:\nu_plugin_inc.exe` becomes `inc`
91    /// - `/home/nu/.cargo/bin/nu_plugin_inc` becomes `inc`
92    pub fn name(&self) -> &str {
93        &self.name
94    }
95
96    /// Create a fake identity for testing.
97    #[cfg(windows)]
98    #[doc(hidden)]
99    pub fn new_fake(name: &str) -> PluginIdentity {
100        PluginIdentity::new(format!(r"C:\fake\path\nu_plugin_{name}.exe"), None)
101            .expect("fake plugin identity path is invalid")
102    }
103
104    /// Create a fake identity for testing.
105    #[cfg(not(windows))]
106    #[doc(hidden)]
107    pub fn new_fake(name: &str) -> PluginIdentity {
108        PluginIdentity::new(format!("/fake/path/nu_plugin_{name}"), None)
109            .expect("fake plugin identity path is invalid")
110    }
111
112    /// A command that could be used to add the plugin, for suggesting in errors.
113    pub fn add_command(&self) -> String {
114        if let Some(shell) = self.shell() {
115            format!(
116                "plugin add --shell '{}' '{}'",
117                shell.display(),
118                self.filename().display(),
119            )
120        } else {
121            format!("plugin add '{}'", self.filename().display())
122        }
123    }
124
125    /// A command that could be used to reload the plugin, for suggesting in errors.
126    pub fn use_command(&self) -> String {
127        format!("plugin use '{}'", self.name())
128    }
129}
130
131#[test]
132fn parses_name_from_path() {
133    assert_eq!("test", PluginIdentity::new_fake("test").name());
134    assert_eq!("test_2", PluginIdentity::new_fake("test_2").name());
135    let absolute_path = if cfg!(windows) {
136        r"C:\path\to\nu_plugin_foo.sh"
137    } else {
138        "/path/to/nu_plugin_foo.sh"
139    };
140    assert_eq!(
141        "foo",
142        PluginIdentity::new(absolute_path, Some("sh".into()))
143            .expect("should be valid")
144            .name()
145    );
146    // Relative paths should be invalid
147    PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into())).expect_err("should be invalid");
148    PluginIdentity::new("other", None).expect_err("should be invalid");
149    PluginIdentity::new("", None).expect_err("should be invalid");
150}