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 			auto fn = buildPath(dir, name) ~ suffix;
125 			if (fn.exists)
126 				return fn;
127 		}
128 
129 	return null;
130 }
131 
132 // ************************************************************************
133 
134 /**
135    Find a program's "home" directory, based on the presence of a file.
136 
137    This can be a directory containing files that are included with or
138    created by the program, and should be accessible no matter how the
139    program is built/invoked of which current directory it is invoked
140    from.
141 
142    Use a set of individually-unreliable methods to find the path. This
143    is necessary, because:
144 
145    - __FILE__ by itself is insufficient, because it may not be an
146      absolute path, and the compiled binary may have been moved after
147      being built;
148 
149    - The executable's directory by itself is insufficient, because
150      build tools such as rdmd can place it in a temporary directory;
151 
152    - The current directory by itself is insufficient, because the
153      program can be launched in more than one way, e.g.:
154 
155      - Running the program from the same directory as the source file
156        containing main() (e.g. rdmd program.d)
157 
158      - Running the program from the upper directory containing all
159        packages and dependencies, so that there is no need to specify
160        include dirs (e.g. rdmd ae/demo/http/httpserve.d)
161 
162      - Running the program from a cronjob or another location, in
163        which the current directory can be unrelated altogether.
164 
165     Params:
166       testFile = Relative path to a file or directory, the presence of
167                  which indicates that the "current" directory (base of
168                  the relative path) is the sought-after program root
169                  directory.
170       sourceFile = Path to a source file part of the program's code
171                    base. Defaults to the __FILE__ of the caller.
172 
173     Returns:
174       Path to the sought root directory, or `null` if one was not found.
175 */
176 string findProgramDirectory(string testFile, string sourceFile = __FILE__)
177 {
178 	import std.file : thisExePath, getcwd, exists;
179 	import core.runtime : Runtime;
180 	import std.range : only;
181 
182 	foreach (path; only(Runtime.args[0].absolutePath().dirName(), thisExePath.dirName, sourceFile.dirName, null))
183 	{
184 		path = path.absolutePath().buildNormalizedPath();
185 		while (true)
186 		{
187 			auto indicator = path.buildPath(testFile);
188 			if (indicator.exists)
189 				return path;
190 			auto parent = dirName(path);
191 			if (parent == path)
192 				break;
193 			path = parent;
194 		}
195 	}
196 	return null;
197 }
198 
199 // ************************************************************************
200 
201 /// The file name for the null device
202 /// (which discards all writes).
203 version (Windows)
204 	enum nullFileName = "nul";
205 else
206 	enum nullFileName = "/dev/null";