| 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 | |