1 /** 
2  * Utilities for interacting with the file system.
3  */
4 module dshutils.fileutils;
5 
6 import std.file;
7 
8 /** 
9  * Removes a file or directory if it exists.
10  * Params:
11  *   file = The file or directory to remove.
12  *   recursive = Whether to recursively remove subdirectories.
13  * Returns: True if the file or directory was removed, or false otherwise.
14  */
15 public bool removeIfExists(string file, bool recursive = true) {
16     if (!exists(file)) return false;
17     if (isFile(file)) {
18         remove(file);
19     } else if (isDir(file)) {
20         if (recursive) {
21             rmdirRecurse(file);
22         } else {
23             rmdir(file);
24         }
25     }
26     return true;
27 }
28 
29 /** 
30  * Removes a set of files or directories, if they exist.
31  * Returns: True if any of the given files were removed.
32  */
33 public bool removeAnyIfExists(string[] files...) {
34     bool any = false;
35     foreach (file; files) {
36         bool result = removeIfExists(file);
37         any = any || result;
38     }
39     return any;
40 }
41 
42 /** 
43  * Checks if a directory is empty.
44  * Params:
45  *   dir = The directory to check.
46  * Returns: True if the given directory exists, is a directory, and is empty.
47  */
48 public bool isDirEmpty(string dir) {
49     import std.range : empty;
50     import std.array : array;
51     if (!exists(dir) || !isDir(dir)) return false;
52     return empty(dirEntries(dir, SpanMode.shallow).array);
53 }
54 
55 unittest {
56     assert(!isDirEmpty("source"));
57     assert(!isDirEmpty("source/dsh.d"));
58     assert(!isDirEmpty("source/some-non-existant-file.blah"));
59     auto d = ".test_empty_dir";
60     d.mkdir;
61     scope(exit) d.rmdir;
62     assert(isDirEmpty(d));
63 }
64 
65 /** 
66  * Gets the user's home directory.
67  * Returns: The user's home directory, or null if it could not be determined.
68  */
69 public string getHomeDir() {
70     import std.process : environment;
71     version (linux) {
72         return environment.get("HOME");
73     }
74     version (Windows) {
75         string drive = environment.get("HOMEDRIVE");
76         string path = environment.get("HOMEPATH");
77         if (drive is null || path is null) return null;
78         return drive ~ path;
79     }
80 }
81 
82 /** 
83  * Walks through all the entries in a directory, and applies the given visitor
84  * function to all entries.
85  * Params:
86  *   dir = The directory to walk through.
87  *   visitor = A visitor delegate function to apply to all entries discovered.
88  *   recursive = Whether to recursively walk through subdirectories.
89  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
90  */
91 public void walkDir(string dir, void delegate(DirEntry entry) visitor, bool recursive = true, int maxDepth = -1) {
92     if (!exists(dir) || !isDir(dir)) return;
93     foreach (DirEntry entry; dirEntries(dir, SpanMode.shallow)) {
94         visitor(entry);
95         if (recursive && entry.isDir && (maxDepth > 0 || maxDepth == -1)) {
96             walkDir(entry.name, visitor, recursive, maxDepth - 1);
97         }
98     }
99 }
100 
101 unittest {
102     ulong totalSize = 0;
103     uint filesVisited = 0;
104     void addSize(DirEntry entry) {
105         totalSize += entry.size;
106         filesVisited++;
107     }
108     walkDir(".", &addSize);
109     assert(totalSize > 0);
110     assert(filesVisited > 10);
111 }
112 
113 /** 
114  * Walks through all the entries in a directory, and applies the given visitor
115  * function to all entries for which a given filter function returns true.
116  * Params:
117  *   dir = The directory to walk through.
118  *   visitor = A visitor delegate function to apply to all entries discovered.
119  *   filter = A filter delegate that determines if an entry should be visited.
120  *   recursive = Whether to recursively walk through subdirectories.
121  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
122  */
123 public void walkDirFiltered(
124     string dir,
125     void delegate(DirEntry entry) visitor,
126     bool delegate(DirEntry entry) filter,
127     bool recursive = true,
128     int maxDepth = -1
129 ) {
130     walkDir(dir, delegate(DirEntry entry) {
131         if (filter(entry)) visitor(entry);
132     }, recursive, maxDepth);
133 }
134 
135 /** 
136  * Walks through all files in a directory.
137  * Params:
138  *   dir = The directory to walk through.
139  *   visitor = A visitor delegate function to apply to all entries discovered.
140  *   recursive = Whether to recursively walk through subdirectories.
141  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
142  */
143 public void walkDirFiles(
144     string dir,
145     void delegate(DirEntry entry) visitor,
146     bool recursive = true,
147     int maxDepth = -1
148 ) {
149     walkDirFiltered(dir, visitor, (entry) {return entry.isFile;}, recursive, maxDepth);
150 }
151 
152 /** 
153  * Finds matching files in a directory.
154  * Params:
155  *   dir = The directory to search in.
156  *   pattern = A regex pattern to match against each filename.
157  *   recursive = Whether to recursively search subdirectories.
158  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
159  * Returns: A list of matching filenames.
160  */
161 public string[] findFiles(string dir, string pattern, bool recursive = true, int maxDepth = -1) {
162     import std.regex : matchFirst, Captures;
163     import std.path : baseName;
164     string[] matches = [];
165     walkDirFiles(dir, (entry) {
166         string filename = baseName(entry.name);
167         Captures!string c = matchFirst(filename, pattern);
168         if (!c.empty && c.hit.length == filename.length) matches ~= entry.name;
169     }, recursive, maxDepth);
170     return matches;
171 }
172 
173 unittest {
174     assert(findFiles("source", ".*\\.d", true, 0) == ["source/dsh.d"]);
175     assert(findFiles(".", "dub.*", false).length == 2);
176 }
177 
178 /** 
179  * Finds all files in a directory that end with the given extension text.
180  * Params:
181  *   dir = The directory to search in.
182  *   extension = The extension for matching files, such as ".txt" or ".d"
183  *   recursive = Whether to recursively search subdirectories.
184  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
185  * Returns: The list of matching files.
186  */
187 public string[] findFilesByExtension(string dir, string extension, bool recursive = true, int maxDepth = -1) {
188     return findFiles(dir, ".*" ~ extension, recursive, maxDepth);
189 }
190 
191 unittest {
192     assert(findFilesByExtension(".", "json").length == 2);
193 }
194 
195 /** 
196  * Tries to find a single file matching the given pattern.
197  * Params:
198  *   dir = The directory to search in.
199  *   pattern = A regex pattern to match against each filename.
200  *   recursive = Whether to recursively search subdirectories.
201  *   maxDepth = How deep to search recursively. -1 indicates infinite recursion.
202  * Returns: The single matching file that was found, or null if no match was
203  * found, or if multiple matches were found.
204  */
205 public string findFile(string dir, string pattern, bool recursive = true, int maxDepth = -1) {
206     auto matches = findFiles(dir, pattern, recursive, maxDepth);
207     if (matches.length != 1) return null;
208     return matches[0];
209 }
210 
211 unittest {
212     assert(findFile(".", "dub\\.json") !is null);
213     assert(findFile(".", ".*\\.d") is null);
214 }
215 
216 /** 
217  * Copies all files from the given source directory, to the given destination
218  * directory. Will create the destination directory if it doesn't exist yet.
219  * Overwrites any files that already exist in the destination directory.
220  * Params:
221  *   sourceDir = The source directory to copy from.
222  *   destDir = The destination directory to copy to.
223  */
224 public void copyDir(string sourceDir, string destDir) {
225     if (!isDir(sourceDir)) return;
226     if (exists(destDir) && !isDir(destDir)) return;
227     if (!exists(destDir)) mkdirRecurse(destDir);
228     import std.path : buildPath, baseName;
229     foreach (DirEntry entry; dirEntries(sourceDir, SpanMode.shallow)) {
230         string destPath = buildPath(destDir, baseName(entry.name));
231         if (entry.isDir) {
232             copyDir(entry.name, destPath);
233         } else if (entry.isFile) {
234             copy(entry.name, destPath);
235         }
236     }
237 }
238 
239 unittest {
240     copyDir("source", "source2");
241     assert(exists("source2") && isDir("source2"));
242     assert(exists("source2/dsh.d"));
243     rmdirRecurse("source2");
244 }