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