├── .github ├── dependabot.yaml └── workflows │ └── rust.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md └── src └── lib.rs /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | - name: Run Clippy 24 | run: cargo clippy --all-targets --all-features 25 | - name: Run fmt 26 | run: cargo fmt --check 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | a/ 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Just raise a PR or an issue to open the conversation. All contributions welcome. 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "with_dir" 3 | version = "0.1.4" 4 | edition = "2021" 5 | authors = ["Huw Percival "] 6 | license-file = "LICENSE" 7 | description = "Scoped current working directory" 8 | repository = "https://github.com/huwper/with_dir" 9 | keywords = ["with_dir", "cwd", "with_cwd", "filesystem"] 10 | 11 | [dependencies] 12 | parking_lot = "0.12" 13 | tempfile = "3.4" 14 | 15 | [dev-dependencies] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Huw Percival 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # with_dir 2 | 3 | Blazingly fast utility library for temporarily changing the current working directory. 4 | 5 | This library provides the following features: 6 | 7 | 1. Convenient scoped changing of directories 8 | 2. Global Reentrant mutex to prevent concurrent instances of WithDir from conflicting. 9 | 10 | The mutex allows this to be safely used across multhreaded tests, where each test 11 | will be entering different directories as no two WithDir instances can exist on different threads. 12 | However nested instances on the same thread can exist. 13 | 14 | ```rust 15 | use with_dir::WithDir; 16 | use std::path::Path; 17 | 18 | let path = Path::new("src"); 19 | 20 | // enter that directory 21 | WithDir::new(path).map(|_| { 22 | // Current working directory is now src 23 | }).unwrap(); 24 | // cwd is reset 25 | ``` 26 | 27 | ## [Documentation](https://docs.rs/with_dir) 28 | 29 | ## Contributing 30 | 31 | Contributions welcome. 32 | 33 | ## FAQ 34 | 35 | ### Is it good? 36 | 37 | yes. 38 | 39 | ## License 40 | 41 | See [LICENSE](./LICENSE) 42 | 43 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Library provides the struct [WithDir](crate::WithDir) which uses RAII 2 | //! to enable scoped change of working directory. See docs for [WithDir](crate::WithDir) 3 | //! for simple example. 4 | use parking_lot::{ReentrantMutex, ReentrantMutexGuard}; 5 | use std::{ 6 | env::{current_dir, set_current_dir}, 7 | fs::{create_dir, create_dir_all}, 8 | path::{Path, PathBuf}, 9 | }; 10 | use tempfile::TempDir; 11 | 12 | static DIR_MUTEX: ReentrantMutex<()> = ReentrantMutex::new(()); 13 | 14 | enum Cwd { 15 | Temp(TempDir), 16 | NotTemp(PathBuf), 17 | } 18 | 19 | /// Scoped modifier of the current working directory. This uses RAII to set the 20 | /// current working directory back to what it was when the instance is dropped. 21 | /// This struct uses a static `parking_lot::ReentrantMutex` to prevent `WithDir` on other 22 | /// threads from updating the current working directory while any WithDir instances 23 | /// exist. However there is nothing stopping other threads from calling `std::env::set_current_dir` 24 | /// directly which would override the working directory. 25 | /// 26 | /// WithDir should be created with `new` which returns a result. Result couldbe Err if the 27 | /// directory doesn't exist, or if the user does not have permission to access. 28 | /// 29 | /// ``` 30 | /// use with_dir::WithDir; 31 | /// 32 | /// // create a directory 33 | /// let path = std::env::current_dir().unwrap().join("a"); 34 | /// if !path.exists() { 35 | /// std::fs::create_dir(&path); 36 | /// } 37 | /// 38 | /// // enter that directory 39 | /// WithDir::new(&path).map( |_| { 40 | /// assert_eq!(std::env::current_dir().unwrap(), path); 41 | /// }).unwrap(); 42 | /// 43 | /// // cwd is reset 44 | /// 45 | /// // enter it again 46 | /// let cwd = WithDir::new("a").unwrap(); 47 | /// // exit it 48 | /// cwd.leave().unwrap(); 49 | /// ``` 50 | /// 51 | /// 52 | pub struct WithDir<'a> { 53 | original_dir: PathBuf, 54 | cwd: Cwd, 55 | mutex: Option>, 56 | } 57 | 58 | impl<'a> WithDir<'a> { 59 | /// On creation, the current working directory is set to `path` 60 | /// and a [ReentrantMutexGuard](parking_lot::ReentrantMutexGuard) is claimed. 61 | pub fn new(path: impl AsRef) -> Result, std::io::Error> { 62 | let m = DIR_MUTEX.lock(); 63 | let original_dir = current_dir()?; 64 | set_current_dir(&path)?; 65 | Ok(WithDir { 66 | original_dir, 67 | cwd: Cwd::NotTemp(path.as_ref().to_owned()), 68 | mutex: Some(m), 69 | }) 70 | } 71 | 72 | /// Uses [TempDir](tempfile::TempDir) to create a temporary 73 | /// directory that with the same lifetime as the returned 74 | /// `WithDir`. The current working dir is change to the temp_dir 75 | pub fn temp() -> Result, std::io::Error> { 76 | let m = DIR_MUTEX.lock(); 77 | let original_dir = current_dir()?; 78 | let temp_dir = TempDir::new()?; 79 | set_current_dir(temp_dir.path())?; 80 | Ok(WithDir { 81 | original_dir, 82 | cwd: Cwd::Temp(temp_dir), 83 | mutex: Some(m), 84 | }) 85 | } 86 | 87 | /// Makes a directory and changes the current working dir to that directory, 88 | /// the directory will persist after this `WithDir` is dropped. Use 89 | /// [create_all](crate::WithDir::create_all) if you want to also make the parent directories 90 | pub fn create(path: impl AsRef) -> Result, std::io::Error> { 91 | let m = DIR_MUTEX.lock(); 92 | let original_dir = current_dir()?; 93 | create_dir(&path)?; 94 | set_current_dir(&path)?; 95 | Ok(WithDir { 96 | original_dir, 97 | cwd: Cwd::NotTemp(path.as_ref().to_path_buf()), 98 | mutex: Some(m), 99 | }) 100 | } 101 | 102 | /// See [create](crate::WithDir::create) for docs 103 | pub fn create_all(path: impl AsRef) -> Result, std::io::Error> { 104 | let m = DIR_MUTEX.lock(); 105 | let original_dir = current_dir()?; 106 | create_dir_all(&path)?; 107 | set_current_dir(&path)?; 108 | Ok(WithDir { 109 | original_dir, 110 | cwd: Cwd::NotTemp(path.as_ref().to_path_buf()), 111 | mutex: Some(m), 112 | }) 113 | } 114 | 115 | /// Get that path that was changed to when this instance 116 | /// was created 117 | pub fn path(&self) -> &Path { 118 | match &self.cwd { 119 | Cwd::NotTemp(p) => p, 120 | Cwd::Temp(p) => p.path(), 121 | } 122 | } 123 | 124 | fn reset_cwd(&self) -> Result<(), std::io::Error> { 125 | set_current_dir(&self.original_dir) 126 | } 127 | 128 | /// Return to original working directory. This is exactly the 129 | /// same as dropping the instance but will not panic. 130 | pub fn leave(mut self) -> Result<(), std::io::Error> { 131 | let ret = self.reset_cwd(); 132 | self.mutex = None; 133 | ret 134 | } 135 | } 136 | 137 | impl AsRef for WithDir<'_> { 138 | /// Returns the current working directory that was set when this 139 | /// instance was created. 140 | fn as_ref(&self) -> &Path { 141 | self.path() 142 | } 143 | } 144 | 145 | impl Drop for WithDir<'_> { 146 | /// Resets current working directory to whatever it was 147 | /// when this instance was created. 148 | /// 149 | /// # Panics 150 | /// 151 | /// Panics if the original directory is no longer accesible (has been deleted, etc.) 152 | fn drop(&mut self) { 153 | if self.mutex.is_some() { 154 | self.reset_cwd().unwrap(); 155 | } 156 | } 157 | } 158 | 159 | // test the code in the readme 160 | #[doc = include_str!("../README.md")] 161 | #[cfg(doctest)] 162 | pub struct ReadmeDoctests; 163 | 164 | #[cfg(test)] 165 | mod tests { 166 | use std::{fs::create_dir_all, thread}; 167 | 168 | use super::*; 169 | 170 | #[test] 171 | fn it_works() { 172 | let cwd = current_dir().unwrap(); 173 | let a = cwd.join("a"); 174 | let b = a.join("b"); 175 | 176 | if !b.exists() { 177 | create_dir_all(&b).unwrap(); 178 | } 179 | 180 | match WithDir::new(&a) { 181 | Ok(_) => { 182 | let cwd = current_dir().unwrap(); 183 | assert_eq!(cwd, a); 184 | WithDir::new(&b) 185 | .map(|wd| { 186 | let cwd = current_dir().unwrap(); 187 | assert_eq!(cwd, wd.path()); 188 | }) 189 | .unwrap(); 190 | let cwd = current_dir().unwrap(); 191 | assert_eq!(cwd, a); 192 | } 193 | Err(e) => { 194 | println!("{:?}", e); 195 | panic!("failed"); 196 | } 197 | }; 198 | } 199 | 200 | fn threaded_test_worker(p: &Path) { 201 | let a = p.join("a"); 202 | let b = a.join("b"); 203 | 204 | if !b.exists() { 205 | create_dir_all(&b).unwrap(); 206 | } 207 | 208 | match WithDir::new(&a) { 209 | Ok(_) => { 210 | let cwd = current_dir().unwrap(); 211 | assert_eq!(cwd, a); 212 | 213 | { 214 | let wd = WithDir::new(&b).unwrap(); 215 | let cwd = current_dir().unwrap(); 216 | assert_eq!(cwd, wd.path()); 217 | }; 218 | 219 | let cwd = current_dir().unwrap(); 220 | assert_eq!(cwd, a); 221 | 222 | // test leave 223 | let wd = WithDir::new(&b).unwrap(); 224 | let cwd = current_dir().unwrap(); 225 | assert_eq!(cwd, wd.path()); 226 | wd.leave().unwrap(); 227 | 228 | let cwd = current_dir().unwrap(); 229 | assert_eq!(cwd, a); 230 | } 231 | Err(e) => panic!("{}", e), 232 | }; 233 | } 234 | 235 | #[test] 236 | fn test_multithreaded() { 237 | let cwd = current_dir().unwrap(); 238 | let p1 = cwd.join("a/1"); 239 | let p2 = cwd.join("a/2"); 240 | let t1 = thread::spawn(move || threaded_test_worker(&p1)); 241 | let t2 = thread::spawn(move || threaded_test_worker(&p2)); 242 | t1.join().unwrap(); 243 | t2.join().unwrap(); 244 | } 245 | 246 | #[test] 247 | fn test_create_dir() { 248 | let cwd = current_dir().unwrap(); 249 | WithDir::create_all(cwd.join("a/create")) 250 | .map(|new_dir| { 251 | assert_eq!(current_dir().unwrap(), new_dir.path()); 252 | 253 | WithDir::create(cwd.join("a/create/b")) 254 | .map(|new_dir| { 255 | assert_eq!(current_dir().unwrap(), new_dir.path()); 256 | }) 257 | .unwrap(); 258 | }) 259 | .unwrap(); 260 | 261 | assert_eq!(cwd, current_dir().unwrap()); 262 | assert!(cwd.join("a/create/b").exists()); 263 | } 264 | 265 | #[test] 266 | fn test_temp_dir() { 267 | let cwd = current_dir().unwrap(); 268 | let mut dir: Option = None; 269 | 270 | WithDir::temp() 271 | .map(|d| { 272 | // path we changed to != original path 273 | assert_ne!(d.path(), cwd); 274 | dir = Some(current_dir().unwrap()); 275 | }) 276 | .unwrap(); 277 | 278 | // temp dir was deleted 279 | assert!(!dir.unwrap().exists()); 280 | } 281 | } 282 | --------------------------------------------------------------------------------