1 /**
2  * Copyright: Copyright (c) 2012 Jacob Carlborg. All rights reserved.
3  * Authors: Jacob Carlborg
4  * Version: Initial created: Aug 1, 2012
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
6  */
7 module mambo.arguments.Formatter;
8 
9 import mambo.arguments.Arguments;
10 import mambo.arguments.Options;
11 import mambo.core._;
12 import mambo.text.Inflections;
13 
14 abstract class Formatter
15 {
16 	protected
17 	{
18 		string appName_;
19 		string appVersion_;
20 	}
21 
22 	@property static Formatter instance (Arguments arguments)
23 	{
24 		return new DefaultFormatter(arguments);
25 	}
26 
27 	@property string appName ()
28 	{
29 		return appName_;
30 	}
31 
32 	@property string appName (string value)
33 	{
34 		return appName_ = value;
35 	}
36 
37 	@property string appVersion ()
38 	{
39 		return appVersion_;
40 	}
41 
42 	@property string appVersion (string value)
43 	{
44 		return appVersion_ = value;
45 	}
46 
47 	abstract @property string helpText ();
48 	abstract string errors (char[] delegate (char[] buffer, const(char)[] format, ...) dg);
49 }
50 
51 class DefaultFormatter : Formatter
52 {
53 	private
54 	{
55 		Arguments arguments;
56 		ArgumentBase[] positionalArguments_;
57 		Option!(int)[] options_;
58 		enum indentation = "    ";
59 
60 		string[] errorMessages_ = defaultErrorMessages;
61 		enum defaultErrorMessages = [
62 			"argument '{0}' expects {2} parameter(s) but has {1}\n",
63 			"argument '{0}' expects {3} parameter(s) but has {1}\n",
64 			"argument '{0}' is missing\n",
65 			"argument '{0}' requires '{4}'\n",
66 			"argument '{0}' conflicts with '{4}'\n",
67 			"unexpected argument '{0}'\n",
68 			"argument '{0}' expects one of {5}\n",
69 			"invalid parameter for argument '{0}': {4}\n",
70 		];
71 	}
72 
73 	this (Arguments arguments)
74 	{
75 		this.arguments = arguments;
76 	}
77 
78 	override string errors (char[] delegate (char[] buffer, const(char)[] format, ...) dg)
79 	{
80 		auto result = arguments.errors(dg).assumeUnique;
81 		char[256] buffer;
82 		auto msg = errorMessages;
83 		auto posArgs = arguments.positionalArguments;
84 		posArgs.sort!((a, b) => a.position < b.position)();
85 
86 		foreach (arg ; posArgs)
87 		{
88 			if (arg.error)
89 				result ~= dg(buffer, msg[arg.error - 1], arg.name, arg.rawValues.length,
90 					arg.min, arg.max);
91 		}
92 
93 		return result;
94 	}
95 
96 	@property string[] errorMessages ()
97 	{
98 		return errorMessages_;
99 	}
100 
101 	@property string[] errorMessages (string[] errors)
102 	in
103 	{
104 		assert(errors.length == defaultErrorMessages.length);
105 	}
106 	body
107 	{
108 		return errorMessages_ = errors;
109 	}
110 
111 	override @property string helpText ()
112 	{
113 		string help = header;
114 
115 		if (positionalArguments.any)
116 			help ~= "\n\n" ~ positionalArgumentsText;
117 
118 		if (options.any)
119 			help ~= "\n\n" ~ optionsText;
120 
121 		help ~= '\n' ~ footer;
122 
123 		return help;
124 	}
125 
126 private:
127 
128 	@property string optionsText ()
129 	{
130 		string help = "Option:\n";
131 		const optionNames = generateOptionNames();
132 		immutable len = lengthOfLongestOption(optionNames);
133 		enum numberOfIndentations = 1;
134 
135 		assert(options.length == optionNames.length);
136 
137 		foreach (i, option ; options)
138 		{
139 			auto text = option.helpText ~ '.';
140 			auto name = optionNames[i];
141 
142 			if (option.name.count == 0 && shortOption(option) == char.init)
143 				help ~= format("{}\n", text);
144 
145 			else if (shortOption(option) == char.init)
146 				help ~= format("{}--{}{}{}{}\n",
147 							indentation ~ indentation,
148 							name,
149 							" ".repeat(len - name.count),
150 							indentation.repeat(numberOfIndentations),
151 							text);
152 
153 			else
154 				help ~= format("{}-{}, --{}{}{}{}\n",
155 							indentation,
156 							shortOption(option),
157 							name,
158 							" ".repeat(len - name.count),
159 							indentation.repeat(numberOfIndentations),
160 							text);
161 		}
162 
163 		return help;
164 	}
165 
166 	@property Option!(int)[] options ()
167 	{
168 		return options_ = options_.any ? options_ : arguments.options;
169 	}
170 
171 	@property char shortOption (Option!(int) option)
172 	{
173 		return option.aliases.any ? option.aliases[0] : char.init;
174 	}
175 
176 	@property size_t lengthOfLongestOption (const string[] names)
177 	{
178 		return names.reduce!((a, b) => a.count > b.count ? a : b).count;
179 	}
180 
181 	@property ArgumentBase[] positionalArguments ()
182 	{
183 		return positionalArguments_.any ? positionalArguments_ : (positionalArguments_ = arguments.positionalArguments);
184 	}
185 
186 	@property string header ()
187 	{
188 		string str = "Usage: " ~ appName;
189 
190 		if (options.any)
191 			str ~= " [options]";
192 
193 		if (positionalArguments.any)
194 			str ~= format(" <{}>", "arg".pluralize(positionalArguments.length));
195 
196 		str ~= "\nVersion " ~ appVersion;
197 
198 		return str;
199 	}
200 
201 	@property string footer ()
202 	{
203 		return "Use the `-h' flag for help.";
204 	}
205 
206 	@property string positionalArgumentsText ()
207 	{
208 		immutable len = lengthOfLongestPositionalArgument;
209 		auto str = "Positional Arguments:\n";
210 		enum numberOfIndentations = 1;
211 
212 		foreach (arg ; positionalArguments)
213 		{
214 			immutable text = arg.helpText ~ '.';
215 
216 			str ~= format("{}{}{}{}{}\n",
217 				indentation,
218 				arg.name,
219 				" ".repeat(len - arg.name.count),
220 				indentation.repeat(numberOfIndentations),
221 				text);
222 		}
223 
224 		return str;
225 	}
226 
227 	@property size_t lengthOfLongestPositionalArgument ()
228 	{
229 		return positionalArguments.reduce!((a, b) => a.name.count > b.name.count ? a : b).name.count;
230 	}
231 
232     string[] generateOptionNames ()
233     {
234 		return options.map!((option) {
235 			string name = option.name;
236 
237 			if (option.min == 1)
238 				name ~= " <arg>";
239 
240 			else if (option.min > 1)
241 				name ~= " <arg0>";
242 
243 			if (option.max > 1)
244 				name ~= " .. <arg" ~ option.max.toString ~ '>';
245 
246 			return name;
247 		}).toArray;
248     }
249 }