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 }