file_io/cd.rs
1use crate::path::get_cwd;
2use std::path::{Path, PathBuf};
3
4/// A struct that changes the current working directory to a specified path.
5///
6/// When an instance of this struct goes out of scope (i.e. it is dropped), it automatically
7/// restores the original current working directory.
8#[must_use]
9pub struct CdGuard {
10 /// Path to the original current working directory.
11 original_cwd: PathBuf,
12}
13
14impl CdGuard {
15 /// Constructor.
16 ///
17 /// # Arguments
18 ///
19 /// * `path` - The path to change the current working directory to (can be a `&str`, [`String`],
20 /// [`Path`], or [`PathBuf`]).
21 ///
22 /// # Returns
23 ///
24 /// An instance of [`CdGuard`] that will restore the original directory when dropped.
25 pub fn new<P: AsRef<Path>>(path: P) -> Self {
26 let path = path.as_ref();
27 let original_cwd = get_cwd();
28 std::env::set_current_dir(path)
29 .unwrap_or_else(|_| panic!("Failed to change directory to '{path:?}'."));
30 Self { original_cwd }
31 }
32}
33
34// Restore the original directory when `cd` goes out of scope.
35impl Drop for CdGuard {
36 fn drop(&mut self) {
37 let original_cwd = self.original_cwd.clone();
38 std::env::set_current_dir(&original_cwd)
39 .unwrap_or_else(|_| panic!("Failed to change directory to '{original_cwd:?}'."))
40 }
41}
42
43/// Change the current working directory.
44///
45/// This function works by creating a [`CdGuard`] instance. When the [`CdGuard`] instance goes out
46/// of scope (i.e. when it is dropped), the original current working directory is automatically
47/// restored.
48///
49/// # Arguments
50///
51/// * `path` - The path to change the current working directory to (can be a `&str`, [`String`],
52/// [`Path`], or [`PathBuf`]).
53///
54/// # Returns
55///
56/// A [`CdGuard`] instance that will automatically restore the original current working directory
57/// when it goes out of scope (i.e. when it is dropped).
58///
59/// # Panics
60///
61/// If `path` does not exist or cannot be accessed.
62///
63/// # Example
64///
65/// ```
66/// use file_io::{cd, get_cwd};
67///
68/// // Get the path to the original current working directory.
69/// let original_cwd_path = get_cwd();
70///
71/// // Verify we are in the `file_io` directory.
72/// assert!(original_cwd_path.ends_with("file_io"));
73///
74/// // Define the directory to change to.
75/// let src_path = original_cwd_path.join("src");
76///
77/// // Enter a new scope.
78/// {
79/// // Change to the `src` directory within this limited scope.
80/// let _cd = cd(&src_path);
81///
82/// // Verify the current directory has changed.
83/// assert_eq!(get_cwd(), src_path);
84/// }
85///
86/// // Verify that outside the scope, we are back in the original directory.
87/// assert_eq!(get_cwd(), original_cwd_path);
88/// ```
89pub fn cd<P: AsRef<Path>>(path: P) -> CdGuard {
90 CdGuard::new(path)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::create::create_folder;
97 use crate::path::to_path_buf;
98 use crate::test_utils::get_temp_dir_path;
99 use serial_test::serial;
100 use tempfile::tempdir;
101
102 #[test]
103 #[serial]
104 fn test_cd_without_scope() {
105 // Get the path to the original current working directory.
106 let original_cwd_path = get_cwd();
107
108 // Verify we are in the `file_io` directory.
109 assert!(original_cwd_path.ends_with("file_io"));
110
111 // Define the directory to change to.
112 let src_path = original_cwd_path.join("src");
113
114 // src directory path in different formats.
115 let src_paths: Vec<Box<dyn AsRef<Path>>> = vec![
116 Box::new(src_path.to_str().unwrap()), // &str
117 Box::new(src_path.to_str().unwrap().to_string()), // String
118 Box::new(src_path.as_path()), // Path
119 Box::new(src_path.clone()), // PathBuf
120 ];
121
122 // Test with all different path formats.
123 for src_path in src_paths {
124 // Get a reference to this path representation (i.e. "unbox").
125 let src_path = src_path.as_ref();
126
127 // Change to the `src` directory.
128 let _cd = cd(src_path);
129
130 // Verify the current directory has changed.
131 assert_eq!(get_cwd(), to_path_buf(src_path));
132
133 // Change back to the original directory.
134 let _cd = cd(&original_cwd_path);
135
136 // Verify that we are back in the original directory.
137 assert_eq!(get_cwd(), original_cwd_path);
138 }
139 }
140
141 #[test]
142 #[serial]
143 fn test_cd_with_scope() {
144 // Get the path to the original current working directory.
145 let original_cwd_path = get_cwd();
146
147 // Verify we are in the `file_io` directory.
148 assert!(original_cwd_path.ends_with("file_io"));
149
150 // Define the directory to change to.
151 let src_path = original_cwd_path.join("src");
152
153 // src directory path in different formats.
154 let src_paths: Vec<Box<dyn AsRef<Path>>> = vec![
155 Box::new(src_path.to_str().unwrap()), // &str
156 Box::new(src_path.to_str().unwrap().to_string()), // String
157 Box::new(src_path.as_path()), // Path
158 Box::new(src_path.clone()), // PathBuf
159 ];
160
161 // Test with all different path formats.
162 for src_path in src_paths {
163 // Get a reference to this path representation (i.e. "unbox").
164 let src_path = src_path.as_ref();
165
166 // Enter a new scope.
167 {
168 // Change to the `src` directory within this limited scope.
169 let _cd = cd(src_path);
170
171 // Verify the current directory has changed.
172 assert_eq!(get_cwd(), to_path_buf(src_path));
173 }
174
175 // Verify that outside the scope, we are back in the original directory.
176 assert_eq!(get_cwd(), original_cwd_path);
177 }
178 }
179
180 #[test]
181 #[serial]
182 fn test_cd_with_panic() {
183 // Create a temporary directory to work in and get its path.
184 let temp_dir = tempdir().unwrap();
185 let temp_dir_path = get_temp_dir_path(&temp_dir);
186
187 // Get the path to the original current working directory.
188 let original_cwd_path = get_cwd();
189
190 // Create a folder within the temporary directory to move into.
191 let new_cwd_path = temp_dir_path.join("subfolder");
192 create_folder(&new_cwd_path);
193
194 // Catch the panic inside this scope.
195 let result = std::panic::catch_unwind(|| {
196 // Change to the new directory.
197 let _cd = cd(&temp_dir);
198
199 // Ensure we changed into the new directory.
200 assert_eq!(get_cwd(), new_cwd_path);
201
202 // Simulate failure.
203 panic!("Simulated failure.");
204 });
205
206 // Make sure a panic actually occurred.
207 assert!(result.is_err());
208
209 // Ensure we are back in the original directory.
210 assert_eq!(get_cwd(), original_cwd_path);
211 }
212}