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}