1 /**
2  * ae.utils.path
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.path;
15 
16 import std.algorithm.searching;
17 import std.path;
18 
19 /// Modify a path under oldBase to a new path with the same subpath under newBase.
20 /// E.g.: `/foo/bar`.rebasePath(`/foo`, `/quux`) == `/quux/bar`
21 string rebasePath(string path, string oldBase, string newBase)
22 {
23 	return buildPath(newBase, path.relPath(oldBase));
24 }
25 
26 /// Variant of std.path.relativePath with the following differences:
27 /// - Works with relative paths.
28 ///   If either path is relative, it is first resolved to an absolute path.
29 /// - If `path` starts with `base`, avoids allocating.
30 string relPath(string path, string base)
31 {
32 	if (base.length && path.length > base.length &&
33 		path[0..base.length] == base)
34 	{
35 		if (base[$-1].isDirSeparator)
36 			return path[base.length..$];
37 		if (path[base.length].isDirSeparator)
38 			return path[base.length+1..$];
39 	}
40 	return relativePath(path.absolutePath, base.absolutePath);
41 }
42 
43 deprecated alias fastRelativePath = relPath;
44 
45 unittest
46 {
47 	version(Windows)
48 	{
49 		assert(relPath(`C:\a\b\c`, `C:\a`) == `b\c`);
50 		assert(relPath(`C:\a\b\c`, `C:\a\`) == `b\c`);
51 		assert(relPath(`C:\a\b\c`, `C:\a/`) == `b\c`);
52 		assert(relPath(`C:\a\b\c`, `C:\a\d`) == `..\b\c`);
53 	}
54 	else
55 	{
56 		assert(relPath("/a/b/c", "/a") == "b/c");
57 		assert(relPath("/a/b/c", "/a/") == "b/c");
58 		assert(relPath("/a/b/c", "/a/d") == "../b/c");
59 	}
60 }
61 
62 /// Like Pascal's IncludeTrailingPathDelimiter
63 string includeTrailingPathSeparator(string path)
64 {
65 	if (path.length && !path[$-1].isDirSeparator())
66 		path ~= dirSeparator;
67 	return path;
68 }
69 
70 /// Like Pascal's ExcludeTrailingPathDelimiter
71 string excludeTrailingPathSeparator(string path)
72 {
73 	if (path.length && path[$-1].isDirSeparator())
74 		path = path[0..$-1];
75 	return path;
76 }
77 
78 /// Like startsWith, but pathStartsWith("/foo/barbara", "/foo/bar") is false.
79 bool pathStartsWith(in char[] path, in char[] prefix)
80 {
81 	// Special cases to accommodate relativePath(path, path) results
82 	if (prefix == "" || prefix == ".")
83 		return true;
84 
85 	return path.startsWith(prefix) &&
86 		(path.length == prefix.length || isDirSeparator(path[prefix.length]));
87 }
88 
89 unittest
90 {
91 	assert( "/foo/bar"    .pathStartsWith("/foo/bar"));
92 	assert( "/foo/bar/baz".pathStartsWith("/foo/bar"));
93 	assert(!"/foo/barbara".pathStartsWith("/foo/bar"));
94 	assert( "/foo/bar"    .pathStartsWith(""));
95 	assert( "/foo/bar"    .pathStartsWith("."));
96 }
97 
98 // ************************************************************************
99 
100 import std.process : environment;
101 import std.string : split;
102 
103 @property string[] pathDirs()
104 {
105 	return environment["PATH"].split(pathSeparator);
106 }
107 
108 bool haveExecutable(string name)
109 {
110 	return findExecutable(name, pathDirs) !is null;
111 }
112 
113 /// Find an executable with the given name
114 /// (no extension) in the given directories.
115 /// Returns null if not found.
116 string findExecutable(string name, string[] dirs)
117 {
118 	import std.file : exists;
119 
120 	version (Windows)
121 		enum executableSuffixes = [".exe", ".bat", ".cmd"];
122 	else
123 		enum executableSuffixes = [""];
124 
125 	foreach (dir; dirs)
126 		foreach (suffix; executableSuffixes)
127 		{
128 			version (Posix)
129 				if (dir == "")
130 					dir = ".";
131 			auto fn = buildPath(dir, name) ~ suffix;
132 			if (fn.exists)
133 				return fn;
134 		}
135 
136 	return null;
137 }
138 
139 // ************************************************************************
140 
141 /**
142    Find a program's "home" directory, based on the presence of a file.
143 
144    This can be a directory containing files that are included with or
145    created by the program, and should be accessible no matter how the
146    program is built/invoked of which current directory it is invoked
147    from.
148 
149    Use a set of individually-unreliable methods to find the path. This
150    is necessary, because:
151 
152    - __FILE__ by itself is insufficient, because it may not be an
153      absolute path, and the compiled binary may have been moved after
154      being built;
155 
156    - The executable's directory by itself is insufficient, because
157      build tools such as rdmd can place it in a temporary directory;
158 
159    - The current directory by itself is insufficient, because the
160      program can be launched in more than one way, e.g.:
161 
162      - Running the program from the same directory as the source file
163        containing main() (e.g. rdmd program.d)
164 
165      - Running the program from the upper directory containing all
166        packages and dependencies, so that there is no need to specify
167        include dirs (e.g. rdmd ae/demo/http/httpserve.d)
168 
169      - Running the program from a cronjob or another location, in
170        which the current directory can be unrelated altogether.
171 
172     Params:
173       testFile = Relative path to a file or directory, the presence of
174                  which indicates that the "current" directory (base of
175                  the relative path) is the sought-after program root
176                  directory.
177       sourceFile = Path to a source file part of the program's code
178                    base. Defaults to the __FILE__ of the caller.
179 
180     Returns:
181       Path to the sought root directory, or `null` if one was not found.
182 */
183 string findProgramDirectory(string testFile, string sourceFile = __FILE__)
184 {
185 	import std.file : thisExePath, getcwd, exists;
186 	import core.runtime : Runtime;
187 	import std.range : only;
188 
189 	foreach (path; only(Runtime.args[0].absolutePath().dirName(), thisExePath.dirName, sourceFile.dirName, null))
190 	{
191 		path = path.absolutePath().buildNormalizedPath();
192 		while (true)
193 		{
194 			auto indicator = path.buildPath(testFile);
195 			if (indicator.exists)
196 				return path;
197 			auto parent = dirName(path);
198 			if (parent == path)
199 				break;
200 			path = parent;
201 		}
202 	}
203 	return null;
204 }
205 
206 // ************************************************************************
207 
208 /// The file name for the null device
209 /// (which discards all writes).
210 version (Windows)
211 	enum nullFileName = "nul";
212 else
213 	enum nullFileName = "/dev/null";