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 
24namespace 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 
38tString tCommand::tGetProgram() 
39
40 return Program
41
42 
43 
44tCommand::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 
62tCommand::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 
74tCommand::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 
86tCommand::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 
98tCommand::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 
110int tCommand::IndentSpaces(int numSpaces
111
112 for (int s = 0; s < numSpaces; s++) 
113 tPrintf(" "); 
114 
115 return numSpaces
116
117 
118 
119const 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 
129void 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 
157void 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 
203static bool ParamSortFn(const tCommand::tParam& a, const tCommand::tParam& b
204
205 return (a.ParamNumber < b.ParamNumber); 
206
207 
208 
209static 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 
215static 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 
221void 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 
344void tCommand::tPrintSyntax() 
345
346 tString syntax
347R"U5AG3(Syntax Help: 
348tool.exe [arguments] 
349 
350Arguments 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 
352put either type of quote inside. If you need to specify paths, it is suggested 
353to use forward slashes, although backslashes will work so long as the filename 
354does not have a single or double quote next. 
355 
356An argument may be an 'option' or a 'parameter'. 
357 
358Options: 
359An option has a short syntax and a long syntax. Short syntax is a - followed by 
360a single non-hyphen character. The long form is -- followed by a word. All 
361options support either long, short, or both forms. Options may have 0 or more 
362arguments. If an option takes zero arguments it is called a flag and you can 
363only test for its presence or lack of. Options can be specified in any order. 
364Short form options may be combined: Eg. -al expands to -a -l 
365 
366Parameters: 
367A parameter is simply an argument that does not start with a - or --. It can be 
368read as a string and parsed arbitrarily (converted to an integer or float etc.) 
369Order is important when specifying parameters. 
370 
371Example: 
372mycopy.exe -R --overwrite fileA.txt -pat fileB.txt --log log.txt 
373 
374The fileA.txt and fileB.txt in the above example are parameters (assuming 
375the overwrite option is a flag). fileA.txt is the first parameter and 
376fileB.txt is the second. 
377 
378The '--log log.txt' is an option with a single argument, log.txt. Flags may be 
379combined. The -pat in the example expands to -p -a -t. It is suggested only to 
380combine flag options as only the last option would get any arguments. 
381 
382If you wish to interpret a hyphen directly instead of as an option specifier 
383this will happen automatically if there are no options matching what comes 
384after the hyphen. Eg. 'tool.exe -.85 --add 33 -87.98 -notpresent' works just 
385fine as long as there are no options that have a short form with digits or a 
386decimal. In this example the -.85 will be the first parameter, --notpresent 
387will be the second, and the --add takes in two number arguments. 
388 
389Variable argument options are not supported due to the extra syntax that would 
390be needed. The same result is achieved by entering the same option more than 
391once. Eg. tool.exe -I /patha/include/ -I /pathb/include 
392 
393)U5AG3"
394 
395 tPrintf("%s", syntax.Pod()); 
396
397 
398 
399void tCommand::tPrintUsage(int versionMajor, int versionMinor, int revision
400
401 tPrintUsage(nullptr, versionMajor, versionMinor, revision); 
402
403 
404 
405void tCommand::tPrintUsage(const char* author, int versionMajor, int versionMinor, int revision
406
407 tPrintUsage(author, nullptr, versionMajor, versionMinor, revision); 
408
409 
410 
411void 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 
433void 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