| 1 | // tCmdLine.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. You may also collect all parameters in a single tParam by setting the  |
| 8 | // parameter number to -1.  |
| 9 | //  |
| 10 | // Copyright (c) 2017, 2020, 2023-2025 Tristan Grimmer.  |
| 11 | // Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby  |
| 12 | // granted, provided that the above copyright notice and this permission notice appear in all copies.  |
| 13 | //  |
| 14 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL  |
| 15 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,  |
| 16 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN  |
| 17 | // AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR  |
| 18 | // PERFORMANCE OF THIS SOFTWARE.  |
| 19 |   |
| 20 | #include <Foundation/tFundamentals.h>  |
| 21 | #include "System/tCmdLine.h"  |
| 22 | #include "System/tFile.h"  |
| 23 |   |
| 24 |   |
| 25 | namespace tCmdLine  |
| 26 | {  |
| 27 | // Any single-hyphen combined option arguments are expanded here. Ex. -abc becomes -a -b -c.  |
| 28 | void ExpandArgs(tList<tStringItem>& args);  |
| 29 | int IndentSpaces(tString* dest, int numSpaces);  |
| 30 |   |
| 31 | void SyntaxInternal(tString* dest, int width);  |
| 32 | void VersionAuthorInternal(tString& verAuthDest, const char8_t* author, int major, int minor = -1, int revision = -1);  |
| 33 | void UsageInternal(tString* dest, bool indent, const char8_t* verAuth, const char8_t* desc);  |
| 34 |   |
| 35 | // I'm relying on zero initialization here. It's all zeroes before any items are constructed.  |
| 36 | tList<tParam> Params(tListMode::StaticZero);  |
| 37 | tList<tOption> Options(tListMode::StaticZero);  |
| 38 | tString Program;  |
| 39 | tString Empty;  |
| 40 | }  |
| 41 |   |
| 42 |   |
| 43 | tString tCmdLine::tGetProgram()  |
| 44 | {  |
| 45 | return Program;  |
| 46 | }  |
| 47 |   |
| 48 |   |
| 49 | tCmdLine::tParam::tParam(int paramNumber, const char* name, const char* description, bool exclude) :  |
| 50 | ParamNumber(paramNumber),  |
| 51 | Values(tListMode::ListOwns),  |
| 52 | Name(),  |
| 53 | Description(),  |
| 54 | ExcludeFromUsage(exclude)  |
| 55 | {  |
| 56 | tAssert(ParamNumber >= 0);  |
| 57 | if (name)  |
| 58 | Name.Set(name);  |
| 59 | else  |
| 60 | tsPrintf(dest&: Name, format: "Param%d" , paramNumber);  |
| 61 |   |
| 62 | if (description)  |
| 63 | Description = tString(description);  |
| 64 |   |
| 65 | Params.Append(item: this);  |
| 66 | }  |
| 67 |   |
| 68 |   |
| 69 | tCmdLine::tOption::tOption(const char* description, char shortName, const char* longName, int numArgs, bool exclude) :  |
| 70 | ShortName(shortName),  |
| 71 | LongName(longName),  |
| 72 | Description(description),  |
| 73 | NumArgsPerOption(numArgs),  |
| 74 | Args(tListMode::ListOwns),  |
| 75 | Present(false),  |
| 76 | ExcludeFromUsage(exclude)  |
| 77 | {  |
| 78 | Options.Append(item: this);  |
| 79 | }  |
| 80 |   |
| 81 |   |
| 82 | tCmdLine::tOption::tOption(const char* description, const char* longName, char shortName, int numArgs, bool exclude) :  |
| 83 | ShortName(shortName),  |
| 84 | LongName(longName),  |
| 85 | Description(description),  |
| 86 | NumArgsPerOption(numArgs),  |
| 87 | Args(tListMode::ListOwns),  |
| 88 | Present(false),  |
| 89 | ExcludeFromUsage(exclude)  |
| 90 | {  |
| 91 | Options.Append(item: this);  |
| 92 | }  |
| 93 |   |
| 94 |   |
| 95 | tCmdLine::tOption::tOption(const char* description, char shortName, int numArgs, bool exclude) :  |
| 96 | ShortName(shortName),  |
| 97 | LongName(),  |
| 98 | Description(description),  |
| 99 | NumArgsPerOption(numArgs),  |
| 100 | Args(tListMode::ListOwns),  |
| 101 | Present(false),  |
| 102 | ExcludeFromUsage(exclude)  |
| 103 | {  |
| 104 | Options.Append(item: this);  |
| 105 | }  |
| 106 |   |
| 107 |   |
| 108 | tCmdLine::tOption::tOption(const char* description, const char* longName, int numArgs, bool exclude) :  |
| 109 | ShortName(),  |
| 110 | LongName(longName),  |
| 111 | Description(description),  |
| 112 | NumArgsPerOption(numArgs),  |
| 113 | Args(tListMode::ListOwns),  |
| 114 | Present(false),  |
| 115 | ExcludeFromUsage(exclude)  |
| 116 | {  |
| 117 | Options.Append(item: this);  |
| 118 | }  |
| 119 |   |
| 120 |   |
| 121 | int tCmdLine::IndentSpaces(tString* dest, int numSpaces)  |
| 122 | {  |
| 123 | for (int s = 0; s < numSpaces; s++)  |
| 124 | tsaPrintf(dest, format: " " );  |
| 125 |   |
| 126 | return numSpaces;  |
| 127 | }  |
| 128 |   |
| 129 |   |
| 130 | const tString& tCmdLine::tOption::ArgN(int n) const  |
| 131 | {  |
| 132 | for (tStringItem* arg = Args.First(); arg; arg = arg->Next(), n--)  |
| 133 | if (n <= 1)  |
| 134 | return *arg;  |
| 135 |   |
| 136 | return Empty;  |
| 137 | }  |
| 138 |   |
| 139 |   |
| 140 | void tCmdLine::tParse(int argc, char** argv, bool sortOptions)  |
| 141 | {  |
| 142 | if (argc <= 0)  |
| 143 | return;  |
| 144 |   |
| 145 | // Create a single line string of all the separate argv's. Arguments with quotes and spaces will come in as  |
| 146 | // distinct argv's, but they all get combined here. I don't believe two consecutive spaces will work.  |
| 147 | tString line;  |
| 148 | for (int a = 0; a < argc; a++)  |
| 149 | {  |
| 150 | char* arg = argv[a];  |
| 151 | if (!arg || (tStd::tStrlen(s: arg) == 0))  |
| 152 | continue;  |
| 153 |   |
| 154 | // Arg may have spaces within it. Such arguments need to be enclosed in quotes.  |
| 155 | tString argStr(arg);  |
| 156 | if (argStr.FindChar(c: ' ') != -1)  |
| 157 | argStr = tString("\"" ) + argStr + tString("\"" );  |
| 158 |   |
| 159 | line += argStr;  |
| 160 | if (a < (argc - 1))  |
| 161 | line += " " ;  |
| 162 | }  |
| 163 |   |
| 164 | tParse(commandLine: line, fullCommandLine: true, sortOptions);  |
| 165 | }  |
| 166 |   |
| 167 |   |
| 168 | void tCmdLine::tParse(int argc, char16_t** argv, bool sortOptions)  |
| 169 | {  |
| 170 | if (argc <= 0)  |
| 171 | return;  |
| 172 |   |
| 173 | // Create a single line string of all the separate argv's. Arguments with quotes and spaces will come in as  |
| 174 | // distinct argv's, but they all get combined here. I don't believe two consecutive spaces will work.  |
| 175 | tString line;  |
| 176 | for (int a = 0; a < argc; a++)  |
| 177 | {  |
| 178 | char16_t* arg16 = argv[a];  |
| 179 | if (!arg16)  |
| 180 | continue;  |
| 181 |   |
| 182 | // Arg may have spaces within it. Such arguments need to be enclosed in quotes.  |
| 183 | // We can construct a UTF-8 tString from a UTF-16 char16_t pointer.  |
| 184 | tString argStr(arg16);  |
| 185 | if (argStr.IsEmpty())  |
| 186 | continue;  |
| 187 |   |
| 188 | if (argStr.FindChar(c: ' ') != -1)  |
| 189 | argStr = tString("\"" ) + argStr + tString("\"" );  |
| 190 |   |
| 191 | line += argStr;  |
| 192 | if (a < (argc - 1))  |
| 193 | line += " " ;  |
| 194 | }  |
| 195 |   |
| 196 | tParse(commandLine: line, fullCommandLine: true, sortOptions);  |
| 197 | }  |
| 198 |   |
| 199 |   |
| 200 | #ifdef PLATFORM_WINDOWS  |
| 201 | void tCmdLine::tParse(int argc, wchar_t** argv, bool sortOptions)  |
| 202 | {  |
| 203 | tParse(argc, (char16_t**)argv, sortOptions);  |
| 204 | }  |
| 205 | #endif  |
| 206 |   |
| 207 |   |
| 208 | void tCmdLine::ExpandArgs(tList<tStringItem>& args)  |
| 209 | {  |
| 210 | tList<tStringItem> expArgs(tListMode::ListOwns);  |
| 211 | while (tStringItem* arg = args.Remove())  |
| 212 | {  |
| 213 | if ((arg->Length() < 2) || ((*arg)[0] != '-') || (((*arg)[0] == '-') && ((*arg)[1] == '-')))  |
| 214 | {  |
| 215 | expArgs.Append(item: arg);  |
| 216 | continue;  |
| 217 | }  |
| 218 | // It's now a single hyphen with something after it.  |
| 219 |   |
| 220 | bool recognized = false;  |
| 221 | for (tOption* option = Options.First(); option; option = option->Next())  |
| 222 | {  |
| 223 | if ( option->ShortName == tString((*arg)[1]) )  |
| 224 | {  |
| 225 | recognized = true;  |
| 226 | break;  |
| 227 | }  |
| 228 | }  |
| 229 |   |
| 230 | // Unrecognized options are left unmodified. Means you can put -10 and have it treated as a parameter.  |
| 231 | // as long as you don't have an option with shortname "1".  |
| 232 | if (!recognized)  |
| 233 | {  |
| 234 | expArgs.Append(item: arg);  |
| 235 | continue;  |
| 236 | }  |
| 237 |   |
| 238 | // By now it's a single hyphen and is expandble. e.g. -abc -> -a -b -c  |
| 239 | for (int flag = 1; flag < arg->Length(); flag++)  |
| 240 | {  |
| 241 | tString newArg = "-" + tString((*arg)[flag]);  |
| 242 | expArgs.Append(item: new tStringItem(newArg));  |
| 243 | }  |
| 244 |   |
| 245 | delete arg;  |
| 246 | }  |
| 247 |   |
| 248 | // Repopulate args.  |
| 249 | while (tStringItem* arg = expArgs.Remove())  |
| 250 | args.Append(item: arg);  |
| 251 | }  |
| 252 |   |
| 253 |   |
| 254 | static bool ParamSortFn(const tCmdLine::tParam& a, const tCmdLine::tParam& b)  |
| 255 | {  |
| 256 | return (a.ParamNumber < b.ParamNumber);  |
| 257 | }  |
| 258 |   |
| 259 |   |
| 260 | static bool OptionSortFnShort(const tCmdLine::tOption& a, const tCmdLine::tOption& b)  |
| 261 | {  |
| 262 | return tStd::tStrcmp(a: a.ShortName.Pod(), b: b.ShortName.Pod()) < 0;  |
| 263 | }  |
| 264 |   |
| 265 |   |
| 266 | static bool OptionSortFnLong(const tCmdLine::tOption& a, const tCmdLine::tOption& b)  |
| 267 | {  |
| 268 | return tStd::tStrcmp(a: a.LongName.Pod(), b: b.LongName.Pod()) < 0;  |
| 269 | }  |
| 270 |   |
| 271 |   |
| 272 | void tCmdLine::tParse(const char8_t* commandLine, bool fullCommandLine, bool sortOptions)  |
| 273 | {  |
| 274 | // At this point the constructors for all tOptions and tParams will have been called and both Params and Options  |
| 275 | // lists are populated. Options can be specified in any order. By default we are going to order them alphabetically  |
| 276 | // by short flag name so they get printed nicely by tPrintUsage. Params must be printed in order based on their param num  |
| 277 | // so we'll do that sort here too. Param sorting cannot be disabled.  |
| 278 | Params.Sort(compare: ParamSortFn);  |
| 279 | if (sortOptions)  |
| 280 | {  |
| 281 | Options.Sort(compare: OptionSortFnShort);  |
| 282 | Options.Sort(compare: OptionSortFnLong);  |
| 283 | }  |
| 284 |   |
| 285 | tString line(commandLine);  |
| 286 |   |
| 287 | // Mark both kinds of escaped quotes that may be present. These may be found when the caller  |
| 288 | // wants a quote inside a string on the command line.  |
| 289 | line.Replace(search: u8"^'" , replace: tStd::u8SeparatorAStr); // Replaces ^'  |
| 290 | line.Replace(search: u8"^\"" , replace: tStd::u8SeparatorBStr); // Replaces ^"  |
| 291 | line.Replace(search: u8"^^" , replace: tStd::u8SeparatorCStr); // Replaces ^^  |
| 292 |   |
| 293 | // Mark the spaces and hyphens inside normal (non escaped) quotes.  |
| 294 | bool inside = false;  |
| 295 | for (char8_t* ch = line.Text(); *ch; ch++)  |
| 296 | {  |
| 297 | if ((*ch == '\'') || (*ch == '\"'))  |
| 298 | inside = !inside;  |
| 299 |   |
| 300 | if (!inside)  |
| 301 | continue;  |
| 302 |   |
| 303 | if (*ch == ' ')  |
| 304 | *ch = tStd::SeparatorD;  |
| 305 |   |
| 306 | if (*ch == '-')  |
| 307 | *ch = tStd::SeparatorE;  |
| 308 | }  |
| 309 |   |
| 310 | line.Remove(rem: '\'');  |
| 311 | line.Remove(rem: '\"');  |
| 312 |   |
| 313 | tList<tStringItem> args(tListMode::ListOwns);  |
| 314 | tStd::tExplode(components&: args, src: line, divider: ' ');  |
| 315 |   |
| 316 | // Now that the arguments are exploded into separate elements we replace the separators with the correct characters.  |
| 317 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
| 318 | {  |
| 319 | arg->Replace(search: tStd::SeparatorA, replace: '\'');  |
| 320 | arg->Replace(search: tStd::SeparatorB, replace: '\"');  |
| 321 | arg->Replace(search: tStd::SeparatorC, replace: '^');  |
| 322 | arg->Replace(search: tStd::SeparatorD, replace: ' ');  |
| 323 | }  |
| 324 |   |
| 325 | // Set the program name as typed in the command line.  |
| 326 | if (fullCommandLine)  |
| 327 | {  |
| 328 | tStringItem* prog = args.Remove();  |
| 329 | Program.Set(prog->Chars());  |
| 330 | delete prog;  |
| 331 | }  |
| 332 | else  |
| 333 | {  |
| 334 | Program.Clear();  |
| 335 | }  |
| 336 |   |
| 337 | ExpandArgs(args);  |
| 338 |   |
| 339 | // Process all options. If there is more than one tOption that uses the same names, they all need to  |
| 340 | // be populated. That's why we need to loop through all arguments for each tOption.  |
| 341 | for (tOption* option = Options.First(); option; option = option->Next())  |
| 342 | {  |
| 343 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
| 344 | {  |
| 345 | if ( (*arg == tString("--" ) + option->LongName) || (*arg == tString("-" ) + option->ShortName) )  |
| 346 | {  |
| 347 | option->Present = true;  |
| 348 | for (int optArgNum = 0; optArgNum < option->NumArgsPerOption; optArgNum++)  |
| 349 | {  |
| 350 | arg = arg->Next();  |
| 351 | tStringItem* argItem = new tStringItem(*arg);  |
| 352 | argItem->Replace(search: tStd::SeparatorE, replace: '-');  |
| 353 | option->Args.Append(item: argItem);  |
| 354 | }  |
| 355 | }  |
| 356 | }  |
| 357 | }  |
| 358 |   |
| 359 | // Now we're going to create a list of just the parameters by skipping any options as we encounter them.  |
| 360 | // For any option that we know about we'll also have to skip its option arguments.  |
| 361 | tList<tStringItem> commandLineParams(tListMode::ListOwns);  |
| 362 | for (tStringItem* arg = args.First(); arg; arg = arg->Next())  |
| 363 | {  |
| 364 | tStringItem* candidate = arg;  |
| 365 |   |
| 366 | // This loop skips any options for the current arg.  |
| 367 | for (tOption* option = Options.First(); option; option = option->Next())  |
| 368 | {  |
| 369 | if (*(arg->Text()) == '-')  |
| 370 | {  |
| 371 | tString optArg = *arg;  |
| 372 |   |
| 373 | // We only skip options we recognize.  |
| 374 | if ( (optArg == tString("--" ) + option->LongName) || (optArg == tString("-" ) + option->ShortName) )  |
| 375 | {  |
| 376 | candidate = nullptr;  |
| 377 | for (int optArgNum = 0; optArgNum < option->NumArgsPerOption; optArgNum++)  |
| 378 | arg = arg->Next();  |
| 379 | }  |
| 380 | }  |
| 381 | }  |
| 382 |   |
| 383 | if (candidate)  |
| 384 | {  |
| 385 | tStringItem* cmdArg = new tStringItem(*candidate);  |
| 386 | cmdArg->Replace(search: tStd::SeparatorE, replace: '-');  |
| 387 | commandLineParams.Append(item: cmdArg);  |
| 388 | }  |
| 389 | }  |
| 390 |   |
| 391 | // Process all parameters. Note again that similarly to tOptions, we need to loop through all commandLineParams  |
| 392 | // arguments for every tParam. This is because more than one tParam may need to collect the same arg. In fact some  |
| 393 | // tParams may have their param number set to 0, in which case they (all) need to collect all parameter arguments.  |
| 394 | for (tParam* param = Params.First(); param; param = param->Next())  |
| 395 | {  |
| 396 | int paramNumber = 1;  |
| 397 | for (tStringItem* arg = commandLineParams.First(); arg; arg = arg->Next(), paramNumber++)  |
| 398 | {  |
| 399 | if ((param->ParamNumber == paramNumber) || (param->ParamNumber == 0))  |
| 400 | {  |
| 401 | param->Values.Append(item: new tStringItem(*arg));  |
| 402 | }  |
| 403 | }  |
| 404 | }  |
| 405 | }  |
| 406 |   |
| 407 |   |
| 408 | void tCmdLine::SyntaxInternal(tString* dest, int width)  |
| 409 | {  |
| 410 | tString syntaxRaw =  |
| 411 | R"SYNTAX(Command Line Syntax Guide   |
| 412 | program [arg1 arg2 arg3 ...]   |
| 413 |   |
| 414 | ARGUMENTS:   |
| 415 | Arguments are separated by spaces. An argument must be enclosed in quotes (single or double) if it has spaces in it or you want the argument to start with a hyphen literal. Hat (^) escape sequences can be used to put either type of quote inside. If you need to specify file paths you may use forward or back slashes. An ARGUMENT is either an OPTION or PARAMETER.   |
| 416 |   |
| 417 | OPTIONS:   |
| 418 | An option is simply an argument that starts with a hyphen (-). An option has a short syntax and a long syntax. Short syntax is a - followed by a single non-hyphen character. The long form is -- followed by a word. All options support either long, short, or both forms. Options may have 0 or more arguments separated by spaces. Options can be specified in any order. Short form options may be combined: e.g. -al expands to -a -l.   |
| 419 |   |
| 420 | FLAGS:   |
| 421 | If an option takes zero arguments it is called a flag. You can only test for a FLAG's presence or lack thereof.   |
| 422 |   |
| 423 | PARAMETERS:   |
| 424 | A parameter is simply an argument that is not one of the available options. It can be read as a string and parsed however is needed (converted to an integer, float etc.) Order is important when specifying parameters. If you need a hyphen in a parameter at the start you will need put the parameter in quotes. For example, a filename _can_ start with -. Note that arguments that start with a hyphen but are not recognized as a valid option just get turned into parameters. This means interpreting a hyphen directly instead of as an option specifier will happen automatically if there are no options matching what comes after the hyphen. e.g. 'tool -.85 --add 33 -87.98 --notpresent' work just fine as long as there are no options that have a short form with digits or a decimal. In this example the -.85 will be the first parameter, --notpresent will be the second. The --add is assumed to take in two number arguments.   |
| 425 |   |
| 426 | ESCAPES:   |
| 427 | Sometimes you need a particular character to appear inside an argument. For example you may need a single or double quote to apprear inside a parameter. The hat (^) followed by the character you need is used for this purpose.   |
| 428 | e.g. ^^ yields ^ | ^' yields ' | ^" yields "   |
| 429 |   |
| 430 | VARIABLE ARGUMENTS:   |
| 431 | A variable number of space-separated parameters may be specified if the tool supports them. The parsing system will collect them all up if the parameter number is unset (-1).   |
| 432 | A variable number of option arguments is not directly supported due to the more complex parsing that would be needed. The same result is achieved by entering the same option more than once. Order is preserved. This can also be done with options that take more than one argument.   |
| 433 | e.g. tool -I /patha/include/ -I /pathb/include   |
| 434 |   |
| 435 | EXAMPLE:   |
| 436 | mycopy -R --overwrite fileA.txt -pat fileB.txt --log log.txt   |
| 437 |   |
| 438 | The fileA.txt and fileB.txt in the above example are parameters (assuming the overwrite option is a flag). fileA.txt is the first parameter and fileB.txt is the second.   |
| 439 |   |
| 440 | The '--log log.txt' is an option with a single argument, log.txt. Flags may be combined. The -pat in the example expands to -p -a -t. It is suggested only to combine flag (boolean) options as only the last option would get any arguments.   |
| 441 | )SYNTAX" ;  |
| 442 |   |
| 443 | // Support column width by processing each line of syntaxRaw and adding to the final syntax string.  |
| 444 | tString syntax;  |
| 445 | while (syntaxRaw.IsValid())  |
| 446 | {  |
| 447 | tString line = syntaxRaw.ExtractLeft(divider: '\n');  |
| 448 | if (line.IsEmpty())  |
| 449 | {  |
| 450 | syntax += "\n" ;  |
| 451 | continue;  |
| 452 | }  |
| 453 |   |
| 454 | // The separator is so we can test if the word we're extracting is the last of the line.  |
| 455 | line += tStd::SeparatorAStr;  |
| 456 | line += " " ;  |
| 457 | int col = 0;  |
| 458 | do  |
| 459 | {  |
| 460 | tString word = line.ExtractLeft(divider: ' ');  |
| 461 | bool lastWord = false;  |
| 462 | if (word.FindChar(c: tStd::SeparatorA, reverse: true) != -1)  |
| 463 | {  |
| 464 | word.RemoveLast();  |
| 465 | lastWord = true;  |
| 466 | }  |
| 467 |   |
| 468 | int wlen = word.Length();  |
| 469 | if (wlen > width)  |
| 470 | {  |
| 471 | // If the word is super long we're stuck. Just add it and move on. We don't break single words.  |
| 472 | syntax += word;  |
| 473 | col = 0;  |
| 474 | continue;  |
| 475 | }  |
| 476 | if (col+wlen <= width)  |
| 477 | {  |
| 478 | syntax += word;  |
| 479 | col += wlen;  |
| 480 |   |
| 481 | // We only add the space after the word if it's not the last one of the input line.  |
| 482 | if (!lastWord)  |
| 483 | {  |
| 484 | syntax += " " ;  |
| 485 | col++;  |
| 486 | }  |
| 487 | }  |
| 488 | else  |
| 489 | {  |
| 490 | // No room on the current output line. PPlace the word on the next line and add a space.  |
| 491 | syntax.RemoveLast();  |
| 492 | syntax += "\n" ;  |
| 493 | syntax += word;  |
| 494 | syntax += " " ;  |
| 495 | col = wlen+1;  |
| 496 | }  |
| 497 | }  |
| 498 | while (line.IsValid());  |
| 499 | syntax += "\n" ;  |
| 500 | }  |
| 501 |   |
| 502 | tsaPrintf(dest, format: "%s" , syntax.Pod());  |
| 503 | }  |
| 504 |   |
| 505 |   |
| 506 | void tCmdLine::tPrintSyntax(int width)  |
| 507 | {  |
| 508 | SyntaxInternal(dest: nullptr, width);  |
| 509 | }  |
| 510 |   |
| 511 |   |
| 512 | void tCmdLine::tStringSyntax(tString& dest, int width)  |
| 513 | {  |
| 514 | SyntaxInternal(dest: &dest, width);  |
| 515 | }  |
| 516 |   |
| 517 |   |
| 518 | void tCmdLine::VersionAuthorInternal(tString& verAuth, const char8_t* author, int major, int minor, int revision)  |
| 519 | {  |
| 520 | tAssert(major >= 0);  |
| 521 | tAssert((minor >= 0) || (revision < 0)); // Not allowed a valid revision number if minor is not also valid.  |
| 522 |   |
| 523 | verAuth.Clear();  |
| 524 | tsaPrintf(dest&: verAuth, format: "Version %d" , major);  |
| 525 | if (minor >= 0)  |
| 526 | {  |
| 527 | tsaPrintf(dest&: verAuth, format: ".%d" , minor);  |
| 528 | if (revision >= 0)  |
| 529 | tsaPrintf(dest&: verAuth, format: ".%d" , revision);  |
| 530 | }  |
| 531 |   |
| 532 | if (author)  |
| 533 | tsaPrintf(dest&: verAuth, format: " by %s" , author);  |
| 534 | }  |
| 535 |   |
| 536 |   |
| 537 | void tCmdLine::UsageInternal(tString* dest, bool doIndent, const char8_t* verAuth, const char8_t* desc)  |
| 538 | {  |
| 539 | tString exeName = "program" ;  |
| 540 | if (!tCmdLine::Program.IsEmpty())  |
| 541 | {  |
| 542 | exeName = tSystem::tGetFileName(file: tCmdLine::Program);  |
| 543 | }  |
| 544 | else  |
| 545 | {  |
| 546 | tString fullPath = tSystem::tGetProgramPath();  |
| 547 | tString fileName = tSystem::tGetFileName(file: fullPath);  |
| 548 | if (fileName.IsValid())  |
| 549 | exeName = fileName;  |
| 550 | }  |
| 551 |   |
| 552 | if (verAuth)  |
| 553 | tsaPrintf(dest, format: "%s %s\n\n" , tPod(tSystem::tGetFileBaseName(exeName)), verAuth);  |
| 554 |   |
| 555 | if (Options.IsEmpty())  |
| 556 | tsaPrintf(dest, format: "USAGE: %s " , exeName.Pod());  |
| 557 | else  |
| 558 | tsaPrintf(dest, format: "USAGE: %s [options] " , exeName.Pod());  |
| 559 |   |
| 560 | for (tParam* param = Params.First(); param; param = param->Next())  |
| 561 | {  |
| 562 | if (param->ExcludeFromUsage)  |
| 563 | continue;  |
| 564 |   |
| 565 | if (!param->Name.IsEmpty() && (param->ParamNumber > 0))  |
| 566 | tsaPrintf(dest, format: "%s " , param->Name.Pod());  |
| 567 | else if (!param->Name.IsEmpty() && (param->ParamNumber == 0))  |
| 568 | tsaPrintf(dest, format: "[%s] " , param->Name.Pod());  |
| 569 | else if (param->ParamNumber > 0)  |
| 570 | tsaPrintf(dest, format: "param%d " , param->ParamNumber);  |
| 571 | else  |
| 572 | tsaPrintf(dest, format: "[params] " );  |
| 573 | }  |
| 574 |   |
| 575 | tsaPrintf(dest, format: "\n\n" );  |
| 576 | if (desc)  |
| 577 | {  |
| 578 | tsaPrintf(dest, format: "%s" , desc);  |
| 579 | tsaPrintf(dest, format: "\n\n" );  |
| 580 | }  |
| 581 |   |
| 582 | int indent = 0;  |
| 583 | int numUsageOptions = 0;  |
| 584 | int numUsageParams = 0;  |
| 585 |   |
| 586 | // Loop through both options and parameters to figure out how far to indent.  |
| 587 | for (tOption* option = Options.First(); option; option = option->Next())  |
| 588 | {  |
| 589 | if (option->ExcludeFromUsage)  |
| 590 | continue;  |
| 591 |   |
| 592 | int numPrint = 0;  |
| 593 | if (!option->LongName.IsEmpty())  |
| 594 | numPrint += tcPrintf(format: "--%s " , option->LongName.Pod());  |
| 595 | if (!option->ShortName.IsEmpty())  |
| 596 | numPrint += tcPrintf(format: "-%s " , option->ShortName.Pod());  |
| 597 |   |
| 598 | if (option->NumArgsPerOption <= 2)  |
| 599 | {  |
| 600 | for (int a = 0; a < option->NumArgsPerOption; a++)  |
| 601 | numPrint += tcPrintf(format: "arg%c " , '1'+a);  |
| 602 | }  |
| 603 | else  |
| 604 | {  |
| 605 | numPrint += tcPrintf(format: "[%d args] " , option->NumArgsPerOption);  |
| 606 | }  |
| 607 |   |
| 608 | if (doIndent)  |
| 609 | indent = tMath::tMax(a: indent, b: numPrint);  |
| 610 | numUsageOptions++;  |
| 611 | }  |
| 612 |   |
| 613 | for (tParam* param = Params.First(); param; param = param->Next())  |
| 614 | {  |
| 615 | if (param->ExcludeFromUsage)  |
| 616 | continue;  |
| 617 |   |
| 618 | int numPrint = 0;  |
| 619 | if (!param->Name.IsEmpty() && (param->ParamNumber > 0))  |
| 620 | numPrint = tcPrintf(format: "%s " , param->Name.Pod());  |
| 621 | else if (!param->Name.IsEmpty() && (param->ParamNumber == 0))  |
| 622 | numPrint = tcPrintf(format: "[%s] " , param->Name.Pod());  |
| 623 | else if (param->ParamNumber > 0)  |
| 624 | numPrint = tcPrintf(format: "param%d " , param->ParamNumber);  |
| 625 | else  |
| 626 | numPrint = tcPrintf(format: "[params] " );  |
| 627 |   |
| 628 | if (doIndent)  |
| 629 | indent = tMath::tMax(a: indent, b: numPrint);  |
| 630 | numUsageParams++;  |
| 631 | }  |
| 632 |   |
| 633 | if (numUsageOptions > 0)  |
| 634 | {  |
| 635 | tsaPrintf(dest, format: "Options:\n" );  |
| 636 | for (tOption* option = Options.First(); option; option = option->Next())  |
| 637 | {  |
| 638 | if (option->ExcludeFromUsage)  |
| 639 | continue;  |
| 640 |   |
| 641 | int numPrinted = 0;  |
| 642 | if (!option->LongName.IsEmpty())  |
| 643 | numPrinted += tsaPrintf(dest, format: "--%s " , option->LongName.Pod());  |
| 644 | if (!option->ShortName.IsEmpty())  |
| 645 | numPrinted += tsaPrintf(dest, format: "-%s " , option->ShortName.Pod());  |
| 646 |   |
| 647 | if (option->NumArgsPerOption <= 2)  |
| 648 | {  |
| 649 | for (int a = 0; a < option->NumArgsPerOption; a++)  |
| 650 | numPrinted += tsaPrintf(dest, format: "arg%c " , '1'+a);  |
| 651 | }  |
| 652 | else  |
| 653 | {  |
| 654 | numPrinted += tsaPrintf(dest, format: "[%d args] " , option->NumArgsPerOption);  |
| 655 | }  |
| 656 |   |
| 657 | if (doIndent)  |
| 658 | IndentSpaces(dest, numSpaces: indent-numPrinted);  |
| 659 | tsaPrintf(dest, format: ": %s\n" , option->Description.Pod());  |
| 660 | }  |
| 661 | tsaPrintf(dest, format: "\n" );  |
| 662 | }  |
| 663 |   |
| 664 | if (numUsageParams > 0)  |
| 665 | {  |
| 666 | tsaPrintf(dest, format: "Parameters:\n" );  |
| 667 | for (tParam* param = Params.First(); param; param = param->Next())  |
| 668 | {  |
| 669 | if (param->ExcludeFromUsage)  |
| 670 | continue;  |
| 671 |   |
| 672 | int numPrinted = 0;  |
| 673 | if (!param->Name.IsEmpty() && (param->ParamNumber > 0))  |
| 674 | numPrinted = tsaPrintf(dest, format: "%s " , param->Name.Pod());  |
| 675 | else if (!param->Name.IsEmpty() && (param->ParamNumber == 0))  |
| 676 | numPrinted = tsaPrintf(dest, format: "[%s] " , param->Name.Pod());  |
| 677 | else if (param->ParamNumber > 0)  |
| 678 | numPrinted = tsaPrintf(dest, format: "param%d " , param->ParamNumber);  |
| 679 | else  |
| 680 | numPrinted = tsaPrintf(dest, format: "[params] " );  |
| 681 |   |
| 682 | if (doIndent)  |
| 683 | IndentSpaces(dest, numSpaces: indent - numPrinted);  |
| 684 |   |
| 685 | if (!param->Description.IsEmpty())  |
| 686 | tsaPrintf(dest, format: ": %s" , param->Description.Pod());  |
| 687 | else  |
| 688 | tsaPrintf(dest, format: ": No description" );  |
| 689 |   |
| 690 | tsaPrintf(dest, format: "\n" );  |
| 691 | }  |
| 692 | tsaPrintf(dest, format: "\n" );  |
| 693 | }  |
| 694 | }  |
| 695 |   |
| 696 |   |
| 697 | void tCmdLine::tPrintUsage(int major, int minor, int revision)  |
| 698 | {  |
| 699 | tPrintUsage(author: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 700 | }  |
| 701 |   |
| 702 |   |
| 703 | void tCmdLine::tPrintUsage(const char8_t* author, int major, int minor, int revision)  |
| 704 | {  |
| 705 | tPrintUsage(author, desc: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 706 | }  |
| 707 |   |
| 708 |   |
| 709 | void tCmdLine::tPrintUsage(const char8_t* author, const char8_t* desc, int major, int minor, int revision)  |
| 710 | {  |
| 711 | tString verAuth;  |
| 712 | VersionAuthorInternal(verAuth, author, major, minor, revision);  |
| 713 | tPrintUsage(versionAuthor: verAuth.Pod(), desc);  |
| 714 | }  |
| 715 |   |
| 716 |   |
| 717 | void tCmdLine::tPrintUsage(const char8_t* verAuth, const char8_t* desc)  |
| 718 | {  |
| 719 | UsageInternal(dest: nullptr, doIndent: true, verAuth, desc);  |
| 720 | }  |
| 721 |   |
| 722 |   |
| 723 | void tCmdLine::tStringUsage(tString& dest, int major, int minor, int revision)  |
| 724 | {  |
| 725 | tStringUsage(dest, author: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 726 | }  |
| 727 |   |
| 728 |   |
| 729 | void tCmdLine::tStringUsage(tString& dest, const char8_t* author, int major, int minor, int revision)  |
| 730 | {  |
| 731 | tStringUsage(dest, author, desc: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 732 | }  |
| 733 |   |
| 734 |   |
| 735 | void tCmdLine::tStringUsage(tString& dest, const char8_t* author, const char8_t* desc, int major, int minor, int revision)  |
| 736 | {  |
| 737 | tString verAuth;  |
| 738 | VersionAuthorInternal(verAuth, author, major, minor, revision);  |
| 739 | tStringUsage(dest, versionAuthor: verAuth.Pod(), desc);  |
| 740 | }  |
| 741 |   |
| 742 |   |
| 743 | void tCmdLine::tStringUsage(tString& dest, const char8_t* verAuth, const char8_t* desc)  |
| 744 | {  |
| 745 | UsageInternal(dest: &dest, doIndent: true, verAuth, desc);  |
| 746 | }  |
| 747 |   |
| 748 |   |
| 749 | void tCmdLine::tStringUsageNI(tString& dest, int major, int minor, int revision)  |
| 750 | {  |
| 751 | tStringUsageNI(dest, author: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 752 | }  |
| 753 |   |
| 754 |   |
| 755 | void tCmdLine::tStringUsageNI(tString& dest, const char8_t* author, int major, int minor, int revision)  |
| 756 | {  |
| 757 | tStringUsageNI(dest, author, desc: nullptr, versionMajor: major, versionMinor: minor, versionRevision: revision);  |
| 758 | }  |
| 759 |   |
| 760 |   |
| 761 | void tCmdLine::tStringUsageNI(tString& dest, const char8_t* author, const char8_t* desc, int major, int minor, int revision)  |
| 762 | {  |
| 763 | tString verAuth;  |
| 764 | VersionAuthorInternal(verAuth, author, major, minor, revision);  |
| 765 | tStringUsageNI(dest, versionAuthor: verAuth.Pod(), desc);  |
| 766 | }  |
| 767 |   |
| 768 |   |
| 769 | void tCmdLine::tStringUsageNI(tString& dest, const char8_t* verAuth, const char8_t* desc)  |
| 770 | {  |
| 771 | UsageInternal(dest: &dest, doIndent: false, verAuth, desc);  |
| 772 | }  |
| 773 | |