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