1use crate::compiler::prelude::*;
2use std::path::Path;
3
4fn dirname(path_str: &str) -> &str {
5 if path_str == "/" {
6 return "/";
7 }
8
9 let path = Path::new(path_str);
10 match path.parent() {
11 Some(parent) => {
12 if parent.as_os_str().is_empty() {
13 "."
14 } else {
15 parent.to_str().unwrap_or(".")
16 }
17 }
18 None => ".",
19 }
20}
21
22#[derive(Clone, Copy, Debug)]
23pub struct DirName;
24
25impl Function for DirName {
26 fn identifier(&self) -> &'static str {
27 "dirname"
28 }
29
30 fn usage(&self) -> &'static str {
31 "Returns the directory component of the given `path`. This is similar to the Unix `dirname` command. The directory component is the path with the final component removed."
32 }
33
34 fn category(&self) -> &'static str {
35 Category::String.as_ref()
36 }
37
38 fn internal_failure_reasons(&self) -> &'static [&'static str] {
39 &["`value` is not a valid string."]
40 }
41
42 fn return_kind(&self) -> u16 {
43 kind::BYTES
44 }
45
46 fn parameters(&self) -> &'static [Parameter] {
47 const PARAMETERS: &[Parameter] = &[Parameter::required(
48 "value",
49 kind::BYTES,
50 "The path from which to extract the directory name.",
51 )];
52 PARAMETERS
53 }
54
55 fn compile(
56 &self,
57 _state: &state::TypeState,
58 _ctx: &mut FunctionCompileContext,
59 arguments: ArgumentList,
60 ) -> Compiled {
61 let value = arguments.required("value");
62
63 Ok(DirNameFn { value }.as_expr())
64 }
65
66 fn examples(&self) -> &'static [Example] {
67 &[
68 example! {
69 title: "Extract dirname from file path",
70 source: r#"dirname!("/usr/local/bin/vrl")"#,
71 result: Ok("\"/usr/local/bin\""),
72 },
73 example! {
74 title: "Extract dirname from file path with extension",
75 source: r#"dirname!("/home/user/file.txt")"#,
76 result: Ok("\"/home/user\""),
77 },
78 example! {
79 title: "Extract dirname from directory path",
80 source: r#"dirname!("/home/user/")"#,
81 result: Ok("\"/home\""),
82 },
83 example! {
84 title: "Root directory dirname is itself",
85 source: r#"dirname!("/")"#,
86 result: Ok("\"/\""),
87 },
88 example! {
89 title: "Relative files have current directory as dirname",
90 source: r#"dirname!("file.txt")"#,
91 result: Ok("\".\""),
92 },
93 ]
94 }
95}
96
97#[derive(Debug, Clone)]
98struct DirNameFn {
99 value: Box<dyn Expression>,
100}
101
102impl FunctionExpression for DirNameFn {
103 fn resolve(&self, ctx: &mut Context) -> Resolved {
104 let value = self.value.resolve(ctx)?;
105 let path_str_cow = value.try_bytes_utf8_lossy()?;
106 let path_str = path_str_cow.as_ref();
107 Ok(Value::from(dirname(path_str)))
108 }
109
110 fn type_def(&self, _: &state::TypeState) -> TypeDef {
111 TypeDef::bytes().fallible()
112 }
113}
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn tdef() -> TypeDef {
119 TypeDef::bytes().fallible()
120 }
121
122 test_function![
123 dirname => DirName;
124
125 home_user_trailing_slash {
126 args: func_args![value: "/home/user/"],
127 want: Ok("/home"),
128 tdef: tdef(),
129 }
130
131 home_user_no_trailing_slash {
132 args: func_args![value: "/home/user"],
133 want: Ok("/home"),
134 tdef: tdef(),
135 }
136
137 root {
138 args: func_args![value: "/"],
139 want: Ok("/"),
140 tdef: tdef(),
141 }
142
143 current_dir {
144 args: func_args![value: "."],
145 want: Ok("."),
146 tdef: tdef(),
147 }
148
149 parent_dir {
150 args: func_args![value: ".."],
151 want: Ok("."),
152 tdef: tdef(),
153 }
154
155 file_in_current_dir {
156 args: func_args![value: "file"],
157 want: Ok("."),
158 tdef: tdef(),
159 }
160
161 hidden_file {
162 args: func_args![value: ".file"],
163 want: Ok("."),
164 tdef: tdef(),
165 }
166
167 empty_string {
168 args: func_args![value: ""],
169 want: Ok("."),
170 tdef: tdef(),
171 }
172
173 double_extension {
174 args: func_args![value: "file.tar.gz"],
175 want: Ok("."),
176 tdef: tdef(),
177 }
178
179 path_with_extension {
180 args: func_args![value: "/home/user/file.txt"],
181 want: Ok("/home/user"),
182 tdef: tdef(),
183 }
184 ];
185}