file_io/
modify.rs

1use crate::load::load_file_as_string;
2use crate::save::save_string_to_file;
3use std::panic;
4use std::path::Path;
5use walkdir::WalkDir;
6
7/// Replaces all occurrences of a string in a file.
8///
9/// # Arguments
10///
11/// * `path` - Path to the file where the replacements will be performed (can be a `&str`,
12///   [`String`], [`Path`], or [`std::path::PathBuf`]).
13/// * `old_string` - The substring to find and replace in all files.
14/// * `new_string` - The replacement string.
15///
16/// # Panics
17///
18/// If some error is encountered while reading from or writing to the file.
19///
20/// # Examples
21///
22/// ## Using a string literal
23///
24/// ```
25/// use file_io::{load_file_as_string, replace_str_in_file, save_string_to_file};
26///
27/// // Path to file.
28/// let path: &str = "folder/subfolder_8/file_5.txt";
29///
30/// // Create a file with some content.
31/// save_string_to_file("Hello, world!", path);
32///
33/// // Replace "Hello" with "Goodbye".
34/// replace_str_in_file(path, "Hello", "Goodbye");
35///
36/// // Verify that the content was replaced.
37/// let content = load_file_as_string(path);
38/// assert_eq!(content, "Goodbye, world!");
39/// ```
40///
41/// ## Using a `Path` reference
42///
43/// ```
44/// use file_io::{load_file_as_string, replace_str_in_file, save_string_to_file};
45/// use std::path::Path;
46///
47/// // Path to file.
48/// let path: &Path = Path::new("folder/subfolder_8/file_6.txt");
49///
50/// // Create a file with some content.
51/// save_string_to_file("Hello, world!", path);
52///
53/// // Replace "Hello" with "Goodbye".
54/// replace_str_in_file(path, "Hello", "Goodbye");
55///
56/// // Verify that the content was replaced.
57/// let content = load_file_as_string(path);
58/// assert_eq!(content, "Goodbye, world!");
59/// ```
60pub fn replace_str_in_file<P: AsRef<Path>>(path: P, old_string: &str, new_string: &str) {
61    // Load the file into a string.
62    let content = load_file_as_string(&path);
63
64    // Replace all instances of `old_string` with `new_string`.
65    if content.contains(old_string) {
66        let new_content = content.replace(old_string, new_string);
67        save_string_to_file(&new_content, path);
68    }
69}
70
71/// Replaces all occurrences of a string in all files within a directory (including subdirectories).
72///
73/// # Arguments
74///
75/// * `path` - Path to the directory or file where the replacements will be performed (can be a
76///   `&str`, [`String`], [`Path`], or [`std::path::PathBuf`]).
77/// * `old_string` - The substring to find and replace in all files.
78/// * `new_string` - The replacement string.
79///
80/// # Note
81///
82/// This function will not panic if a single read/write fails (since this function may pull in
83/// private, inaccessible files). However, a warning will be printed to `stderr`.
84///
85/// # Examples
86///
87/// ```ignore
88/// use file_io::replace_str_in_files;
89///
90/// let dir = Path::new("/path/to/folder");
91///
92/// // Replace "foo" with "bar" in all files within the "/path/to/folder/" directory (including
93/// // subdirectories).
94/// replace_str_in_files(dir, "foo", "bar");
95/// ```
96pub fn replace_str_in_files<P: AsRef<Path>>(path: P, old_string: &str, new_string: &str) {
97    // Traverse over all entries (files and folders) in the directory and its subdirectories.
98    for entry in WalkDir::new(path).into_iter().filter_map(Result::ok) {
99        // Get the path of the current entry.
100        let entry_path = entry.path();
101
102        // If the entry is a file, replace any instances of `old_string` with `new_string`.
103        if entry_path.is_file() {
104            // We use `panic::catch_unwind` to handle any potential panics gracefully (since some
105            // folders could have private, inaccessible files).
106            let result =
107                panic::catch_unwind(|| replace_str_in_file(entry_path, old_string, new_string));
108
109            // If the replacement failed, print an error message to `stderr`.
110            if result.is_err() {
111                eprintln!(
112                    "Failed to replace string in file '{}'.",
113                    entry_path.display(),
114                );
115            }
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::test_utils::get_temp_dir_path;
124    use std::path::PathBuf;
125    use tempfile::tempdir;
126
127    #[test]
128    fn test_replace_str_in_file() {
129        // Create a temporary directory.
130        let temp_dir = tempdir().unwrap();
131
132        // Get the path to the temporary directory.
133        let temp_dir_path = get_temp_dir_path(&temp_dir);
134
135        // File path.
136        let file_path: PathBuf = temp_dir_path.join("test_file.txt");
137
138        // File path in different formats.
139        let file_paths: Vec<Box<dyn AsRef<Path>>> = vec![
140            Box::new(file_path.to_str().unwrap()),             // &str
141            Box::new(file_path.to_str().unwrap().to_string()), // String
142            Box::new(file_path.as_path()),                     // Path
143            Box::new(file_path.clone()),                       // PathBuf
144        ];
145
146        // Test with all different path formats.
147        for file_path in file_paths {
148            // Get a reference to this path representation (i.e. "unbox").
149            let file_path = file_path.as_ref();
150
151            // Create a file with some content.
152            save_string_to_file("Hello, world, hello, Hello!", file_path);
153
154            // Replace "Hello" with "Goodbye".
155            replace_str_in_file(file_path, "Hello", "Goodbye");
156
157            // Verify that the content was replaced.
158            let content = load_file_as_string(file_path);
159            assert_eq!(content, "Goodbye, world, hello, Goodbye!");
160        }
161    }
162
163    #[test]
164    fn test_replace_str_in_files_basic() {
165        // Create a temporary directory.
166        let temp_dir = tempdir().unwrap();
167
168        // Get the path to the temporary directory.
169        let temp_dir_path = get_temp_dir_path(&temp_dir);
170
171        // Paths to files.
172        let file_1_path = temp_dir_path.join("file_1.txt");
173        let file_2_path = temp_dir_path.join("file_2.txt");
174        let file_3_path = temp_dir_path.join("file_3.txt");
175
176        // File paths in different formats.
177        let file_1_paths: Vec<Box<dyn AsRef<Path>>> = vec![
178            Box::new(file_1_path.to_str().unwrap()),             // &str
179            Box::new(file_1_path.to_str().unwrap().to_string()), // String
180            Box::new(file_1_path.as_path()),                     // Path
181            Box::new(file_1_path.clone()),                       // PathBuf
182        ];
183        let file_2_paths: Vec<Box<dyn AsRef<Path>>> = vec![
184            Box::new(file_2_path.to_str().unwrap()),             // &str
185            Box::new(file_2_path.to_str().unwrap().to_string()), // String
186            Box::new(file_2_path.as_path()),                     // Path
187            Box::new(file_2_path.clone()),                       // PathBuf
188        ];
189        let file_3_paths: Vec<Box<dyn AsRef<Path>>> = vec![
190            Box::new(file_3_path.to_str().unwrap()),             // &str
191            Box::new(file_3_path.to_str().unwrap().to_string()), // String
192            Box::new(file_3_path.as_path()),                     // Path
193            Box::new(file_3_path.clone()),                       // PathBuf
194        ];
195
196        // Contents of the files.
197        let file_1_contents = "hello foo world";
198        let file_2_contents = "no foo here";
199        let file_3_contents = "nothing to replace";
200
201        // Test with all different path formats.
202        for ((file_1_path, file_2_path), file_3_path) in
203            file_1_paths.into_iter().zip(file_2_paths).zip(file_3_paths)
204        {
205            // Get a reference to the path representations (i.e. "unbox").
206            let file_1_path = file_1_path.as_ref();
207            let file_2_path = file_2_path.as_ref();
208            let file_3_path = file_3_path.as_ref();
209
210            // Create files with known content.
211            save_string_to_file(file_1_contents, file_1_path);
212            save_string_to_file(file_2_contents, file_2_path);
213            save_string_to_file(file_3_contents, file_3_path);
214
215            // Run the replacement function.
216            replace_str_in_files(&temp_dir_path, "foo", "bar");
217
218            // Check that file 1 content changed.
219            let content1 = load_file_as_string(file_1_path);
220            assert_eq!(content1, "hello bar world");
221
222            // Check that file 2 content changed.
223            let content2 = load_file_as_string(file_2_path);
224            assert_eq!(content2, "no bar here");
225
226            // Check that file 3 content is unchanged.
227            let content3 = load_file_as_string(file_3_path);
228            assert_eq!(content3, "nothing to replace");
229        }
230    }
231
232    #[test]
233    fn test_replace_str_in_files_nested() {
234        // Create a temporary directory.
235        let temp_dir = tempdir().unwrap();
236
237        // Get the path to the temporary directory.
238        let temp_dir_path = get_temp_dir_path(&temp_dir);
239
240        // File paths.
241        let root_file_path = temp_dir_path.join("root.txt");
242        let nested_file_path = temp_dir_path.join("nested/nested.txt");
243
244        // File paths in different formats.
245        let root_file_paths: Vec<Box<dyn AsRef<Path>>> = vec![
246            Box::new(root_file_path.to_str().unwrap()), // &str
247            Box::new(root_file_path.to_str().unwrap().to_string()), // String
248            Box::new(root_file_path.as_path()),         // Path
249            Box::new(root_file_path.clone()),           // PathBuf
250        ];
251        let nested_file_paths: Vec<Box<dyn AsRef<Path>>> = vec![
252            Box::new(nested_file_path.to_str().unwrap()), // &str
253            Box::new(nested_file_path.to_str().unwrap().to_string()), // String
254            Box::new(nested_file_path.as_path()),         // Path
255            Box::new(nested_file_path.clone()),           // PathBuf
256        ];
257
258        // Test with all different path formats.
259        for (root_file_path, nested_file_path) in root_file_paths.into_iter().zip(nested_file_paths)
260        {
261            // Get a reference to the path representations (i.e. "unbox").
262            let root_file_path = root_file_path.as_ref();
263            let nested_file_path = nested_file_path.as_ref();
264
265            // Create files in the root and nested directories.
266            save_string_to_file("replace me", root_file_path);
267            save_string_to_file("replace me too", nested_file_path);
268
269            // Replace "replace" with "changed".
270            replace_str_in_files(temp_dir.path(), "replace", "changed");
271
272            // Check root file content.
273            let root_content = load_file_as_string(root_file_path);
274            assert_eq!(root_content, "changed me");
275
276            // Check nested file content.
277            let nested_content = load_file_as_string(nested_file_path);
278            assert_eq!(nested_content, "changed me too");
279        }
280    }
281}