1 /*
2                                     __
3                                    / _|
4   __ _ _   _ _ __ ___  _ __ __ _  | |_ ___  ___ ___
5  / _` | | | | '__/ _ \| '__/ _` | |  _/ _ \/ __/ __|
6 | (_| | |_| | | | (_) | | | (_| | | || (_) \__ \__ \
7  \__,_|\__,_|_|  \___/|_|  \__,_| |_| \___/|___/___/
8 
9 Copyright (C) 2018-2019 Aurora Free Open Source Software.
10 Copyright (C) 2018-2019 Luís Ferreira <luis@aurorafoss.org>
11 
12 This file is part of the Aurora Free Open Source Software. This
13 organization promote free and open source software that you can
14 redistribute and/or modify under the terms of the GNU Lesser General
15 Public License Version 3 as published by the Free Software Foundation or
16 (at your option) any later version approved by the Aurora Free Open Source
17 Software Organization. The license is available in the package root path
18 as 'LICENSE' file. Please review the following information to ensure the
19 GNU Lesser General Public License version 3 requirements will be met:
20 https://www.gnu.org/licenses/lgpl.html .
21 
22 Alternatively, this file may be used under the terms of the GNU General
23 Public License version 3 or later as published by the Free Software
24 Foundation. Please review the following information to ensure the GNU
25 General Public License requirements will be met:
26 https://www.gnu.org/licenses/gpl-3.0.html.
27 
28 NOTE: All products, services or anything associated to trademarks and
29 service marks used or referenced on this file are the property of their
30 respective companies/owners or its subsidiaries. Other names and brands
31 may be claimed as the property of others.
32 
33 For more info about intellectual property visit: aurorafoss.org or
34 directly send an email to: contact (at) aurorafoss.org .
35 */
36 
37 module aurorafw.core.opt;
38 
39 public import aurorafw.stdx.getopt : defaultRuntimeArgs;
40 import std.algorithm : startsWith, canFind, sort, any;
41 import aurorafw.stdx..string : isAlpha;
42 import std.array : split, array;
43 import std.range.primitives : empty;
44 import std.exception;
45 import std..string : indexOf;
46 import std.format : format;
47 import std.variant;
48 import core.runtime : Runtime;
49 import std.traits : fullyQualifiedName;
50 import std.typecons;
51 
52 version (unittest) import aurorafw.unit.assertion;
53 
54 @safe pure
55 class OptionHandlerException : Exception
56 {
57 	mixin basicExceptionCtors;
58 }
59 
60 static assert(is(typeof(new OptionHandlerException("message"))));
61 static assert(is(typeof(new OptionHandlerException("message", Exception.init))));
62 
63 @safe pure
64 struct OptionHandler
65 {
66 	struct Option
67 	{
68 		string[] opts;
69 		string help;
70 	}
71 
72 	struct Argument
73 	{
74 		string name;
75 		ArgumentSize size;
76 		string value = null;
77 	}
78 
79 	enum ArgumentSize
80 	{
81 		Long,
82 		Short
83 	}
84 
85 	@safe pure
86 	public this(string[] args)
87 	{
88 		foreach (string arg; args[1 .. $])
89 		{
90 			if (arg.startsWith("--"))
91 			{
92 				string splitted = arg.split("--")[1];
93 				if (splitted.length == 0)
94 					break;
95 
96 				ptrdiff_t hasValue = splitted.indexOf('=');
97 				if (hasValue != -1)
98 				{
99 					string newSplitted = splitted[0 .. hasValue];
100 					if (newSplitted.isAlpha)
101 						this.args ~= Argument(newSplitted, ArgumentSize.Long, splitted[hasValue + 1 .. $]);
102 				}
103 				else if (splitted.isAlpha)
104 					this.args ~= Argument(splitted, ArgumentSize.Long);
105 			}
106 			else if (arg.startsWith("-"))
107 			{
108 				string splitted = arg.split("-")[1];
109 				ptrdiff_t hasValue = splitted.indexOf('=');
110 				if (hasValue != -1)
111 				{
112 					string newSplitted = splitted[0 .. hasValue];
113 					if (newSplitted.isAlpha)
114 						this.args ~= Argument(newSplitted, ArgumentSize.Short, splitted[hasValue + 1 .. $]);
115 				}
116 				else if (splitted.isAlpha && splitted.length != 0)
117 					this.args ~= Argument(splitted, ArgumentSize.Short);
118 			}
119 		}
120 	}
121 
122 	@safe pure
123 	public T[] read(T)(string opts, string help, bool required = false)
124 	{
125 		read(opts, help);
126 
127 		T[] ret;
128 		bool value = false;
129 		foreach (arg; args)
130 			if (opts.canFind(arg.name))
131 			{
132 				value = true;
133 				string retstr = arg.value;
134 				if (retstr !is null)
135 				{
136 					import std.conv : to;
137 
138 					ret ~= to!T(retstr);
139 				}
140 			}
141 
142 		if (required && (!value || (value && ret.empty)))
143 			throw new OptionHandlerException("Required option %s with %s type".format(opts, fullyQualifiedName!T));
144 
145 		return ret;
146 	}
147 
148 	@safe pure
149 	public Nullable!Argument read(string opts, string help, ref bool value, bool required = false)
150 	{
151 		Option opte = read(opts, help);
152 		value = false;
153 		foreach (opt; opte.opts)
154 		{
155 			foreach (arg; args)
156 				if (arg.name == opt)
157 				{
158 					value = true;
159 					return arg.nullable;
160 				}
161 		}
162 		if (value == false && required == true)
163 			throw new OptionHandlerException("Required option " ~ opts);
164 
165 		return Nullable!Argument();
166 	}
167 
168 	@safe pure
169 	private Option read(string opts, string help)
170 	{
171 		Option opte;
172 		opte.opts = opts.split("|").sort!((a, b) => a.length < b.length).array;
173 		opte.help = help;
174 
175 		if (this.opts.any!(o => o.opts == opte.opts))
176 			throw new OptionHandlerException("Trying to read the same option name twice");
177 
178 		this.opts ~= opte;
179 
180 		return opte;
181 	}
182 
183 	@safe pure nothrow
184 	public bool helpWanted()
185 	{
186 		foreach (arg; args)
187 		{
188 			if (arg.name == "help")
189 				return true;
190 		}
191 		return false;
192 	}
193 
194 	@safe pure
195 	public string printableHelp(string programName)
196 	{
197 		string ret;
198 
199 		ret ~= "Usage:\n\t%s -- <options>\n\nOptions:\n".format(programName);
200 
201 		foreach (opt; opts)
202 		{
203 			ret ~= opt.opts[0];
204 			if (opt.opts.length == 2)
205 			{
206 				ret ~= " " ~ opt.opts[1];
207 			}
208 			ret ~= "\t" ~ opt.help ~ "\n";
209 		}
210 		return ret;
211 	}
212 
213 	@safe pure
214 	@property public Argument[] arguments()
215 	{
216 		return this.args.dup;
217 	}
218 
219 	@property @safe pure
220 	public Option[] options()
221 	{
222 		return this.opts.dup;
223 	}
224 
225 	private Argument[] args;
226 	private Option[] opts;
227 }
228 
229 @safe pure
230 @("Option Handler: help")
231 unittest
232 {
233 	bool dummy;
234 
235 	{
236 		OptionHandler optHandler = OptionHandler(["prog", "--help"]);
237 		optHandler.read("dummy", "some", dummy);
238 		optHandler.read("duuu|d", "none", dummy);
239 
240 		auto pHelp = "Usage:\n\tprogram -- <options>\n\nOptions:\n";
241 		pHelp ~= "dummy\tsome\n";
242 		pHelp ~= "d duuu\tnone\n";
243 
244 		assertEquals(optHandler.printableHelp("program"), pHelp);
245 		assertTrue(optHandler.helpWanted);
246 
247 		optHandler = OptionHandler(["prog"]);
248 		assertFalse(optHandler.helpWanted);
249 	}
250 }
251 
252 @safe pure
253 @("Option Handler: check arguments and options")
254 unittest
255 {
256 	OptionHandler opts = OptionHandler(["prog", "--foo", "-f", "--bar=foobar", "--", "tunaisgood"]);
257 
258 	OptionHandler.Argument[] args = [
259 		OptionHandler.Argument("foo", OptionHandler.ArgumentSize.Long),
260 		OptionHandler.Argument("f", OptionHandler.ArgumentSize.Short),
261 		OptionHandler.Argument("bar", OptionHandler.ArgumentSize.Long, "foobar")
262 	];
263 
264 	assertEquals(args, opts.arguments);
265 	assertEquals([], opts.options);
266 }
267 
268 @safe pure
269 @("Option Handler: check read")
270 unittest
271 {
272 	bool isFoo, isFoobar;
273 
274 	{
275 		OptionHandler optHandler = OptionHandler(["prog", "--foo", "-f"]);
276 
277 		optHandler.read("foo", "information", isFoo, true);
278 		optHandler.read("foobar|f", "quick", isFoobar);
279 
280 		auto opts = [
281 			OptionHandler.Option(["foo"], "information"),
282 			OptionHandler.Option(["f", "foobar"], "quick")
283 		];
284 
285 		assertEquals(opts, optHandler.options);
286 
287 		assertThrown(optHandler.read!int("tuna", "tunaisgood", true));
288 	}
289 
290 	assertTrue(isFoo);
291 	assertTrue(isFoobar);
292 }
293 
294 @safe pure
295 @("Option Handler: check values")
296 unittest
297 {
298 	int[] foo;
299 
300 	{
301 		OptionHandler optHandler = OptionHandler(["prog", "--foo=4", "-f=7"]);
302 
303 		foo = optHandler.read!int("foo|f", "information", true);
304 	}
305 
306 	assertFalse(foo.empty);
307 	assertEquals(4, foo[0]);
308 	assertEquals(7, foo[1]);
309 }
310 
311 @safe pure
312 @("Option Handler: required exception")
313 unittest
314 {
315 	bool isFoo, isBar;
316 
317 	{
318 		OptionHandler optHandler = OptionHandler(["prog"]);
319 		optHandler.read("bar", "information", isBar);
320 
321 		assertThrown!OptionHandlerException(optHandler.read("foo", "more information", isFoo, true));
322 	}
323 }
324 
325 @safe pure
326 @("Option Handler: twice")
327 unittest
328 {
329 	bool dummy;
330 
331 	{
332 		OptionHandler optHandler = OptionHandler(["prog"]);
333 		optHandler.read("dummy", "some", dummy);
334 
335 		assertThrown!OptionHandlerException(optHandler.read("dummy", "same", dummy));
336 	}
337 }