nu_command/path/
parse.rs

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        // This doesn't match explicit nulls
61        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        // This doesn't match explicit nulls
82        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}