1 /* 2 * hunt-console eases the creation of beautiful and testable command line interfaces. 3 * 4 * Copyright (C) 2018-2019, HuntLabs 5 * 6 * Website: https://www.huntlabs.net 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.console.input.ArgvInput; 13 14 import std.algorithm.searching; 15 import std.string; 16 import std.conv; 17 import hunt.collection.ArrayList; 18 // import hunt.collection.Arrays; 19 import hunt.collection.List; 20 import hunt.console.input.AbstractInput; 21 import hunt.console.input.InputDefinition; 22 import hunt.text.Common; 23 import hunt.Exceptions; 24 import hunt.console.input.InputArgument; 25 import hunt.console.input.InputOption; 26 27 import hunt.logging.ConsoleLogger; 28 29 import std.range; 30 31 32 /** 33 * 34 */ 35 class ArgvInput : AbstractInput 36 { 37 private List!(string) tokens; 38 private List!(string) parsed; 39 40 this(string[] args) 41 { 42 this(args, null); 43 } 44 45 this(string[] args, InputDefinition definition) 46 { 47 this(new ArrayList!(string)(args), definition); 48 } 49 50 this(List!(string) args, InputDefinition definition) 51 { 52 tokens = args; 53 54 if (definition is null) { 55 this.definition = new InputDefinition(); 56 } else { 57 bind(definition); 58 validate(); 59 } 60 } 61 62 protected void setTokens(List!(string) tokens) 63 { 64 this.tokens = tokens; 65 } 66 67 override protected void parse() 68 { 69 bool parseOptions = true; 70 parsed = tokens; 71 while(parsed.size() > 0) { 72 string token = parsed.removeAt(0); 73 if (parseOptions && token == ("")) { 74 parseArgument(token); 75 } else if (parseOptions && token == ("--")) { 76 parseOptions = false; 77 } else if (parseOptions && token.startsWith("--")) { 78 parseLongOption(token); 79 } else if (parseOptions && token[0] == '-' && !(token == ("-"))) { 80 parseShortOption(token); 81 } else { 82 parseArgument(token); 83 } 84 } 85 } 86 87 /** 88 * Parses a short option. 89 */ 90 private void parseShortOption(string token) 91 { 92 string option = token[1 .. $]; 93 version(HUNT_CONSOLE_DEBUG) { 94 tracef("option: %s", option); 95 } 96 97 if (option.length > 1) { 98 string name = format("%c", option[0]); 99 if (definition.hasShortcut(name) && definition.getOptionForShortcut(name).acceptValue()) { 100 // an option with a value (with no space) 101 option = option[1..$]; 102 if(option[0] == '=') { 103 option = option[1..$]; // skip the '='; 104 } 105 addShortOption(name, option); 106 } else { 107 parseShortOptionSet(option); 108 } 109 } else { 110 addShortOption(option, null); 111 } 112 } 113 114 private void parseShortOptionSet(string options) 115 { 116 int len = cast(int)(options.length); 117 string name; 118 for (int i = 0; i < len; i++) { 119 name = to!string(options[i]); 120 if (!definition.hasShortcut(name)) { 121 throw new RuntimeException(format("The '-%s' option does not exist.", name)); 122 } 123 124 InputOption option = definition.getOptionForShortcut(name); 125 if (option.acceptValue()) { 126 addLongOption(option.getName(), i == len - 1 ? null : options.substring(i + 1)); 127 break; 128 } else { 129 addLongOption(option.getName(), null); 130 } 131 } 132 } 133 134 private void parseLongOption(string token) 135 { 136 string option = token.substring(2); 137 138 int pos = cast(int)(option.indexOf('=')); 139 if (pos > -1) { 140 addLongOption(option.substring(0, pos), option.substring(pos + 1)); 141 } else { 142 addLongOption(option, null); 143 } 144 } 145 146 private void parseArgument(string token) 147 { 148 int c = arguments.size(); 149 150 // if input is expecting another argument, add it 151 if (definition.hasArgument(c)) { 152 InputArgument argument = definition.getArgument(c); 153 // todo array arguments 154 arguments.put(argument.getName(), token); 155 156 // if last argument isArray(), append token to last argument 157 } else if (definition.hasArgument(c - 1) && definition.getArgument(c - 1).isArray()) { 158 InputArgument argument = definition.getArgument(c - 1); 159 // todo implement 160 161 // unexpected argument 162 } else { 163 throw new RuntimeException("Too many arguments"); 164 } 165 } 166 167 private void addShortOption(string shortcut, string value) 168 { 169 if (!definition.hasShortcut(shortcut)) { 170 throw new RuntimeException(format("The '-%s' option does not exist.", shortcut)); 171 } 172 173 addLongOption(definition.getOptionForShortcut(shortcut).getName(), value); 174 } 175 176 private void addLongOption(string name, string value) 177 { 178 version(HUNT_CONSOLE_DEBUG) { 179 warningf("%s, %s", name, value); 180 } 181 182 if (!definition.hasOption(name)) { 183 throw new RuntimeException(format("The '--%s' option does not exist.", name)); 184 } 185 186 InputOption option = definition.getOption(name); 187 188 if (value !is null && !option.acceptValue()) { 189 throw new RuntimeException(format("The '--%s' option does not accept a value.", name)); 190 } 191 192 if (value is null && option.acceptValue() && parsed.size() > 0) { 193 // if option accepts an optional or mandatory argument 194 // let's see if there is one provided 195 string next = parsed.removeAt(0); 196 if (!next.isEmpty && next[0] != '-') { 197 value = next; 198 } else if (next.isEmpty()) { 199 value = ""; 200 } else { 201 parsed.add(0, next); 202 } 203 } 204 205 if (value is null) { 206 if (option.isValueRequired()) { 207 throw new RuntimeException(format("The '--%s' option requires a value.", name)); 208 } 209 210 if (!option.isArray()) { 211 value = option.isValueOptional() ? option.getDefaultValue() : "true"; 212 } 213 } 214 215 if (option.isArray()) { 216 // todo implement 217 } else { 218 options.put(name, value); 219 } 220 } 221 222 /** 223 * Returns the first argument from the raw parameters (not parsed). 224 */ 225 override string getFirstArgument() 226 { 227 foreach (string token ; tokens) { 228 if (!token.isEmpty() && token[0] == '-') { 229 continue; 230 } 231 232 return token; 233 } 234 235 return null; 236 } 237 238 239 /** 240 * Returns true if the raw parameters (not parsed) contain a value. 241 * 242 * This method is to be used to introspect the input parameters 243 * before they have been validated. It must be used carefully. 244 */ 245 override bool hasParameterOption(string[] values, bool onlyParams = false) 246 { 247 foreach (string token ; tokens) { 248 if(onlyParams && token == "--") return false; 249 250 foreach (string value ; values) { 251 // Options with values: 252 // For long options, test for '--option=' at beginning 253 // For short options, test for '-o' at beginning 254 ptrdiff_t index = value.indexOf("--"); 255 string leading = index == 0 ? value ~ "=" : value; 256 257 if (token == value || (!leading.empty() && token.indexOf(leading) == 0)) { 258 return true; 259 } 260 } 261 } 262 263 return false; 264 } 265 266 /** 267 * Returns the value of a raw option (not parsed). 268 * 269 * This method is to be used to introspect the input parameters 270 * before they have been validated. It must be used carefully 271 */ 272 override string getParameterOption(string[] values, string defaultValue, bool onlyParams = false) 273 { 274 List!(string) tokens = new ArrayList!(string)(this.tokens); 275 string token; 276 277 while(tokens.size() > 0) { 278 token = tokens.removeAt(0); 279 if(onlyParams && token == "--") return defaultValue; 280 281 foreach(string value; values) { 282 if(token == value) { 283 return tokens.removeAt(0); 284 } 285 // Options with values: 286 // For long options, test for '--option=' at beginning 287 // For short options, test for '-o' at beginning 288 ptrdiff_t index = value.indexOf("--"); 289 string leading = index == 0 ? value ~ "=" : value; 290 291 if (!leading.empty() && token.indexOf(leading) == 0) { 292 return token[leading.length .. $]; 293 } 294 } 295 } 296 297 return defaultValue; 298 } 299 300 override string toString() 301 { 302 return "ArgvInput{" ~ 303 "tokens=" ~ tokens.toString() ~ 304 '}'; 305 } 306 }