path_macro2/
lib.rs

1#![doc = include_str!("../README.md")]
2
3/// Cross-platform path construction macro.
4///
5/// Returns a [`PathBuf`].
6///
7/// # Supported Syntax
8///
9/// Supports two styles of separators:
10/// - `path!(a / b / c)` — uses slashes (`/`)
11/// - `path!(a, b, c)` — uses commas (`,`).
12///
13/// # Supported Segment Types
14///
15/// - **Identifiers:** `vendor`, `dll` (converted with `stringify!`)
16/// - **Dotted identifiers:** `file.txt`, `windivert.c` (treated as single segments)
17/// - **String literals:** `"my folder"`, `"file name.txt"`
18/// - **Variable expressions:** wrapped in curly braces `{base_path}`, `{my_var}`
19///
20/// # Examples
21///
22/// ```rust
23/// use path_macro2::path;
24///
25/// // Basic usage
26/// let path1 = path!(vendor / dll / windivert.c);
27/// let path2 = path!(vendor, dll, windivert.c);
28///
29/// // Quoted segments (for names containing spaces)
30/// let path3 = path!("my folder" / "sub folder" / file.txt);
31///
32/// // Using variables (wrapped in `{}`)
33/// let base = "vendor";
34/// let path4 = path!({base} / dll / file.txt);
35///
36/// // ---
37/// // Platform-specific examples
38///
39/// // Unix absolute path
40/// #[cfg(not(target_os = "windows"))]
41/// {
42///     let abs = path!("/", "test", "data", "windivert.c");
43///     assert_eq!(abs, std::path::PathBuf::from("/test/data/windivert.c"));
44/// }
45///
46/// // Windows absolute path (with drive letter)
47/// #[cfg(target_os = "windows")]
48/// {
49///     let a = path!("C:\\", "Program Files", "Windivert", "driver.sys");
50///     assert_eq!(
51///         a.to_string_lossy(),
52///         "C:\\Program Files\\Windivert\\driver.sys"
53///     );
54///     // UNC-style path
55///     let unc = path!("\\\\server", "share dir", "file.txt");
56///     assert_eq!(unc.to_string_lossy(), "\\\\server\\share dir\\file.txt");
57/// }
58/// ```
59///
60/// Works consistently across all platforms.
61#[macro_export]
62macro_rules! path {
63    // === Phase 1: Build segments (accumulate tokens until a delimiter is found) ===
64
65    // When encountering a string literal, treat it as a complete segment
66    (@build_seg [$($result:expr),*] [$($current:tt)*] $lit:literal $($rest:tt)*) => {
67        path!(@build_seg [$($result,)* path!(@finish_seg [$($current)*]), $lit.to_string()] [] $($rest)*)
68    };
69
70    // When encountering a variable expression {expr}, treat it as a complete segment
71    (@build_seg [$($result:expr),*] [$($current:tt)*] { $($expr:tt)+ } $($rest:tt)*) => {
72        path!(@build_seg [$($result,)* path!(@finish_seg [$($current)*]), ($($expr)+).to_string()] [] $($rest)*)
73    };
74
75    // When encountering a slash `/`, complete the current segment
76    (@build_seg [$($result:expr),*] [$($current:tt)+] / $($rest:tt)*) => {
77        path!(@build_seg [$($result,)* path!(@finish_seg [$($current)+])] [] $($rest)*)
78    };
79
80    // When encountering a slash `/` but the current segment is empty, skip it
81    (@build_seg [$($result:expr),*] [] / $($rest:tt)*) => {
82        path!(@build_seg [$($result),*] [] $($rest)*)
83    };
84
85    // When encountering a comma `,`, complete the current segment
86    (@build_seg [$($result:expr),*] [$($current:tt)+] , $($rest:tt)*) => {
87        path!(@build_seg [$($result,)* path!(@finish_seg [$($current)+])] [] $($rest)*)
88    };
89
90    // When encountering a comma `,` but the current segment is empty, skip it
91    (@build_seg [$($result:expr),*] [] , $($rest:tt)*) => {
92        path!(@build_seg [$($result),*] [] $($rest)*)
93    };
94
95    // Accumulate normal tokens into the current segment
96    (@build_seg [$($result:expr),*] [$($current:tt)*] $next:tt $($rest:tt)*) => {
97        path!(@build_seg [$($result),*] [$($current)* $next] $($rest)*)
98    };
99
100    // End of tokens: process the final segment (if any)
101    (@build_seg [$($result:expr),*] [$($current:tt)+]) => {
102        vec![$($result,)* path!(@finish_seg [$($current)+])]
103    };
104
105    // End of tokens: no remaining segment
106    (@build_seg [$($result:expr),*] []) => {
107        vec![$($result),*]
108    };
109
110    // === Helper: finalize one segment (stringify or return empty) ===
111    (@finish_seg []) => {
112        String::new()
113    };
114
115    (@finish_seg [$($tokens:tt)+]) => {
116        stringify!($($tokens)+).to_string()
117    };
118
119    // === Entry point ===
120    ($($tokens:tt)*) => {{
121        let segments: Vec<String> = path!(@build_seg [] [] $($tokens)*);
122        let mut path = std::path::PathBuf::new();
123        for seg in segments {
124            if !seg.is_empty() {
125                path.push(seg);
126            }
127        }
128        path
129    }};
130}