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