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