1 /++
2 $(H2 Scriptlike $(SCRIPTLIKE_VERSION))
3 
4 Extra Scriptlike-only functionality to complement $(MODULE_STD_PATH).
5 
6 Copyright: Copyright (C) 2014-2017 Nick Sabalausky
7 License:   zlib/libpng
8 Authors:   Nick Sabalausky
9 +/
10 module scriptlike.path.extras;
11 
12 import std.algorithm;
13 import std.conv;
14 import std.datetime;
15 import std.file;
16 import std.process;
17 import std.range;
18 import std.stdio;
19 import std..string;
20 import std.traits;
21 import std.typecons;
22 import std.typetuple;
23 
24 static import std.path;
25 import std.path : dirSeparator, pathSeparator, isDirSeparator,
26 	CaseSensitive, buildPath, buildNormalizedPath;
27 
28 import scriptlike.path.wrappers;
29 
30 /// Represents a file extension.
31 struct Ext
32 {
33 	private string str;
34 	
35 	/// Main constructor.
36 	this(string extension) pure @safe nothrow
37 	{
38 		this.str = extension;
39 	}
40 	
41 	/// Convert to string.
42 	string toString() pure @safe nothrow
43 	{
44 		return str;
45 	}
46 	
47 	/// No longer needed. Use Ext.toString() instead.
48 	deprecated("Use Ext.toString() instead.")
49 	string toRawString() pure @safe nothrow
50 	{
51 		return str;
52 	}
53 	
54 	/// Compare using OS-specific case-sensitivity rules. If you want to force
55 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
56 	int opCmp(ref const Ext other) const
57 	{
58 		return std.path.filenameCmp(this.str, other.str);
59 	}
60 
61 	///ditto
62 	int opCmp(Ext other) const
63 	{
64 		return std.path.filenameCmp(this.str, other.str);
65 	}
66 
67 	///ditto
68 	int opCmp(string other) const
69 	{
70 		return std.path.filenameCmp(this.str, other);
71 	}
72 
73 	/// Compare using OS-specific case-sensitivity rules. If you want to force
74 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
75 	int opEquals(ref const Ext other) const
76 	{
77 		return opCmp(other) == 0;
78 	}
79 
80 	///ditto
81 	int opEquals(Ext other) const
82 	{
83 		return opCmp(other) == 0;
84 	}
85 
86 	///ditto
87 	int opEquals(string other) const
88 	{
89 		return opCmp(other) == 0;
90 	}
91 
92 	/// Convert to bool
93 	T opCast(T)() if(is(T==bool))
94 	{
95 		return !!str;
96 	}
97 }
98 
99 /// Represents a filesystem path. The path is always kept normalized
100 /// automatically (as performed by buildNormalizedPathFixed).
101 struct Path
102 {
103 	private string str = ".";
104 	
105 	/// Main constructor.
106 	this(string path) pure @safe nothrow
107 	{
108 		this.str = buildNormalizedPathFixed(path);
109 	}
110 	
111 	pure @trusted nothrow invariant()
112 	{
113 		assert(str == buildNormalizedPathFixed(str));
114 	}
115 	
116 	/// Convert to string, quoting or escaping spaces if necessary.
117 	string toString()
118 	{
119 		return .escapeShellArg(str);
120 	}
121 	
122 	/// Returns the underlying string. Does NOT do any escaping, even if path contains spaces.
123 	string raw() const pure @safe nothrow
124 	{
125 		return str;
126 	}
127 
128 	///ditto
129 	deprecated("Use Path.raw instead.")
130 	alias toRawString = raw;
131 
132 	/// Concatenates two paths, with a directory separator in between.
133 	Path opBinary(string op)(Path rhs) if(op=="~")
134 	{
135 		Path newPath;
136 		newPath.str = buildNormalizedPathFixed(this.str, rhs.str);
137 		return newPath;
138 	}
139 	
140 	///ditto
141 	Path opBinary(string op)(string rhs) if(op=="~")
142 	{
143 		Path newPath;
144 		newPath.str = buildNormalizedPathFixed(this.str, rhs);
145 		return newPath;
146 	}
147 	
148 	///ditto
149 	Path opBinaryRight(string op)(string lhs) if(op=="~")
150 	{
151 		Path newPath;
152 		newPath.str = buildNormalizedPathFixed(lhs, this.str);
153 		return newPath;
154 	}
155 	
156 	/// Appends an extension to a path. Naturally, a directory separator
157 	/// is NOT inserted in between.
158 	Path opBinary(string op)(Ext rhs) if(op=="~")
159 	{
160 		Path newPath;
161 		newPath.str = std.path.setExtension(this.str, rhs.str);
162 		return newPath;
163 	}
164 	
165 	/// Appends a path to this one, with a directory separator in between.
166 	Path opOpAssign(string op)(Path rhs) if(op=="~")
167 	{
168 		str = buildNormalizedPathFixed(str, rhs.str);
169 		return this;
170 	}
171 	
172 	///ditto
173 	Path opOpAssign(string op)(string rhs) if(op=="~")
174 	{
175 		str = buildNormalizedPathFixed(str, rhs);
176 		return this;
177 	}
178 	
179 	/// Appends an extension to this path. Naturally, a directory separator
180 	/// is NOT inserted in between.
181 	Path opOpAssign(string op)(Ext rhs) if(op=="~")
182 	{
183 		str = std.path.setExtension(str, rhs.str);
184 		return this;
185 	}
186 	
187 	/// Compare using OS-specific case-sensitivity rules. If you want to force
188 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
189 	int opCmp(ref const Path other) const
190 	{
191 		return std.path.filenameCmp(this.str, other.str);
192 	}
193 
194 	///ditto
195 	int opCmp(Path other) const
196 	{
197 		return std.path.filenameCmp(this.str, other.str);
198 	}
199 
200 	///ditto
201 	int opCmp(string other) const
202 	{
203 		return std.path.filenameCmp(this.str, other);
204 	}
205 
206 	/// Compare using OS-specific case-sensitivity rules. If you want to force
207 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
208 	int opEquals(ref const Path other) const
209 	{
210 		return opCmp(other) == 0;
211 	}
212 
213 	///ditto
214 	int opEquals(Path other) const
215 	{
216 		return opCmp(other) == 0;
217 	}
218 
219 	///ditto
220 	int opEquals(string other) const
221 	{
222 		return opCmp(other) == 0;
223 	}
224 	
225 	/// Convert to bool
226 	T opCast(T)() if(is(T==bool))
227 	{
228 		return !!str;
229 	}
230 	
231 	/// Returns the parent path, according to $(FULL_STD_PATH dirName).
232 	@property Path up()
233 	{
234 		return this.dirName();
235 	}
236 	
237 	/// Is this path equal to empty string?
238 	@property bool empty()
239 	{
240 		return str == "";
241 	}
242 }
243 
244 /// Convenience alias
245 alias extOf      = extension;
246 alias stripExt   = stripExtension;   ///ditto
247 alias setExt     = setExtension;     ///ditto
248 alias defaultExt = defaultExtension; ///ditto
249 
250 /// Like buildNormalizedPath, but if the result is the current directory,
251 /// this returns "." instead of "". However, if all the inputs are "", or there
252 /// are no inputs, this still returns "" just like buildNormalizedPath.
253 ///
254 /// Also, unlike buildNormalizedPath, this converts back/forward slashes to
255 /// native on BOTH Windows and Posix, not just on Windows.
256 string buildNormalizedPathFixed(string[] paths...)
257 	@trusted pure nothrow
258 {
259 	if(all!`a is null`(paths))
260 		return null;
261 	
262 	if(all!`a==""`(paths))
263 		return "";
264 	
265 	auto result = std.path.buildNormalizedPath(paths);
266 
267 	version(Posix)        result = result.replace(`\`, `/`);
268 	else version(Windows) { /+ do nothing +/ }
269 	else                  static assert(0);
270 
271 	return result==""? "." : result;
272 }
273 
274 /// Properly escape arguments containing spaces for the command shell, if necessary.
275 ///
276 /// Although Path doesn't stricktly need this (since Path.toString automatically
277 /// calls this anyway), an overload of escapeShellArg which accepts a Path is
278 /// provided for the sake of generic code.
279 const(string) escapeShellArg(in string str)
280 {
281 	if(str.canFind(' '))
282 	{
283 		version(Windows)
284 			return escapeWindowsArgument(str);
285 		else version(Posix)
286 			return escapeShellFileName(str);
287 		else
288 			static assert(0, "This platform not supported.");
289 	}
290 	else
291 		return str;
292 }
293 
294 ///ditto
295 string escapeShellArg(Path path)
296 {
297 	return path.toString();
298 }
299