nu_command/path/
dirname.rs

1use super::PathSubcommandArguments;
2use nu_engine::command_prelude::*;
3use nu_protocol::engine::StateWorkingSet;
4use std::path::Path;
5
6struct Arguments {
7    replace: Option<Spanned<String>>,
8    num_levels: Option<i64>,
9}
10
11impl PathSubcommandArguments for Arguments {}
12
13#[derive(Clone)]
14pub struct PathDirname;
15
16impl Command for PathDirname {
17    fn name(&self) -> &str {
18        "path dirname"
19    }
20
21    fn signature(&self) -> Signature {
22        Signature::build("path dirname")
23            .input_output_types(vec![
24                (Type::String, Type::String),
25                (
26                    Type::List(Box::new(Type::String)),
27                    Type::List(Box::new(Type::String)),
28                ),
29            ])
30            .named(
31                "replace",
32                SyntaxShape::String,
33                "Return original path with dirname replaced by this string",
34                Some('r'),
35            )
36            .named(
37                "num-levels",
38                SyntaxShape::Int,
39                "Number of directories to walk up",
40                Some('n'),
41            )
42            .category(Category::Path)
43    }
44
45    fn description(&self) -> &str {
46        "Get the parent directory of a path."
47    }
48
49    fn is_const(&self) -> bool {
50        true
51    }
52
53    fn run(
54        &self,
55        engine_state: &EngineState,
56        stack: &mut Stack,
57        call: &Call,
58        input: PipelineData,
59    ) -> Result<PipelineData, ShellError> {
60        let head = call.head;
61        let args = Arguments {
62            replace: call.get_flag(engine_state, stack, "replace")?,
63            num_levels: call.get_flag(engine_state, stack, "num-levels")?,
64        };
65
66        // This doesn't match explicit nulls
67        if matches!(input, PipelineData::Empty) {
68            return Err(ShellError::PipelineEmpty { dst_span: head });
69        }
70        input.map(
71            move |value| super::operate(&get_dirname, &args, value, head),
72            engine_state.signals(),
73        )
74    }
75
76    fn run_const(
77        &self,
78        working_set: &StateWorkingSet,
79        call: &Call,
80        input: PipelineData,
81    ) -> Result<PipelineData, ShellError> {
82        let head = call.head;
83        let args = Arguments {
84            replace: call.get_flag_const(working_set, "replace")?,
85            num_levels: call.get_flag_const(working_set, "num-levels")?,
86        };
87
88        // This doesn't match explicit nulls
89        if matches!(input, PipelineData::Empty) {
90            return Err(ShellError::PipelineEmpty { dst_span: head });
91        }
92        input.map(
93            move |value| super::operate(&get_dirname, &args, value, head),
94            working_set.permanent().signals(),
95        )
96    }
97
98    #[cfg(windows)]
99    fn examples(&self) -> Vec<Example> {
100        vec![
101            Example {
102                description: "Get dirname of a path",
103                example: "'C:\\Users\\joe\\code\\test.txt' | path dirname",
104                result: Some(Value::test_string("C:\\Users\\joe\\code")),
105            },
106            Example {
107                description: "Get dirname of a list of paths",
108                example: r"[ C:\Users\joe\test.txt, C:\Users\doe\test.txt ] | path dirname",
109                result: Some(Value::test_list(vec![
110                    Value::test_string(r"C:\Users\joe"),
111                    Value::test_string(r"C:\Users\doe"),
112                ])),
113            },
114            Example {
115                description: "Walk up two levels",
116                example: "'C:\\Users\\joe\\code\\test.txt' | path dirname --num-levels 2",
117                result: Some(Value::test_string("C:\\Users\\joe")),
118            },
119            Example {
120                description: "Replace the part that would be returned with a custom path",
121                example: "'C:\\Users\\joe\\code\\test.txt' | path dirname --num-levels 2 --replace C:\\Users\\viking",
122                result: Some(Value::test_string("C:\\Users\\viking\\code\\test.txt")),
123            },
124        ]
125    }
126
127    #[cfg(not(windows))]
128    fn examples(&self) -> Vec<Example> {
129        vec![
130            Example {
131                description: "Get dirname of a path",
132                example: "'/home/joe/code/test.txt' | path dirname",
133                result: Some(Value::test_string("/home/joe/code")),
134            },
135            Example {
136                description: "Get dirname of a list of paths",
137                example: "[ /home/joe/test.txt, /home/doe/test.txt ] | path dirname",
138                result: Some(Value::test_list(vec![
139                    Value::test_string("/home/joe"),
140                    Value::test_string("/home/doe"),
141                ])),
142            },
143            Example {
144                description: "Walk up two levels",
145                example: "'/home/joe/code/test.txt' | path dirname --num-levels 2",
146                result: Some(Value::test_string("/home/joe")),
147            },
148            Example {
149                description: "Replace the part that would be returned with a custom path",
150                example: "'/home/joe/code/test.txt' | path dirname --num-levels 2 --replace /home/viking",
151                result: Some(Value::test_string("/home/viking/code/test.txt")),
152            },
153        ]
154    }
155}
156
157fn get_dirname(path: &Path, span: Span, args: &Arguments) -> Value {
158    let num_levels = args.num_levels.as_ref().map_or(1, |val| *val);
159
160    let mut dirname = path;
161    let mut reached_top = false;
162    for _ in 0..num_levels {
163        dirname = dirname.parent().unwrap_or_else(|| {
164            reached_top = true;
165            dirname
166        });
167        if reached_top {
168            break;
169        }
170    }
171
172    let path = match args.replace {
173        Some(ref newdir) => {
174            let remainder = path.strip_prefix(dirname).unwrap_or(dirname);
175            if !remainder.as_os_str().is_empty() {
176                Path::new(&newdir.item).join(remainder)
177            } else {
178                Path::new(&newdir.item).to_path_buf()
179            }
180        }
181        None => dirname.to_path_buf(),
182    };
183
184    Value::string(path.to_string_lossy(), span)
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_examples() {
193        use crate::test_examples;
194
195        test_examples(PathDirname {})
196    }
197}