nu_protocol/plugin/
identity.rs1use std::path::{Path, PathBuf};
2
3use crate::{ParseError, ShellError, Spanned, shell_error::generic::GenericError};
4
5#[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 filename: PathBuf,
42 shell: Option<PathBuf>,
44 name: String,
46}
47
48impl PluginIdentity {
49 pub fn new(
54 filename: impl Into<PathBuf>,
55 shell: Option<PathBuf>,
56 ) -> Result<PluginIdentity, InvalidPluginFilename> {
57 let filename: PathBuf = filename.into();
58
59 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 pub fn filename(&self) -> &Path {
79 &self.filename
80 }
81
82 pub fn shell(&self) -> Option<&Path> {
84 self.shell.as_deref()
85 }
86
87 pub fn name(&self) -> &str {
93 &self.name
94 }
95
96 #[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 #[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 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 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 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}