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