1 /++
2 $(H2 Scriptlike $(SCRIPTLIKE_VERSION))
3 
4 Extra Scriptlike-only functionality to complement $(MODULE_STD_PATH).
5 
6 Copyright: Copyright (C) 2014-2015 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, osDefaultCaseSensitivity, 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 = null) 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 	string toRawString() pure @safe nothrow
49 	{
50 		return str;
51 	}
52 	
53 	/// Compare using OS-specific case-sensitivity rules. If you want to force
54 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
55 	int opCmp(ref const Ext other) const
56 	{
57 		return std.path.filenameCmp(this.str, other.str);
58 	}
59 
60 	///ditto
61 	int opCmp(Ext other) const
62 	{
63 		return std.path.filenameCmp(this.str, other.str);
64 	}
65 
66 	///ditto
67 	int opCmp(string other) const
68 	{
69 		return std.path.filenameCmp(this.str, other);
70 	}
71 
72 	/// Compare using OS-specific case-sensitivity rules. If you want to force
73 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
74 	int opEquals(ref const Ext other) const
75 	{
76 		return opCmp(other) == 0;
77 	}
78 
79 	///ditto
80 	int opEquals(Ext other) const
81 	{
82 		return opCmp(other) == 0;
83 	}
84 
85 	///ditto
86 	int opEquals(string other) const
87 	{
88 		return opCmp(other) == 0;
89 	}
90 
91 	/// Convert to bool
92 	T opCast(T)() if(is(T==bool))
93 	{
94 		return !!str;
95 	}
96 }
97 
98 /// Represents a filesystem path. The path is always kept normalized
99 /// automatically (as performed by buildNormalizedPathFixed).
100 struct Path
101 {
102 	private string str = ".";
103 	
104 	/// Main constructor.
105 	this(string path = ".") pure @safe nothrow
106 	{
107 		this.str = buildNormalizedPathFixed(path);
108 	}
109 	
110 	pure @trusted nothrow invariant()
111 	{
112 		assert(str == buildNormalizedPathFixed(str));
113 	}
114 	
115 	/// Convert to string, quoting or escaping spaces if necessary.
116 	string toString()
117 	{
118 		return escapeShellArg(str);
119 	}
120 	
121 	/// Returns the underlying string. Does NOT do any escaping, even if path contains spaces.
122 	string toRawString() const pure @safe nothrow
123 	{
124 		return str;
125 	}
126 
127 	/// Concatenates two paths, with a directory separator in between.
128 	Path opBinary(string op)(Path rhs) if(op=="~")
129 	{
130 		Path newPath;
131 		newPath.str = buildNormalizedPathFixed(this.str, rhs.str);
132 		return newPath;
133 	}
134 	
135 	///ditto
136 	Path opBinary(string op)(string rhs) if(op=="~")
137 	{
138 		Path newPath;
139 		newPath.str = buildNormalizedPathFixed(this.str, rhs);
140 		return newPath;
141 	}
142 	
143 	///ditto
144 	Path opBinaryRight(string op)(string lhs) if(op=="~")
145 	{
146 		Path newPath;
147 		newPath.str = buildNormalizedPathFixed(lhs, this.str);
148 		return newPath;
149 	}
150 	
151 	/// Appends an extension to a path. Naturally, a directory separator
152 	/// is NOT inserted in between.
153 	Path opBinary(string op)(Ext rhs) if(op=="~")
154 	{
155 		Path newPath;
156 		newPath.str = std.path.setExtension(this.str, rhs.str);
157 		return newPath;
158 	}
159 	
160 	/// Appends a path to this one, with a directory separator in between.
161 	Path opOpAssign(string op)(Path rhs) if(op=="~")
162 	{
163 		str = buildNormalizedPathFixed(str, rhs.str);
164 		return this;
165 	}
166 	
167 	///ditto
168 	Path opOpAssign(string op)(string rhs) if(op=="~")
169 	{
170 		str = buildNormalizedPathFixed(str, rhs);
171 		return this;
172 	}
173 	
174 	/// Appends an extension to this path. Naturally, a directory separator
175 	/// is NOT inserted in between.
176 	Path opOpAssign(string op)(Ext rhs) if(op=="~")
177 	{
178 		str = std.path.setExtension(str, rhs.str);
179 		return this;
180 	}
181 	
182 	/// Compare using OS-specific case-sensitivity rules. If you want to force
183 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
184 	int opCmp(ref const Path other) const
185 	{
186 		return std.path.filenameCmp(this.str, other.str);
187 	}
188 
189 	///ditto
190 	int opCmp(Path other) const
191 	{
192 		return std.path.filenameCmp(this.str, other.str);
193 	}
194 
195 	///ditto
196 	int opCmp(string other) const
197 	{
198 		return std.path.filenameCmp(this.str, other);
199 	}
200 
201 	/// Compare using OS-specific case-sensitivity rules. If you want to force
202 	/// case-sensitive or case-insensitive, then call filenameCmp instead.
203 	int opEquals(ref const Path other) const
204 	{
205 		return opCmp(other) == 0;
206 	}
207 
208 	///ditto
209 	int opEquals(Path other) const
210 	{
211 		return opCmp(other) == 0;
212 	}
213 
214 	///ditto
215 	int opEquals(string other) const
216 	{
217 		return opCmp(other) == 0;
218 	}
219 	
220 	/// Convert to bool
221 	T opCast(T)() if(is(T==bool))
222 	{
223 		return !!str;
224 	}
225 	
226 	/// Returns the parent path, according to $(FULL_STD_PATH dirName).
227 	@property Path up()
228 	{
229 		return this.dirName();
230 	}
231 	
232 	/// Is this path equal to empty string?
233 	@property bool empty()
234 	{
235 		return str == "";
236 	}
237 }
238 
239 /// Convenience alias
240 alias extOf      = extension;
241 alias stripExt   = stripExtension;   ///ditto
242 alias setExt     = setExtension;     ///ditto
243 alias defaultExt = defaultExtension; ///ditto
244 
245 /// Like buildNormalizedPath, but if the result is the current directory,
246 /// this returns "." instead of "". However, if all the inputs are "", or there
247 /// are no inputs, this still returns "" just like buildNormalizedPath.
248 string buildNormalizedPathFixed(string[] paths...)
249 	@trusted pure nothrow
250 {
251 	if(all!`a is null`(paths))
252 		return null;
253 	
254 	if(all!`a==""`(paths))
255 		return "";
256 	
257 	auto result = std.path.buildNormalizedPath(paths);
258 	return result==""? "." : result;
259 }
260 
261 /// Properly escape arguments containing spaces for the command shell, if necessary.
262 const(string) escapeShellArg(in string str)
263 {
264 	if(str.canFind(' '))
265 	{
266 		version(Windows)
267 			return escapeWindowsArgument(str);
268 		else version(Posix)
269 			return escapeShellFileName(str);
270 		else
271 			static assert(0, "This platform not supported.");
272 	}
273 	else
274 		return str;
275 }