1 | // tCommand.cpp  |
2 | //  |
3 | // Parses a command line. A description of how to use the parser is in the header. Internally the first step is the  |
4 | // expansion of combined single hyphen options. Next the parameters and options are parsed out. For each registered  |
5 | // tOption and tParam object, its members are set to reflect the current command line when the tParse call is made.  |
6 | // You may have more than one tOption that responds to the same option name. You may have more than one tParam that  |
7 | // responds to the same parameter number.  |
8 | //  |
9 | // Copyright (c) 2017, 2020 Tristan Grimmer.  |
10 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
11 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
12 | //  |
13 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
14 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
15 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
16 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
17 | // PERFORMANCE OF THIS SOFTWARE.  |
18 |   |
19 | #include <Foundation/tFundamentals.h>  |
20 | #include "System/tCommand.h"  |
21 | #include "System/tFile.h"  |
22 |   |
23 |   |
24 | namespace tCommand  |
25 | {  |
26 | // Any single-hyphen combined arguments are expanded here. Ex. -abc becomes -a -b -c.  |
27 | void ExpandArgs(tList<tStringItem>& args);  |
28 | int IndentSpaces(int numSpaces);  |
29 |   |
30 | // I'm relying on zero initialization here. It's all zeroes before any items are constructed.  |
31 | tList<tParam> Params(tListMode::StaticZero);  |
32 | tList<tOption> Options(tListMode::StaticZero);  |
33 | tString Program;  |
34 | tString Empty;  |
35 | }  |
36 |   |
37 |   |
38 | tString tCommand::tGetProgram()  |
39 | {  |
40 | return Program;  |
41 | }  |
42 |   |
43 |   |
44 | tCommand::tParam::tParam(int paramNumber, const char* name, const char* description) :  |
45 | ParamNumber(paramNumber),  |
46 | Param(),  |
47 | Name(),  |
48 | Description()  |
49 | {  |
50 | if (name)  |
51 | Name = tString(name);  |
52 | else  |
53 | tsPrintf(Name, "Param%d" , paramNumber);  |
54 |   |
55 | if (description)  |
56 | Description = tString(description);  |
57 |   |
58 | Params.Append(this);  |
59 | }  |
60 |   |
61 |   |
62 | tCommand::tOption::tOption(const char* description, char shortName, const char* longName, int numArgs) :  |
63 | ShortName(shortName),  |
64 | LongName(longName),  |
65 | Description(description),  |
66 | NumFlagArgs(numArgs),  |
67 | Args(tListMode::ListOwns),  |
68 | Present(false)  |
69 | {  |
70 | Options.Append(this);  |
71 | }  |
72 |   |
73 |   |
74 | tCommand::tOption::tOption(const char* description, const char* longName, char shortName, int numArgs) :  |
75 | ShortName(shortName),  |
76 | LongName(longName),  |
77 | Description(description),  |
78 | NumFlagArgs(numArgs),  |
79 | Args(tListMode::ListOwns),  |
80 | Present(false)  |
81 | {  |
82 | Options.Append(this);  |
83 | }  |
84 |   |
85 |   |
86 | tCommand::tOption::tOption(const char* description, char shortName, int numArgs) :  |
87 | ShortName(shortName),  |
88 | LongName(),  |
89 | Description(description),  |
90 | NumFlagArgs(numArgs),  |
91 | Args(tListMode::ListOwns),  |
92 | Present(false)  |
93 | {  |
94 | Options.Append(this);  |
95 | }  |
96 |   |
97 |   |
98 | tCommand::tOption::tOption(const char* description, const char* longName, int numArgs) :  |
99 | ShortName(),  |
100 | LongName(longName),  |
101 | Description(description),  |
102 | NumFlagArgs(numArgs),  |
103 | Args(tListMode::ListOwns),  |
104 | Present(false)  |
105 | {  |
106 | Options.Append(this);  |
107 | }  |
108 |   |
109 |   |
110 | int tCommand::IndentSpaces(int numSpaces)  |
111 | {  |
112 | for (int s = 0; s < numSpaces; s++)  |
113 | tPrintf(" " );  |
114 |   |
115 | return numSpaces;  |
116 | }  |
117 |   |
118 |   |
119 | const tString& tCommand::tOption::ArgN(int n) const  |
120 | {  |
121 | for (tStringItem* arg = Args.First(); arg; arg = arg->Next(), n--)  |
122 | if (n <= 1)  |
123 | return *arg;  |
124 |   |
125 | return Empty;  |
126 | }  |
127 |   |
128 |   |
129 | void tCommand::tParse(int argc, char** argv)  |
130 | {  |
131 | if (argc <= 0)  |
132 | return;  |
133 |   |
134 | // Create a single line string of all the separate argv's. Arguments with quotes and spaces will come in as  |
135 | // distinct argv's, but they all get combined here. I don't believe two consecutive spaces will work.  |
136 | tString line;  |
137 | for (int a = 0; a < argc; a++)  |
138 | {  |
139 | char* arg = argv[a];  |
140 | if (!arg || (tStd::tStrlen(arg) == 0))  |
141 | continue;  |
142 |   |
143 | // Arg may have spaces within it. Such arguments need to be enclosed in quotes.  |
144 | tString argStr(arg);  |
145 | if (argStr.FindChar(' ') != -1)  |
146 | argStr = tString("\"" ) + argStr + tString("\"" );  |
147 |   |
148 | line += argStr;  |
149 | if (a < (argc - 1))  |
150 | line += " " ;  |
151 | }  |
152 |   |
153 | tParse(line, true);  |
154 | }  |
155 |   |
156 |   |
157 | void tCommand::ExpandArgs(tList<tStringItem>& args)  |
158 | {  |
159 | tList<tStringItem> expArgs(tListMode::ListOwns);  |
160 | while (tStringItem* arg = args.Remove())  |
161 | {  |
162 | if ((arg->Length() < 2) || ((*arg)[0] != '-') || (((*arg)[0] == '-') && ((*arg)[1] == '-')))  |
163 | {  |
164 | expArgs.Append(arg);  |
165 | continue;  |
166 | }  |
167 | // It's now a single hyphen with something after it.  |
168 |   |
169 | bool recognized = false;  |
170 | for (tOption* option = Options.First(); option; option = option->Next())  |
171 | {  |
172 | if ( option->ShortName == tString((*arg)[1]) )  |
173 | {  |
174 | recognized = true;  |
175 | break;  |
176 | }  |
177 | }  |
178 |   |
179 | // Unrecognized options are left unmodified. Means you can put -10 and have it treated as a parameter.  |
180 | // as long as you don't have an option with shortname "1".  |
181 | if (!recognized)  |
182 | {  |
183 | expArgs.Append(arg);  |
184 | continue;  |
185 | }  |
186 |   |
187 | // By now it's a single hyphen and is expandble. eg. -abc -> -a -b -c  |
188 | for (int flag = 1; flag < arg->Length(); flag++)  |
189 | {  |
190 | tString newArg = "-" + tString((*arg)[flag]);  |
191 | expArgs.Append(new tStringItem(newArg));  |
192 | }  |
193 |   |
194 | delete arg;  |
195 | }  |
196 |   |
197 | // Repopulate args.  |
198 | while (tStringItem* arg = expArgs.Remove())  |
199 | args.Append(arg);  |
200 | }  |
201 |   |
202 |   |
203 | static bool ParamSortFn(const tCommand::tParam& a, const tCommand::tParam& b)  |
204 | {  |
205 | return (a.ParamNumber < b.ParamNumber);  |
206 | }  |
207 |   |
208 |   |
209 | static bool OptionSortFnShort(const tCommand::tOption& a, const tCommand::tOption& b)  |
210 | {  |
211 | return tStd::tStrcmp(a.ShortName.Pod(), b.ShortName.Pod()) < 0;  |
212 | }  |
213 |   |
214 |   |
215 | static bool OptionSortFnLong(const tCommand::tOption& a, const tCommand::tOption& b)  |
216 | {  |
217 | return tStd::tStrcmp(a.LongName.Pod(), b.LongName.Pod()) < 0;  |
218 | }  |
219 |   |
220 |   |
221 | void tCommand::tParse(const char* commandLine, bool fullCommandLine)  |
222 | {  |
223 | // At this point the constructors for all tOptions and tParams will have been called and both Params and Options  |
224 | // lists are populated. Options can be specified in any order, but we're going to order them alphabetically by short  |
225 | // flag name so they get printed nicely by tPrintUsage. Params must be printed in order based on their param num  |
226 | // so we'll do that sort here too.  |
227 | Params.Sort(ParamSortFn);  |
228 | Options.Sort(OptionSortFnShort);  |
229 | Options.Sort(OptionSortFnLong);  |
230 |   |
231 | tString line(commandLine);  |
232 |   |
233 | // Mark both kinds of escaped quotes that may be present. These may be found when the caller  |
234 | // wants a quote inside a string on the command line.  |
235 | line.Replace("\\'" , tStd::SeparatorAStr);  |
236 | line.Replace("\\\"" , tStd::SeparatorBStr);  |
237 |   |
238 | // Mark the spaces and hyphens inside normal (non escaped) quotes.  |
239 | bool inside = false;  |
240 | for (char* ch = line.Text(); *ch; ch++)  |
241 | {  |
242 | if ((*ch == '\'') || (*ch == '\"'))  |
243 | inside = !inside;  |
244 |   |
245 | if (!inside)  |
246 | continue;  |
247 |   |
248 | if (*ch == ' ')  |
249 | *ch = tStd::SeparatorC;  |
250 |   |
251 | if (*ch == '-')  |
252 | *ch = tStd::SeparatorD;  |
253 | }  |
254 |   |
255 | line.Remove('\'');  |
256 | line.Remove('\"');  |
257 |   |
258 | tList<tStringItem> args(tListMode::ListOwns);  |
259 | tStd::tExplode(args, line, ' ');  |
260 |   |
261 | // Now that the arguments are exploded into separate elements we replace the separators with the correct characters.  |
262 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
263 | {  |
264 | arg->Replace(tStd::SeparatorA, '\'');  |
265 | arg->Replace(tStd::SeparatorB, '\"');  |
266 | arg->Replace(tStd::SeparatorC, ' ');  |
267 | }  |
268 |   |
269 | // Set the program name as typed in the command line.  |
270 | if (fullCommandLine)  |
271 | {  |
272 | tStringItem* prog = args.Remove();  |
273 | Program.Set(prog->ConstText());  |
274 | delete prog;  |
275 | }  |
276 | else  |
277 | {  |
278 | Program.Clear();  |
279 | }  |
280 |   |
281 | ExpandArgs(args);  |
282 |   |
283 | // Process all options.  |
284 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
285 | {  |
286 | for (tOption* option = Options.First(); option; option = option->Next())  |
287 | {  |
288 | if ( (*arg == tString("--" ) + option->LongName) || (*arg == tString("-" ) + option->ShortName) )  |
289 | {  |
290 | option->Present = true;  |
291 | for (int optArgNum = 0; optArgNum < option->NumFlagArgs; optArgNum++)  |
292 | {  |
293 | arg = arg->Next();  |
294 | tStringItem* argItem = new tStringItem(*arg);  |
295 | argItem->Replace(tStd::SeparatorD, '-');  |
296 | option->Args.Append(argItem);  |
297 | }  |
298 | }  |
299 | }  |
300 | }  |
301 |   |
302 | // Now we're going to create a list of just the parameters by skipping any options as we encounter them.  |
303 | // For any option that we know about we'll also have to skip its option arguments.  |
304 | tList<tStringItem> commandLineParams(tListMode::ListOwns);  |
305 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
306 | {  |
307 | tStringItem* candidate = arg;  |
308 |   |
309 | // This loop skips any options for the current arg.  |
310 | for (tOption* option = Options.First(); option; option = option->Next())  |
311 | {  |
312 | if (*(arg->Text()) == '-')  |
313 | {  |
314 | tString flagArg = *arg;  |
315 |   |
316 | // We only skip flags we recognize.  |
317 | if ( (flagArg == tString("--" ) + option->LongName) || (flagArg == tString("-" ) + option->ShortName) )  |
318 | {  |
319 | candidate = nullptr;  |
320 | for (int optArgNum = 0; optArgNum < option->NumFlagArgs; optArgNum++)  |
321 | arg = arg->Next();  |
322 | }  |
323 | }  |
324 | }  |
325 |   |
326 | if (candidate)  |
327 | commandLineParams.Append(new tStringItem(*candidate));  |
328 | }  |
329 |   |
330 | // Process all parameters.  |
331 | int paramNumber = 1;  |
332 | for (tStringItem* arg = commandLineParams.First(); arg; arg = arg->Next(), paramNumber++)  |
333 | {  |
334 | arg->Replace(tStd::SeparatorD, '-');  |
335 | for (tParam* param = Params.First(); param; param = param->Next())  |
336 | {  |
337 | if (param->ParamNumber == paramNumber)  |
338 | param->Param = *arg;  |
339 | }  |
340 | }  |
341 | }  |
342 |   |
343 |   |
344 | void tCommand::tPrintSyntax()  |
345 | {  |
346 | tString syntax =  |
347 | R"U5AG3(Syntax Help:   |
348 | tool.exe [arguments]   |
349 |   |
350 | Arguments are separated by spaces. An argument must be enclosed in quotes   |
351 | (single or double) if it has a space or hyphen in it. Use escape sequences to   |
352 | put either type of quote inside. If you need to specify paths, it is suggested   |
353 | to use forward slashes, although backslashes will work so long as the filename   |
354 | does not have a single or double quote next.   |
355 |   |
356 | An argument may be an 'option' or a 'parameter'.   |
357 |   |
358 | Options:   |
359 | An option has a short syntax and a long syntax. Short syntax is a - followed by   |
360 | a single non-hyphen character. The long form is -- followed by a word. All   |
361 | options support either long, short, or both forms. Options may have 0 or more   |
362 | arguments. If an option takes zero arguments it is called a flag and you can   |
363 | only test for its presence or lack of. Options can be specified in any order.   |
364 | Short form options may be combined: Eg. -al expands to -a -l   |
365 |   |
366 | Parameters:   |
367 | A parameter is simply an argument that does not start with a - or --. It can be   |
368 | read as a string and parsed arbitrarily (converted to an integer or float etc.)   |
369 | Order is important when specifying parameters.   |
370 |   |
371 | Example:   |
372 | mycopy.exe -R --overwrite fileA.txt -pat fileB.txt --log log.txt   |
373 |   |
374 | The fileA.txt and fileB.txt in the above example are parameters (assuming   |
375 | the overwrite option is a flag). fileA.txt is the first parameter and   |
376 | fileB.txt is the second.   |
377 |   |
378 | The '--log log.txt' is an option with a single argument, log.txt. Flags may be   |
379 | combined. The -pat in the example expands to -p -a -t. It is suggested only to   |
380 | combine flag options as only the last option would get any arguments.   |
381 |   |
382 | If you wish to interpret a hyphen directly instead of as an option specifier   |
383 | this will happen automatically if there are no options matching what comes   |
384 | after the hyphen. Eg. 'tool.exe -.85 --add 33 -87.98 -notpresent' works just   |
385 | fine as long as there are no options that have a short form with digits or a   |
386 | decimal. In this example the -.85 will be the first parameter, --notpresent   |
387 | will be the second, and the --add takes in two number arguments.   |
388 |   |
389 | Variable argument options are not supported due to the extra syntax that would   |
390 | be needed. The same result is achieved by entering the same option more than   |
391 | once. Eg. tool.exe -I /patha/include/ -I /pathb/include   |
392 |   |
393 | )U5AG3" ;  |
394 |   |
395 | tPrintf("%s" , syntax.Pod());  |
396 | }  |
397 |   |
398 |   |
399 | void tCommand::tPrintUsage(int versionMajor, int versionMinor, int revision)  |
400 | {  |
401 | tPrintUsage(nullptr, versionMajor, versionMinor, revision);  |
402 | }  |
403 |   |
404 |   |
405 | void tCommand::tPrintUsage(const char* author, int versionMajor, int versionMinor, int revision)  |
406 | {  |
407 | tPrintUsage(author, nullptr, versionMajor, versionMinor, revision);  |
408 | }  |
409 |   |
410 |   |
411 | void tCommand::tPrintUsage(const char* author, const char* desc, int versionMajor, int versionMinor, int revision)  |
412 | {  |
413 | tAssert(versionMajor >= 0);  |
414 | tAssert((versionMinor >= 0) || (revision < 0)); // Not allowed a valid revision number if minor is not also valid.  |
415 |   |
416 | char verAuth[128];  |
417 | char* va = verAuth;  |
418 | va += tsPrintf(va, "Version %d" , versionMajor);  |
419 | if (versionMinor >= 0)  |
420 | {  |
421 | va += tsPrintf(va, ".%d" , versionMinor);  |
422 | if (revision >= 0)  |
423 | va += tsPrintf(va, ".%d" , revision);  |
424 | }  |
425 |   |
426 | if (author)  |
427 | va += tsPrintf(va, " by %s" , author);  |
428 |   |
429 | tPrintUsage(verAuth, desc);  |
430 | }  |
431 |   |
432 |   |
433 | void tCommand::tPrintUsage(const char* versionAuthorString, const char* desc)  |
434 | {  |
435 | tString exeName = "Program.exe" ;  |
436 | if (!tCommand::Program.IsEmpty())  |
437 | exeName = tSystem::tGetFileName(tCommand::Program);  |
438 |   |
439 | if (versionAuthorString)  |
440 | tPrintf("%s %s\n\n" , tPod(tSystem::tGetFileBaseName(exeName)), versionAuthorString);  |
441 |   |
442 | if (Options.IsEmpty())  |
443 | tPrintf("USAGE: %s " , exeName.Pod());  |
444 | else  |
445 | tPrintf("USAGE: %s [options] " , exeName.Pod());  |
446 |   |
447 | // Support 256 parameters.  |
448 | bool printedParamNum[256];  |
449 | tStd::tMemset(printedParamNum, 0, sizeof(printedParamNum));  |
450 | for (tParam* param = Params.First(); param; param = param->Next())  |
451 | {  |
452 | if ((param->ParamNumber < 256) && !printedParamNum[param->ParamNumber])  |
453 | {  |
454 | if (!param->Name.IsEmpty())  |
455 | tPrintf("%s " , param->Name.Pod());  |
456 | else  |
457 | tPrintf("param%d " , param->ParamNumber);  |
458 | printedParamNum[param->ParamNumber] = true;  |
459 | }  |
460 | }  |
461 |   |
462 | tPrintf("\n\n" );  |
463 | if (desc)  |
464 | tPrintf("%s" , desc);  |
465 | tPrintf("\n\n" );  |
466 |   |
467 | int indent = 0;  |
468 | if (!Options.IsEmpty())  |
469 | {  |
470 | for (tOption* option = Options.First(); option; option = option->Next())  |
471 | {  |
472 | int numPrint = 0;  |
473 | if (!option->LongName.IsEmpty())  |
474 | numPrint += tcPrintf("--%s " , option->LongName.Pod());  |
475 | if (!option->ShortName.IsEmpty())  |
476 | numPrint += tcPrintf("-%s " , option->ShortName.Pod());  |
477 | for (int a = 0; a < option->NumFlagArgs; a++)  |
478 | numPrint += tcPrintf("arg%c " , '1'+a);  |
479 |   |
480 | indent = tMath::tMax(indent, numPrint);  |
481 | }  |
482 | }  |
483 |   |
484 | if (!Params.IsEmpty())  |
485 | {  |
486 | // Loop through them all to figure out how far to indent.  |
487 | for (tParam* param = Params.First(); param; param = param->Next())  |
488 | {  |
489 | int numPrint = 0;  |
490 | if (!param->Name.IsEmpty())  |
491 | numPrint = tcPrintf("%s " , param->Name.Pod());  |
492 | else  |
493 | numPrint = tcPrintf("param%d " , param->ParamNumber);  |
494 | indent = tMath::tMax(indent, numPrint);  |
495 | }  |
496 | }  |
497 |   |
498 | if (!Options.IsEmpty())  |
499 | {  |
500 | tPrintf("Options:\n" );  |
501 | for (tOption* option = Options.First(); option; option = option->Next())  |
502 | {  |
503 | int numPrinted = 0;  |
504 | if (!option->LongName.IsEmpty())  |
505 | numPrinted += tPrintf("--%s " , option->LongName.Pod());  |
506 | if (!option->ShortName.IsEmpty())  |
507 | numPrinted += tPrintf("-%s " , option->ShortName.Pod());  |
508 |   |
509 | for (int a = 0; a < option->NumFlagArgs; a++)  |
510 | numPrinted += tPrintf("arg%c " , '1'+a);  |
511 |   |
512 | IndentSpaces(indent-numPrinted);  |
513 | tPrintf(" : %s\n" , option->Description.Pod());  |
514 | }  |
515 | tPrintf("\n" );  |
516 | }  |
517 |   |
518 | if (!Params.IsEmpty())  |
519 | {  |
520 | tPrintf("Parameters:\n" );  |
521 | for (tParam* param = Params.First(); param; param = param->Next())  |
522 | {  |
523 | int numPrinted = 0;  |
524 | if (!param->Name.IsEmpty())  |
525 | numPrinted = tPrintf("%s " , param->Name.Pod());  |
526 | else  |
527 | numPrinted = tPrintf("param%d " , param->ParamNumber);  |
528 |   |
529 | IndentSpaces(indent - numPrinted);  |
530 |   |
531 | if (!param->Description.IsEmpty())  |
532 | tPrintf(" : %s" , param->Description.Pod());  |
533 |   |
534 | tPrintf("\n" );  |
535 | }  |
536 | tPrintf("\n" );  |
537 | }  |
538 | }  |
539 | |