1use super::PathSubcommandArguments;
2use nu_engine::command_prelude::*;
3use nu_protocol::engine::StateWorkingSet;
4use std::path::Path;
5
6struct Arguments {
7 extension: Option<Spanned<String>>,
8}
9
10impl PathSubcommandArguments for Arguments {}
11
12#[derive(Clone)]
13pub struct PathParse;
14
15impl Command for PathParse {
16 fn name(&self) -> &str {
17 "path parse"
18 }
19
20 fn signature(&self) -> Signature {
21 Signature::build("path parse")
22 .input_output_types(vec![
23 (Type::String, Type::record()),
24 (Type::List(Box::new(Type::String)), Type::table()),
25 ])
26 .named(
27 "extension",
28 SyntaxShape::String,
29 "Manually supply the extension (without the dot)",
30 Some('e'),
31 )
32 .category(Category::Path)
33 }
34
35 fn description(&self) -> &str {
36 "Convert a path into structured data."
37 }
38
39 fn extra_description(&self) -> &str {
40 r#"Each path is split into a table with 'parent', 'stem' and 'extension' fields.
41On Windows, an extra 'prefix' column is added."#
42 }
43
44 fn is_const(&self) -> bool {
45 true
46 }
47
48 fn run(
49 &self,
50 engine_state: &EngineState,
51 stack: &mut Stack,
52 call: &Call,
53 input: PipelineData,
54 ) -> Result<PipelineData, ShellError> {
55 let head = call.head;
56 let args = Arguments {
57 extension: call.get_flag(engine_state, stack, "extension")?,
58 };
59
60 if matches!(input, PipelineData::Empty) {
62 return Err(ShellError::PipelineEmpty { dst_span: head });
63 }
64 input.map(
65 move |value| super::operate(&parse, &args, value, head),
66 engine_state.signals(),
67 )
68 }
69
70 fn run_const(
71 &self,
72 working_set: &StateWorkingSet,
73 call: &Call,
74 input: PipelineData,
75 ) -> Result<PipelineData, ShellError> {
76 let head = call.head;
77 let args = Arguments {
78 extension: call.get_flag_const(working_set, "extension")?,
79 };
80
81 if matches!(input, PipelineData::Empty) {
83 return Err(ShellError::PipelineEmpty { dst_span: head });
84 }
85 input.map(
86 move |value| super::operate(&parse, &args, value, head),
87 working_set.permanent().signals(),
88 )
89 }
90
91 #[cfg(windows)]
92 fn examples(&self) -> Vec<Example> {
93 vec![
94 Example {
95 description: "Parse a single path",
96 example: r"'C:\Users\viking\spam.txt' | path parse",
97 result: Some(Value::test_record(record! {
98 "prefix" => Value::test_string("C:"),
99 "parent" => Value::test_string(r"C:\Users\viking"),
100 "stem" => Value::test_string("spam"),
101 "extension" => Value::test_string("txt"),
102 })),
103 },
104 Example {
105 description: "Replace a complex extension",
106 example: r"'C:\Users\viking\spam.tar.gz' | path parse --extension tar.gz | upsert extension { 'txt' }",
107 result: None,
108 },
109 Example {
110 description: "Ignore the extension",
111 example: r"'C:\Users\viking.d' | path parse --extension ''",
112 result: Some(Value::test_record(record! {
113 "prefix" => Value::test_string("C:"),
114 "parent" => Value::test_string(r"C:\Users"),
115 "stem" => Value::test_string("viking.d"),
116 "extension" => Value::test_string(""),
117 })),
118 },
119 Example {
120 description: "Parse all paths in a list",
121 example: r"[ C:\Users\viking.d C:\Users\spam.txt ] | path parse",
122 result: Some(Value::test_list(vec![
123 Value::test_record(record! {
124 "prefix" => Value::test_string("C:"),
125 "parent" => Value::test_string(r"C:\Users"),
126 "stem" => Value::test_string("viking"),
127 "extension" => Value::test_string("d"),
128 }),
129 Value::test_record(record! {
130 "prefix" => Value::test_string("C:"),
131 "parent" => Value::test_string(r"C:\Users"),
132 "stem" => Value::test_string("spam"),
133 "extension" => Value::test_string("txt"),
134 }),
135 ])),
136 },
137 ]
138 }
139
140 #[cfg(not(windows))]
141 fn examples(&self) -> Vec<Example> {
142 vec![
143 Example {
144 description: "Parse a path",
145 example: r"'/home/viking/spam.txt' | path parse",
146 result: Some(Value::test_record(record! {
147 "parent" => Value::test_string("/home/viking"),
148 "stem" => Value::test_string("spam"),
149 "extension" => Value::test_string("txt"),
150 })),
151 },
152 Example {
153 description: "Replace a complex extension",
154 example: r"'/home/viking/spam.tar.gz' | path parse --extension tar.gz | upsert extension { 'txt' }",
155 result: None,
156 },
157 Example {
158 description: "Ignore the extension",
159 example: r"'/etc/conf.d' | path parse --extension ''",
160 result: Some(Value::test_record(record! {
161 "parent" => Value::test_string("/etc"),
162 "stem" => Value::test_string("conf.d"),
163 "extension" => Value::test_string(""),
164 })),
165 },
166 Example {
167 description: "Parse all paths in a list",
168 example: r"[ /home/viking.d /home/spam.txt ] | path parse",
169 result: Some(Value::test_list(vec![
170 Value::test_record(record! {
171 "parent" => Value::test_string("/home"),
172 "stem" => Value::test_string("viking"),
173 "extension" => Value::test_string("d"),
174 }),
175 Value::test_record(record! {
176 "parent" => Value::test_string("/home"),
177 "stem" => Value::test_string("spam"),
178 "extension" => Value::test_string("txt"),
179 }),
180 ])),
181 },
182 ]
183 }
184}
185
186fn parse(path: &Path, span: Span, args: &Arguments) -> Value {
187 let mut record = Record::new();
188
189 #[cfg(windows)]
190 {
191 use std::path::Component;
192
193 let prefix = match path.components().next() {
194 Some(Component::Prefix(prefix_component)) => {
195 prefix_component.as_os_str().to_string_lossy()
196 }
197 _ => "".into(),
198 };
199 record.push("prefix", Value::string(prefix, span));
200 }
201
202 let parent = path
203 .parent()
204 .unwrap_or_else(|| "".as_ref())
205 .to_string_lossy();
206
207 record.push("parent", Value::string(parent, span));
208
209 let basename = path
210 .file_name()
211 .unwrap_or_else(|| "".as_ref())
212 .to_string_lossy();
213
214 match &args.extension {
215 Some(Spanned {
216 item: extension,
217 span: extension_span,
218 }) => {
219 let ext_with_dot = [".", extension].concat();
220 if basename.ends_with(&ext_with_dot) && !extension.is_empty() {
221 let stem = basename.trim_end_matches(&ext_with_dot);
222 record.push("stem", Value::string(stem, span));
223 record.push("extension", Value::string(extension, *extension_span));
224 } else {
225 record.push("stem", Value::string(basename, span));
226 record.push("extension", Value::string("", span));
227 }
228 }
229 None => {
230 let stem = path
231 .file_stem()
232 .unwrap_or_else(|| "".as_ref())
233 .to_string_lossy();
234 let extension = path
235 .extension()
236 .unwrap_or_else(|| "".as_ref())
237 .to_string_lossy();
238
239 record.push("stem", Value::string(stem, span));
240 record.push("extension", Value::string(extension, span));
241 }
242 }
243
244 Value::record(record, span)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn test_examples() {
253 use crate::test_examples;
254
255 test_examples(PathParse {})
256 }
257}