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 version(unittest) import aurorafw.unit.assertion;
52 
53 @safe pure
54 class OptionHandlerException : Exception
55 {
56 	mixin basicExceptionCtors;
57 }
58 
59 static assert(is(typeof(new OptionHandlerException("message"))));
60 static assert(is(typeof(new OptionHandlerException("message", Exception.init))));
61 
62 @safe pure
63 struct OptionHandler {
64 	struct Option {
65 		string[] opts;
66 		string help;
67 	}
68 
69 	struct Argument {
70 		string name;
71 		ArgumentSize size;
72 		string value = null;
73 	}
74 
75 	enum ArgumentSize {
76 		Long,
77 		Short
78 	}
79 
80 	@safe pure
81 	public this(string[] args)
82 	{
83 		foreach(string arg ; args[1 .. $])
84 		{
85 			if(arg.startsWith("--"))
86 			{
87 				string splitted = arg.split("--")[1];
88 				if(splitted.length == 0)
89 					break;
90 
91 				ptrdiff_t hasValue = splitted.indexOf('=');
92 				if(hasValue != -1)
93 				{
94 					string newSplitted = splitted[0 .. hasValue];
95 					if(newSplitted.isAlpha)
96 						this.args ~= Argument(newSplitted, ArgumentSize.Long, splitted[hasValue + 1 .. $]);
97 				}
98 				else if(splitted.isAlpha)
99 					this.args ~= Argument(splitted, ArgumentSize.Long);
100 			}
101 			else if (arg.startsWith("-"))
102 			{
103 				string splitted = arg.split("-")[1];
104 				ptrdiff_t hasValue = splitted.indexOf('=');
105 				if(hasValue != -1)
106 				{
107 					string newSplitted = splitted[0 .. hasValue];
108 					if(newSplitted.isAlpha)
109 						this.args ~= Argument(newSplitted, ArgumentSize.Short, splitted[hasValue + 1 .. $]);
110 				}
111 				else if(splitted.isAlpha && splitted.length != 0)
112 					this.args ~= Argument(splitted, ArgumentSize.Short);
113 			}
114 		}
115 	}
116 
117 	@safe pure
118 	public T[] read(T)(string opts, string help, bool required = false)
119 	{
120 		read(opts, help);
121 
122 		T[] ret;
123 		bool value = false;
124 		foreach(arg; args) if(opts.canFind(arg.name))
125 		{
126 			value = true;
127 			string retstr = arg.value;
128 			if(retstr !is null)
129 			{
130 				import std.conv : to;
131 				ret ~= to!T(retstr);
132 			}
133 		}
134 
135 		if(required && (!value || (value && ret.empty)))
136 			throw new OptionHandlerException("Required option %s with %s type".format(opts, fullyQualifiedName!T));
137 
138 		return ret;
139 	}
140 
141 	@safe pure
142 	public Nullable!Argument read(string opts, string help, ref bool value, bool required = false)
143 	{
144 		Option opte = read(opts, help);
145 		value = false;
146 		foreach(opt; opte.opts)
147 		{
148 			foreach(arg; args) if(arg.name == opt)
149 			{
150 				value = true;
151 					return arg.nullable;
152 			}
153 		}
154 		if(value == false && required == true)
155 			throw new OptionHandlerException("Required option " ~ opts);
156 
157 		return Nullable!Argument();
158 	}
159 
160 	@safe pure
161 	private Option read(string opts, string help)
162 	{
163 		Option opte;
164 		opte.opts = opts.split("|").sort!((a, b) => a.length < b.length).array;
165 		opte.help = help;
166 
167 		if(this.opts.any!(o => o.opts == opte.opts))
168 			throw new OptionHandlerException("Trying to read the same option name twice");
169 
170 		this.opts ~= opte;
171 
172 		return opte;
173 	}
174 
175 	@safe pure nothrow
176 	public bool helpWanted()
177 	{
178 		foreach(arg; args)
179 		{
180 			if(arg.name == "help")
181 				return true;
182 		}
183 		return false;
184 	}
185 
186 	@safe pure
187 	public string printableHelp(string programName)
188 	{
189 		string ret;
190 
191 		ret ~= "Usage:\n\t%s -- <options>\n\nOptions:\n".format(programName);
192 
193 		foreach(opt; opts)
194 		{
195 			ret ~= opt.opts[0];
196 			if(opt.opts.length == 2)
197 			{
198 				ret ~= " " ~ opt.opts[1];
199 			}
200 			ret ~= "\t" ~ opt.help ~ "\n";
201 		}
202 		return ret;
203 	}
204 
205 	@safe pure
206 	@property public Argument[] arguments()
207 	{
208 		return this.args.dup;
209 	}
210 
211 	@property @safe pure
212 	public Option[] options()
213 	{
214 		return this.opts.dup;
215 	}
216 
217 	private Argument[] args;
218 	private Option[] opts;
219 }
220 
221 @safe pure
222 @("Option Handler: help")
223 unittest
224 {
225 	bool dummy;
226 
227 	{
228 		OptionHandler optHandler = OptionHandler(["prog", "--help"]);
229 		optHandler.read("dummy", "some", dummy);
230 		optHandler.read("duuu|d", "none", dummy);
231 
232 		auto pHelp = "Usage:\n\tprogram -- <options>\n\nOptions:\n";
233 		pHelp ~= "dummy\tsome\n";
234 		pHelp ~= "d duuu\tnone\n";
235 
236 		assertEquals(optHandler.printableHelp("program"), pHelp);
237 		assertTrue(optHandler.helpWanted);
238 
239 		optHandler = OptionHandler(["prog"]);
240 		assertFalse(optHandler.helpWanted);
241 	}
242 }
243 
244 @safe pure
245 @("Option Handler: check arguments and options")
246 unittest
247 {
248 	OptionHandler opts = OptionHandler(["prog", "--foo", "-f", "--bar=foobar", "--", "tunaisgood"]);
249 
250 	OptionHandler.Argument[] args = [
251 		OptionHandler.Argument("foo", OptionHandler.ArgumentSize.Long),
252 		OptionHandler.Argument("f", OptionHandler.ArgumentSize.Short),
253 		OptionHandler.Argument("bar", OptionHandler.ArgumentSize.Long, "foobar")
254 	];
255 
256 	assertEquals(args, opts.arguments);
257 	assertEquals([], opts.options);
258 }
259 
260 
261 @safe pure
262 @("Option Handler: check read")
263 unittest
264 {
265 	bool isFoo, isFoobar;
266 
267 	{
268 		OptionHandler optHandler = OptionHandler(["prog", "--foo", "-f"]);
269 
270 		optHandler.read("foo", "information", isFoo, true);
271 		optHandler.read("foobar|f", "quick", isFoobar);
272 
273 		auto opts = [
274 			OptionHandler.Option(["foo"], "information"),
275 			OptionHandler.Option(["f", "foobar"], "quick")
276 		];
277 
278 		assertEquals(opts, optHandler.options);
279 
280 		assertThrown(optHandler.read!int("tuna", "tunaisgood", true));
281 	}
282 
283 	assertTrue(isFoo);
284 	assertTrue(isFoobar);
285 }
286 
287 
288 @safe pure
289 @("Option Handler: check values")
290 unittest
291 {
292 	int[] foo;
293 
294 	{
295 		OptionHandler optHandler = OptionHandler(["prog", "--foo=4", "-f=7"]);
296 
297 		foo = optHandler.read!int("foo|f", "information", true);
298 	}
299 
300 	assertFalse(foo.empty);
301 	assertEquals(4, foo[0]);
302 	assertEquals(7, foo[1]);
303 }
304 
305 
306 @safe pure
307 @("Option Handler: required exception")
308 unittest
309 {
310 	bool isFoo, isBar;
311 
312 	{
313 		OptionHandler optHandler = OptionHandler(["prog"]);
314 		optHandler.read("bar", "information", isBar);
315 
316 		assertThrown!OptionHandlerException(optHandler.read("foo", "more information", isFoo, true));
317 	}
318 }
319 
320 @safe pure
321 @("Option Handler: twice")
322 unittest
323 {
324 	bool dummy;
325 
326 	{
327 		OptionHandler optHandler = OptionHandler(["prog"]);
328 		optHandler.read("dummy", "some", dummy);
329 
330 		assertThrown!OptionHandlerException(optHandler.read("dummy", "same", dummy));
331 	}
332 }