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 }