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 @safe pure
53 class OptionHandlerException : Exception
54 {
55 	mixin basicExceptionCtors;
56 }
57 
58 static assert(is(typeof(new OptionHandlerException("message"))));
59 static assert(is(typeof(new OptionHandlerException("message", Exception.init))));
60 
61 @safe pure
62 struct OptionHandler {
63 	struct Option {
64 		string[] opts;
65 		string help;
66 	}
67 
68 	struct Argument {
69 		string name;
70 		ArgumentSize size;
71 		string value = null;
72 	}
73 
74 	enum ArgumentSize {
75 		Long,
76 		Short
77 	}
78 
79 	@safe pure
80 	public this(string[] args)
81 	{
82 		foreach(string arg ; args[1 .. $])
83 		{
84 			if(arg.startsWith("--"))
85 			{
86 				string splitted = arg.split("--")[1];
87 				if(splitted.length == 0)
88 					break;
89 
90 				ptrdiff_t hasValue = splitted.indexOf('=');
91 				if(hasValue != -1)
92 				{
93 					string newSplitted = splitted[0 .. hasValue];
94 					if(newSplitted.isAlpha)
95 						this.args ~= Argument(newSplitted, ArgumentSize.Long, splitted[hasValue + 1 .. $]);
96 				}
97 				else if(splitted.isAlpha)
98 					this.args ~= Argument(splitted, ArgumentSize.Long);
99 			}
100 			else if (arg.startsWith("-"))
101 			{
102 				string splitted = arg.split("-")[1];
103 				ptrdiff_t hasValue = splitted.indexOf('=');
104 				if(hasValue != -1)
105 				{
106 					string newSplitted = splitted[0 .. hasValue];
107 					if(newSplitted.isAlpha)
108 						this.args ~= Argument(newSplitted, ArgumentSize.Short, splitted[hasValue + 1 .. $]);
109 				}
110 				else if(splitted.isAlpha && splitted.length != 0)
111 					this.args ~= Argument(splitted, ArgumentSize.Short);
112 			}
113 		}
114 	}
115 
116 	@safe pure
117 	public T[] read(T)(string opts, string help, bool required = false)
118 	{
119 		Option opte = read(opts, help);
120 		T[] ret;
121 		bool value = false;
122 		foreach(arg; args) if(opts.canFind(arg.name))
123 		{
124 			value = true;
125 			string retstr = arg.value;
126 			if(retstr !is null)
127 			{
128 				import std.conv : to;
129 				ret ~= to!T(retstr);
130 			}
131 		}
132 		if(required)
133 		{
134 			if(!value || (value && ret.empty))
135 				throw new OptionHandlerException("Required option %s with %s type".format(opts, fullyQualifiedName!T));
136 		}
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 	public bool helpWanted()
176 	{
177 		foreach(arg; args)
178 		{
179 			if(arg.name == "help")
180 				return true;
181 		}
182 		return false;
183 	}
184 
185 	public string printableHelp(string programName)
186 	{
187 		string ret;
188 
189 		ret ~= "Usage:\n\t%s -- <options>\n\nOptions:\n".format(programName);
190 
191 		foreach(opt; opts)
192 		{
193 			ret ~= opt.opts[0];
194 			if(opt.opts.length == 2)
195 			{
196 				ret ~= " " ~ opt.opts[1];
197 			}
198 			ret ~= "\t" ~ opt.help ~ "\n";
199 		}
200 		return ret;
201 	}
202 
203 	@safe pure
204 	@property public Argument[] arguments()
205 	{
206 		return this.args.dup;
207 	}
208 
209 	@property
210 	public Option[] options()
211 	{
212 		return this.opts.dup;
213 	}
214 
215 	private Argument[] args;
216 	private Option[] opts;
217 }
218 
219 @safe pure
220 @("Option Handler: check arguments")
221 unittest
222 {
223 	OptionHandler opts = OptionHandler(["prog", "--foo", "-f", "--bar=foobar"]);
224 
225 	OptionHandler.Argument[] args = [
226 		OptionHandler.Argument("foo", OptionHandler.ArgumentSize.Long),
227 		OptionHandler.Argument("f", OptionHandler.ArgumentSize.Short),
228 		OptionHandler.Argument("bar", OptionHandler.ArgumentSize.Long, "foobar")
229 	];
230 
231 	assert(opts.arguments == args);
232 }
233 
234 
235 @safe pure
236 @("Option Handler: check read")
237 unittest
238 {
239 	bool isFoo, isFoobar;
240 
241 	{
242 		OptionHandler optHandler = OptionHandler(["prog", "--foo", "-f"]);
243 
244 		optHandler.read("foo", "information", isFoo, true);
245 		optHandler.read("foobar|f", "quick", isFoobar);
246 	}
247 
248 	assert(isFoo == true);
249 	assert(isFoobar == true);
250 }
251 
252 
253 @safe pure
254 @("Option Handler: check values")
255 unittest
256 {
257 	int[] foo;
258 
259 	{
260 		OptionHandler optHandler = OptionHandler(["prog", "--foo=4", "-f=7"]);
261 
262 		foo = optHandler.read!int("foo|f", "information", true);
263 	}
264 
265 	assert(!foo.empty);
266 	assert(foo[0] == 4);
267 	assert(foo[1] == 7);
268 }
269 
270 
271 @safe pure
272 @("Option Handler: required exception")
273 unittest
274 {
275 	bool isFoo, isBar;
276 
277 	{
278 		OptionHandler optHandler = OptionHandler(["prog"]);
279 
280 		optHandler.read("bar", "information", isBar);
281 
282 		try {
283 			optHandler.read("foo", "more information", isFoo, true);
284 			assert(0);
285 		} catch(OptionHandlerException) {}
286 	}
287 }
288 
289 @safe pure
290 @("Option Handler: twice")
291 unittest
292 {
293 	bool dummy;
294 	OptionHandler optHandler = OptionHandler(["prog"]);
295 	optHandler.read("dummy", "some", dummy);
296 
297 	try {
298 		optHandler.read("dummy", "same", dummy);
299 		assert(0);
300 	} catch (OptionHandlerException) {}
301 }