C++ Utilities  4.12.1
Useful C++ classes and routines such as argument parser, IO and conversion utilities
argumentparsertests.cpp
Go to the documentation of this file.
1 #include "./outputcheck.h"
2 #include "./testutils.h"
3 
4 #include "../conversion/stringbuilder.h"
5 
6 #include "../application/argumentparser.h"
7 #include "../application/argumentparserprivate.h"
8 #include "../application/commandlineutils.h"
9 #include "../application/failure.h"
10 #include "../application/fakeqtconfigarguments.h"
11 
12 #include "../io/ansiescapecodes.h"
13 #include "../io/path.h"
14 
15 #include "resources/config.h"
16 
17 #include <cppunit/TestFixture.h>
18 #include <cppunit/extensions/HelperMacros.h>
19 
20 #include <cstdlib>
21 #include <cstring>
22 
23 using namespace std;
24 using namespace ApplicationUtilities;
25 using namespace ConversionUtilities;
26 using namespace TestUtilities;
27 using namespace TestUtilities::Literals;
28 
29 using namespace CPPUNIT_NS;
30 
34 class ArgumentParserTests : public TestFixture {
35  CPPUNIT_TEST_SUITE(ArgumentParserTests);
36  CPPUNIT_TEST(testArgument);
37  CPPUNIT_TEST(testParsing);
38  CPPUNIT_TEST(testCallbacks);
39  CPPUNIT_TEST(testBashCompletion);
40  CPPUNIT_TEST(testHelp);
41  CPPUNIT_TEST(testSetMainArguments);
42  CPPUNIT_TEST(testNoColorArgument);
43  CPPUNIT_TEST_SUITE_END();
44 
45 public:
46  void setUp();
47  void tearDown();
48 
49  void testArgument();
50  void testParsing();
51  void testCallbacks();
52  void testBashCompletion();
53  void testHelp();
54  void testSetMainArguments();
55  void testNoColorArgument();
56 
57 private:
58  void callback();
59 };
60 
62 
64 {
65 }
66 
68 {
69 }
70 
75 {
76  Argument argument("test", 't', "some description");
77  CPPUNIT_ASSERT_EQUAL(argument.isRequired(), false);
78  argument.setConstraints(1, 10);
79  CPPUNIT_ASSERT_EQUAL(argument.isRequired(), true);
80  Argument subArg("sub", 's', "sub arg");
81  argument.addSubArgument(&subArg);
82  CPPUNIT_ASSERT_EQUAL(subArg.parents().at(0), &argument);
83  CPPUNIT_ASSERT(!subArg.conflictsWithArgument());
84  CPPUNIT_ASSERT(!argument.firstValue());
85  argument.setEnvironmentVariable("FOO_ENV_VAR");
86  setenv("FOO_ENV_VAR", "foo", true);
87  CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "foo"));
88  ArgumentOccurrence occurrence(0, vector<Argument *>(), nullptr);
89  occurrence.values.emplace_back("bar");
90  argument.m_occurrences.emplace_back(move(occurrence));
91  CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "bar"));
92 }
93 
98 {
99  // setup parser with some test argument definitions
100  ArgumentParser parser;
102  QT_CONFIG_ARGUMENTS qtConfigArgs;
103  HelpArgument helpArg(parser);
104  Argument verboseArg("verbose", 'v', "be verbose");
105  verboseArg.setCombinable(true);
106  Argument fileArg("file", 'f', "specifies the path of the file to be opened");
107  fileArg.setValueNames({ "path" });
108  fileArg.setRequiredValueCount(1);
109  fileArg.setEnvironmentVariable("PATH");
110  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
111  filesArg.setValueNames({ "path 1", "path 2" });
112  filesArg.setRequiredValueCount(Argument::varValueCount);
113  Argument outputFileArg("output-file", 'o', "specifies the path of the output file");
114  outputFileArg.setValueNames({ "path" });
115  outputFileArg.setRequiredValueCount(1);
116  outputFileArg.setRequired(true);
117  outputFileArg.setCombinable(true);
118  Argument printFieldNamesArg("print-field-names", '\0', "prints available field names");
119  Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
120  Argument notAlbumArg("album", 'a', "should not be confused with album value");
121  displayFileInfoArg.setDenotesOperation(true);
122  displayFileInfoArg.setSubArguments({ &fileArg, &verboseArg, &notAlbumArg });
123  Argument fieldsArg("fields", '\0', "specifies the fields");
124  fieldsArg.setRequiredValueCount(Argument::varValueCount);
125  fieldsArg.setValueNames({ "title", "album", "artist", "trackpos" });
126  fieldsArg.setImplicit(true);
127  Argument displayTagInfoArg("get", 'p', "displays the values of all specified tag fields (displays all fields if none specified)");
128  displayTagInfoArg.setDenotesOperation(true);
129  displayTagInfoArg.setSubArguments({ &fieldsArg, &filesArg, &verboseArg, &notAlbumArg });
130  NoColorArgument noColorArg;
131  parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &helpArg, &noColorArg });
132 
133  // no args present
134  parser.parseArgs(0, nullptr);
135  CPPUNIT_ASSERT(!parser.executable());
136  CPPUNIT_ASSERT(!parser.specifiedOperation());
137  CPPUNIT_ASSERT_EQUAL(0u, parser.actualArgumentCount());
138 
139  // error about uncombinable arguments
140  const char *argv[] = { "tageditor", "get", "album", "title", "diskpos", "-f", "somefile" };
141  // try to parse, this should fail
142  try {
143  parser.parseArgs(7, argv);
144  CPPUNIT_FAIL("Exception expected.");
145  } catch (const Failure &e) {
146  CPPUNIT_ASSERT_EQUAL("The argument \"files\" can not be combined with \"fields\"."s, string(e.what()));
147  // test printing btw
148  stringstream ss;
149  ss << e;
150  CPPUNIT_ASSERT_EQUAL(
151  "Error: Unable to parse arguments: The argument \"files\" can not be combined with \"fields\".\nSee --help for available commands.\n"s,
152  ss.str());
153  }
154  CPPUNIT_ASSERT(parser.isUncombinableMainArgPresent());
155 
156  // arguments read correctly after successful parse
157  filesArg.setCombinable(true);
158  parser.resetArgs();
159  parser.parseArgs(7, argv);
160  // check results
161  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
162  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
163  CPPUNIT_ASSERT(!strcmp(parser.executable(), "tageditor"));
164  CPPUNIT_ASSERT(!verboseArg.isPresent());
165  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
166  CPPUNIT_ASSERT(fieldsArg.isPresent());
167  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album"));
168  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
169  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
170  CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
171  CPPUNIT_ASSERT_EQUAL(&displayTagInfoArg, parser.specifiedOperation());
172 
173  // skip empty args
174  const char *argv2[] = { "tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile" };
175  // reparse the args
176  parser.resetArgs();
177  parser.parseArgs(9, argv2);
178  // check results again
179  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
180  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
181  CPPUNIT_ASSERT(!verboseArg.isPresent());
182  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
183  CPPUNIT_ASSERT(fieldsArg.isPresent());
184  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album"));
185  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
186  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
187  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(3), ""));
188  CPPUNIT_ASSERT_THROW(fieldsArg.values().at(4), out_of_range);
189  CPPUNIT_ASSERT(filesArg.isPresent());
190  CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
191 
192  // error about unknown argument: forget get/-p
193  const char *argv3[] = { "tageditor", "album", "title", "diskpos", "--files", "somefile" };
194  try {
195  parser.resetArgs();
196  parser.parseArgs(6, argv3);
197  CPPUNIT_FAIL("Exception expected.");
198  } catch (const Failure &e) {
199  CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown."s, string(e.what()));
200  }
201 
202  // warning about unknown argument
203  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
204  // redirect stderr to check whether warnings are printed correctly
205  stringstream buffer;
206  streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
207  parser.resetArgs();
208  EscapeCodes::enabled = false;
209  try {
210  parser.parseArgs(6, argv3);
211  } catch (...) {
212  cerr.rdbuf(regularCerrBuffer);
213  throw;
214  }
215  cerr.rdbuf(regularCerrBuffer);
216  CPPUNIT_ASSERT_EQUAL("Warning: The specified argument \"album\" is unknown and will be ignored.\n"s
217  "Warning: The specified argument \"title\" is unknown and will be ignored.\n"s
218  "Warning: The specified argument \"diskpos\" is unknown and will be ignored.\n"s
219  "Warning: The specified argument \"--files\" is unknown and will be ignored.\n"s
220  "Warning: The specified argument \"somefile\" is unknown and will be ignored.\n"s,
221  buffer.str());
222  // none of the arguments should be present now
223  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
224  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
225  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
226  CPPUNIT_ASSERT(!fieldsArg.isPresent());
227  CPPUNIT_ASSERT(!filesArg.isPresent());
228 
229  // combined abbreviations like "-vf"
230  const char *argv4[] = { "tageditor", "-i", "-vf", "test" };
231  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail);
232  parser.resetArgs();
233  parser.parseArgs(4, argv4);
234  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
235  CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
236  CPPUNIT_ASSERT(verboseArg.isPresent());
237  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
238  CPPUNIT_ASSERT(!filesArg.isPresent());
239  CPPUNIT_ASSERT(fileArg.isPresent());
240  CPPUNIT_ASSERT(!strcmp(fileArg.values().at(0), "test"));
241  CPPUNIT_ASSERT_THROW(fileArg.values().at(1), out_of_range);
242 
243  // constraint checking: no multiple occurrences (not resetting verboseArg on purpose)
244  displayFileInfoArg.reset(), fileArg.reset();
245  try {
246  parser.parseArgs(4, argv4);
247  CPPUNIT_FAIL("Exception expected.");
248  } catch (const Failure &e) {
249  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
250  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time."));
251  }
252 
253  // constraint checking: no contraint (not resetting verboseArg on purpose)
254  displayFileInfoArg.reset(), fileArg.reset();
255  verboseArg.setConstraints(0, Argument::varValueCount);
256  parser.parseArgs(4, argv4);
257  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
258 
259  // constraint checking: mandatory argument
260  verboseArg.setRequired(true);
261  parser.resetArgs();
262  parser.parseArgs(4, argv4);
263  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
264 
265  // contraint checking: error about missing mandatory argument
266  const char *argv5[] = { "tageditor", "-i", "-f", "test" };
267  displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
268  try {
269  parser.parseArgs(4, argv5);
270  CPPUNIT_FAIL("Exception expected.");
271  } catch (const Failure &e) {
272  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
273  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time."));
274  }
275  verboseArg.setRequired(false);
276 
277  // combined abbreviation with nesting "-pf"
278  const char *argv10[] = { "tageditor", "-pf", "test" };
279  parser.resetArgs();
280  parser.parseArgs(3, argv10);
281  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
282  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
283  CPPUNIT_ASSERT(!fileArg.isPresent());
284  CPPUNIT_ASSERT(filesArg.isPresent());
285  CPPUNIT_ASSERT_EQUAL(1_st, filesArg.values(0).size());
286  CPPUNIT_ASSERT(!strcmp(filesArg.values(0).front(), "test"));
287  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
288 
289  // constraint checking: no complains about missing -i
290  const char *argv6[] = { "tageditor", "-g" };
291  parser.resetArgs();
292  parser.parseArgs(2, argv6);
293  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
294 
295  // constraint checking: dependend arguments (-f requires -i or -p)
296  const char *argv7[] = { "tageditor", "-f", "test" };
297  parser.resetArgs();
298  try {
299  parser.parseArgs(3, argv7);
300  CPPUNIT_FAIL("Exception expected.");
301  } catch (const Failure &e) {
302  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
303  CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown."s, string(e.what()));
304  }
305 
306  // equation sign syntax
307  const char *argv11[] = { "tageditor", "-if=test-v" };
308  parser.resetArgs();
309  parser.parseArgs(2, argv11);
310  CPPUNIT_ASSERT(!filesArg.isPresent());
311  CPPUNIT_ASSERT(fileArg.isPresent());
312  CPPUNIT_ASSERT(!verboseArg.isPresent());
313  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
314  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
315  const char *argv15[] = { "tageditor", "-i", "--file=test", "-v" };
316  parser.resetArgs();
317  parser.parseArgs(4, argv15);
318  CPPUNIT_ASSERT(!filesArg.isPresent());
319  CPPUNIT_ASSERT(fileArg.isPresent());
320  CPPUNIT_ASSERT(verboseArg.isPresent());
321  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
322  CPPUNIT_ASSERT_EQUAL("test"s, string(fileArg.values(0).front()));
323 
324  // specifying value directly after abbreviation
325  const char *argv12[] = { "tageditor", "-iftest" };
326  parser.resetArgs();
327  parser.parseArgs(2, argv12);
328  CPPUNIT_ASSERT(!filesArg.isPresent());
329  CPPUNIT_ASSERT(fileArg.isPresent());
330  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
331  CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test"));
332 
333  // specifying top-level argument after abbreviation
334  const char *argv17[] = { "tageditor", "-if=test-v", "--no-color" };
335  parser.resetArgs();
336  parser.parseArgs(3, argv17);
337  CPPUNIT_ASSERT(!filesArg.isPresent());
338  CPPUNIT_ASSERT(fileArg.isPresent());
339  CPPUNIT_ASSERT(!verboseArg.isPresent());
340  CPPUNIT_ASSERT(noColorArg.isPresent());
341  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
342  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
343 
344  // default argument
345  const char *argv8[] = { "tageditor" };
346  parser.resetArgs();
347  parser.parseArgs(1, argv8);
348  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
349  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
350  CPPUNIT_ASSERT(!verboseArg.isPresent());
351  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
352  CPPUNIT_ASSERT(!filesArg.isPresent());
353  CPPUNIT_ASSERT(!fileArg.isPresent());
354  if (getenv("PATH")) {
355  CPPUNIT_ASSERT(fileArg.firstValue());
356  CPPUNIT_ASSERT(!strcmp(fileArg.firstValue(), getenv("PATH")));
357  } else {
358  CPPUNIT_ASSERT(!fileArg.firstValue());
359  }
360 
361  // constraint checking: required value count with sufficient number of provided parameters
362  const char *argv13[] = { "tageditor", "get", "--fields", "album=test", "title", "diskpos", "--files", "somefile" };
363  verboseArg.setRequired(false);
364  parser.resetArgs();
365  parser.parseArgs(8, argv13);
366  // this should still work without complaints
367  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
368  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
369  CPPUNIT_ASSERT(!verboseArg.isPresent());
370  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
371  CPPUNIT_ASSERT(fieldsArg.isPresent());
372  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
373  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
374  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
375  CPPUNIT_ASSERT_THROW(fieldsArg.values().at(3), out_of_range);
376  CPPUNIT_ASSERT(filesArg.isPresent());
377  CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
378  CPPUNIT_ASSERT(!notAlbumArg.isPresent());
379 
380  // constraint checking: required value count with insufficient number of provided parameters
381  const char *argv9[] = { "tageditor", "-p", "album", "title", "diskpos" };
382  fieldsArg.setRequiredValueCount(4);
383  parser.resetArgs();
384  try {
385  parser.parseArgs(5, argv9);
386  CPPUNIT_FAIL("Exception expected.");
387  } catch (const Failure &e) {
388  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
389  CPPUNIT_ASSERT_EQUAL(
390  "Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos"s,
391  string(e.what()));
392  }
393 
394  // constraint checking: truncated argument not wrongly detected
395  const char *argv16[] = { "tageditor", "--hel", "-p", "album", "title", "diskpos" };
396  fieldsArg.setRequiredValueCount(Argument::varValueCount);
397  parser.resetArgs();
398  try {
399  parser.parseArgs(6, argv16);
400  CPPUNIT_FAIL("Exception expected.");
401  } catch (const Failure &e) {
402  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
403  CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown."s, string(e.what()));
404  }
405 
406  // nested operations
407  const char *argv14[] = { "tageditor", "get", "fields", "album=test", "-f", "somefile" };
408  parser.resetArgs();
409  fieldsArg.setDenotesOperation(true);
410  parser.parseArgs(6, argv14);
411  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
412  CPPUNIT_ASSERT(fieldsArg.isPresent());
413  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
414 
415  // implicit flag still works when argument doesn't denote operation
416  parser.resetArgs();
417  fieldsArg.setDenotesOperation(false);
418  parser.parseArgs(6, argv14);
419  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
420  CPPUNIT_ASSERT(fieldsArg.isPresent());
421  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "fields"));
422  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "album=test"));
423 }
424 
429 {
430  ArgumentParser parser;
431  Argument callbackArg("with-callback", 't', "callback test");
432  callbackArg.setRequiredValueCount(2);
433  callbackArg.setCallback([](const ArgumentOccurrence &occurrence) {
434  CPPUNIT_ASSERT_EQUAL(0_st, occurrence.index);
435  CPPUNIT_ASSERT(occurrence.path.empty());
436  CPPUNIT_ASSERT_EQUAL(2_st, occurrence.values.size());
437  CPPUNIT_ASSERT(!strcmp(occurrence.values[0], "val1"));
438  CPPUNIT_ASSERT(!strcmp(occurrence.values[1], "val2"));
439  throw 42;
440  });
441  Argument noCallbackArg("no-callback", 'l', "callback test");
442  noCallbackArg.setRequiredValueCount(2);
443  parser.setMainArguments({ &callbackArg, &noCallbackArg });
444 
445  // test whether callback is invoked when argument with callback is specified
446  const char *argv[] = { "test", "-t", "val1", "val2" };
447  try {
448  parser.parseArgs(4, argv);
449  } catch (int i) {
450  CPPUNIT_ASSERT_EQUAL(i, 42);
451  }
452 
453  // test whether callback is not invoked when argument with callback is not specified
454  callbackArg.reset();
455  const char *argv2[] = { "test", "-l", "val1", "val2" };
456  parser.parseArgs(4, argv2);
457 }
458 
462 static bool exitCalled = false;
463 
469 {
470  ArgumentParser parser;
471  HelpArgument helpArg(parser);
472  Argument verboseArg("verbose", 'v', "be verbose");
473  verboseArg.setCombinable(true);
474  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
475  filesArg.setRequiredValueCount(Argument::varValueCount);
476  filesArg.setCombinable(true);
477  Argument nestedSubArg("nested-sub", '\0', "nested sub arg");
478  Argument subArg("sub", '\0', "sub arg");
479  subArg.setSubArguments({ &nestedSubArg });
480  Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
481  displayFileInfoArg.setDenotesOperation(true);
482  displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg });
483  Argument fieldsArg("fields", '\0', "specifies the fields");
484  fieldsArg.setRequiredValueCount(Argument::varValueCount);
485  fieldsArg.setPreDefinedCompletionValues("title album artist trackpos");
486  fieldsArg.setImplicit(true);
487  Argument valuesArg("values", '\0', "specifies the fields");
488  valuesArg.setRequiredValueCount(Argument::varValueCount);
489  valuesArg.setPreDefinedCompletionValues("title album artist trackpos");
490  valuesArg.setImplicit(false);
491  valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
492  Argument selectorsArg("selectors", '\0', "has some more pre-defined values");
493  selectorsArg.setRequiredValueCount(Argument::varValueCount);
494  selectorsArg.setPreDefinedCompletionValues("tag=id3v1 tag=id3v2 tag=matroska target=file target=track");
495  selectorsArg.setImplicit(false);
496  selectorsArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues);
497  selectorsArg.setCallback(
498  [&selectorsArg](const ArgumentOccurrence &) { selectorsArg.setPreDefinedCompletionValues("tag=matroska tag=mp4 tag=vorbis"); });
499  Argument getArg("get", 'g', "gets tag values");
500  getArg.setSubArguments({ &fieldsArg, &filesArg });
501  Argument setArg("set", 's', "sets tag values");
502  setArg.setSubArguments({ &valuesArg, &filesArg, &selectorsArg });
503 
504  parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg });
505 
506  // fail due to operation flags not set
507  const char *const argv1[] = { "se" };
508  ArgumentReader reader(parser, argv1, argv1 + 1, true);
509  {
510  const OutputCheck c("COMPREPLY=()\n");
511  reader.read();
512  parser.printBashCompletion(1, argv1, 0, reader);
513  }
514 
515  // correct operation arg flags
516  getArg.setDenotesOperation(true);
517  setArg.setDenotesOperation(true);
518  {
519  const OutputCheck c("COMPREPLY=('set' )\n");
520  reader.reset(argv1, argv1 + 1).read();
521  parser.printBashCompletion(1, argv1, 0, reader);
522  }
523 
524  // argument at current cursor position already specified -> the completion should just return the argument
525  const char *const argv2[] = { "set" };
526  parser.resetArgs();
527  {
528  const OutputCheck c("COMPREPLY=('set' )\n");
529  reader.reset(argv2, argv2 + 1).read();
530  parser.printBashCompletion(1, argv2, 0, reader);
531  }
532 
533  // advance the cursor position -> the completion should propose the next argument
534  parser.resetArgs();
535  {
536  const OutputCheck c("COMPREPLY=('--files' '--selectors' '--values' )\n");
537  reader.reset(argv2, argv2 + 1).read();
538  parser.printBashCompletion(1, argv2, 1, reader);
539  }
540 
541  // nested operations should be proposed as operations
542  parser.resetArgs();
543  filesArg.setDenotesOperation(true);
544  {
545  const OutputCheck c("COMPREPLY=('files' '--selectors' '--values' )\n");
546  reader.reset(argv2, argv2 + 1).read();
547  parser.printBashCompletion(1, argv2, 1, reader);
548  }
549 
550  // specifying no args should propose all main arguments
551  parser.resetArgs();
552  filesArg.setDenotesOperation(false);
553  {
554  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
555  reader.reset(nullptr, nullptr).read();
556  parser.printBashCompletion(0, nullptr, 0, reader);
557  }
558 
559  // pre-defined values
560  const char *const argv3[] = { "get", "--fields" };
561  parser.resetArgs();
562  {
563  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n");
564  reader.reset(argv3, argv3 + 2).read();
565  parser.printBashCompletion(2, argv3, 2, reader);
566  }
567 
568  // pre-defined values with equation sign, one letter already present
569  const char *const argv4[] = { "set", "--values", "a" };
570  parser.resetArgs();
571  {
572  const OutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n");
573  reader.reset(argv4, argv4 + 3).read();
574  parser.printBashCompletion(3, argv4, 2, reader);
575  }
576 
577  // pre-defined values containing equation sign, equation sign already present
578  const char *const argv12[] = { "set", "--selectors", "tag=id3" };
579  parser.resetArgs();
580  {
581  const OutputCheck c("COMPREPLY=('tag=id3v1' 'tag=id3v2' )\n");
582  reader.reset(argv12, argv12 + 3).read();
583  parser.printBashCompletion(3, argv12, 2, reader);
584  }
585 
586  // recombining pre-defined values containing equation sign, equation sign already present
587  const char *const argv13[] = { "set", "--selectors", "tag", "=", "id3" };
588  parser.resetArgs();
589  {
590  const OutputCheck c("COMPREPLY=('id3v1' 'id3v2' )\n");
591  reader.reset(argv13, argv13 + 5).read();
592  parser.printBashCompletion(5, argv13, 4, reader);
593  }
594  parser.resetArgs();
595  {
596  const OutputCheck c("COMPREPLY=('id3v1' 'id3v2' 'matroska' )\n");
597  reader.reset(argv13, argv13 + 5).read();
598  parser.printBashCompletion(5, argv13, 3, reader);
599  }
600 
601  // computing pre-defined values just in time using callback
602  selectorsArg.setValueCompletionBehavior(selectorsArg.valueCompletionBehaviour() | ValueCompletionBehavior::InvokeCallback);
603  parser.resetArgs();
604  {
605  const OutputCheck c("COMPREPLY=('matroska' 'mp4' 'vorbis' )\n");
606  reader.reset(argv13, argv13 + 5).read();
607  parser.printBashCompletion(5, argv13, 3, reader);
608  }
609 
610  // pre-defined values for implicit argument
611  parser.resetArgs();
612  {
613  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n");
614  reader.reset(argv3, argv3 + 1).read();
615  parser.printBashCompletion(1, argv3, 2, reader);
616  }
617 
618  // file names
619  string iniFilePath = TestUtilities::testFilePath("test.ini");
620  iniFilePath.resize(iniFilePath.size() - 4);
621  string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv");
622  mkvFilePath.resize(mkvFilePath.size() - 17);
623  parser.resetArgs();
624  const char *const argv5[] = { "get", "--files", iniFilePath.c_str() };
625  {
626  // order for file names is not specified
627  const OutputCheck c("COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n",
628  "COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n");
629  reader.reset(argv5, argv5 + 3).read();
630  parser.printBashCompletion(3, argv5, 2, reader);
631  }
632 
633  // sub arguments
634  const char *const argv6[] = { "set", "--" };
635  parser.resetArgs();
636  {
637  const OutputCheck c("COMPREPLY=('--files' '--selectors' '--values' )\n");
638  reader.reset(argv6, argv6 + 2).read();
639  parser.printBashCompletion(2, argv6, 1, reader);
640  }
641 
642  // nested sub arguments
643  const char *const argv7[] = { "-i", "--sub", "--" };
644  parser.resetArgs();
645  {
646  const OutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n");
647  reader.reset(argv7, argv7 + 3).read();
648  parser.printBashCompletion(3, argv7, 2, reader);
649  }
650 
651  // started pre-defined values with equation sign, one letter already present, last value matches
652  const char *const argv8[] = { "set", "--values", "t" };
653  parser.resetArgs();
654  {
655  const OutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n");
656  reader.reset(argv8, argv8 + 3).read();
657  parser.printBashCompletion(3, argv8, 2, reader);
658  }
659 
660  // combined abbreviations
661  const char *const argv9[] = { "-gf" };
662  parser.resetArgs();
663  {
664  const OutputCheck c("COMPREPLY=('-gf' )\n");
665  reader.reset(argv9, argv9 + 1).read();
666  parser.printBashCompletion(1, argv9, 0, reader);
667  }
668  parser.resetArgs();
669  {
670  const OutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); });
671  reader.reset(argv9, argv9 + 1).read();
672  parser.printBashCompletion(1, argv9, 1, reader);
673  }
674 
675  // override exit function to prevent readArgs() from terminating the test run
676  exitFunction = [](int) { exitCalled = true; };
677 
678  // call completion via readArgs() with current word index
679  const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" };
680  parser.resetArgs();
681  {
682  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
683  parser.readArgs(3, argv10);
684  }
685  CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable()));
686  CPPUNIT_ASSERT(exitCalled);
687 
688  // call completion via readArgs() without current word index
689  const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" };
690  parser.resetArgs();
691  {
692  const OutputCheck c("COMPREPLY=('get' )\n");
693  parser.readArgs(3, argv11);
694  }
695 }
696 
701 {
702  // identation
703  Indentation indent;
704  indent = indent + 3;
705  CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(4 + 3), indent.level);
706 
707  // setup parser
708  ArgumentParser parser;
709  HelpArgument helpArg(parser);
710  helpArg.setRequired(true);
711  OperationArgument verboseArg("verbose", 'v', "be verbose", "actually not an operation");
712  verboseArg.setCombinable(true);
713  ConfigValueArgument nestedSubArg("nested-sub", '\0', "nested sub arg", { "value1", "value2" });
714  nestedSubArg.setRequiredValueCount(Argument::varValueCount);
715  Argument subArg("foo", 'f', "dummy");
716  subArg.setName("sub");
717  subArg.setAbbreviation('\0');
718  subArg.setDescription("sub arg");
719  subArg.setExample("sub arg example");
720  subArg.setRequired(true);
721  subArg.addSubArgument(&nestedSubArg);
722  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
723  filesArg.setCombinable(true);
724  filesArg.addSubArgument(&subArg);
725  filesArg.setSubArguments({ &subArg }); // test re-assignment btw
726  Argument envArg("env", '\0', "env");
727  envArg.setEnvironmentVariable("FILES");
728  envArg.setRequiredValueCount(2);
729  envArg.appendValueName("file");
730  parser.addMainArgument(&helpArg);
731  parser.addMainArgument(&verboseArg);
732  parser.addMainArgument(&filesArg);
733  parser.addMainArgument(&envArg);
734  dependencyVersions = { "somelib", "some other lib" };
735 
736  // parse args and assert output
737  const char *const argv[] = { "app", "-h" };
738  {
739  const OutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n"
740  "\e[0mLinked against: somelib, some other lib\n"
741  "\n\e[0m"
742  "Available operations:\n"
743  "\e[1mverbose, -v\e[0m\n"
744  " be verbose\n"
745  " example: actually not an operation\n"
746  "\n"
747  "Available top-level options:\n"
748  "\e[1m--files, -f\e[0m\n"
749  " specifies the path of the file(s) to be opened\n"
750  " \e[1m--sub\e[0m\n"
751  " sub arg\n"
752  " particularities: mandatory if parent argument is present\n"
753  " \e[1m--nested-sub\e[0m [value1] [value2] ...\n"
754  " nested sub arg\n"
755  " example: sub arg example\n"
756  "\n"
757  "\e[1m--env\e[0m [file] [value 2]\n"
758  " env\n"
759  " default environment variable: FILES\n"
760  "\n"
761  "Project website: " APP_URL "\n");
762  EscapeCodes::enabled = true;
763  parser.parseArgs(2, argv);
764  }
765 
766  verboseArg.setDenotesOperation(false);
767  {
768  const OutputCheck c(APP_NAME ", version " APP_VERSION "\n"
769  "Linked against: somelib, some other lib\n"
770  "\n"
771  "Available arguments:\n"
772  "--verbose, -v\n"
773  " be verbose\n"
774  " example: actually not an operation\n"
775  "\n"
776  "--files, -f\n"
777  " specifies the path of the file(s) to be opened\n"
778  " --sub\n"
779  " sub arg\n"
780  " particularities: mandatory if parent argument is present\n"
781  " --nested-sub [value1] [value2] ...\n"
782  " nested sub arg\n"
783  " example: sub arg example\n"
784  "\n"
785  "--env [file] [value 2]\n"
786  " env\n"
787  " default environment variable: FILES\n"
788  "\n"
789  "Project website: " APP_URL "\n");
790  EscapeCodes::enabled = false;
791  parser.resetArgs();
792  parser.parseArgs(2, argv);
793  }
794 }
795 
800 {
801  ArgumentParser parser;
802  HelpArgument helpArg(parser);
803  Argument subArg("sub-arg", 's', "mandatory sub arg");
804  subArg.setRequired(true);
805  helpArg.addSubArgument(&subArg);
806  parser.addMainArgument(&helpArg);
807  parser.setMainArguments({});
808  CPPUNIT_ASSERT_MESSAGE("clear main args", parser.mainArguments().empty());
809  parser.setMainArguments({ &helpArg });
810  CPPUNIT_ASSERT_MESSAGE("no default due to required sub arg", !parser.defaultArgument());
811  subArg.setConstraints(0, 20);
812  parser.setMainArguments({ &helpArg });
813  CPPUNIT_ASSERT_MESSAGE("default if no required sub arg", &helpArg == parser.defaultArgument());
814 }
815 
820 {
821  // assume escape codes are enabled by default
822  EscapeCodes::enabled = true;
823 
824  // ensure the initialization is not skipped
825  NoColorArgument::s_instance = nullptr;
826 
827  {
828  unsetenv("ENABLE_ESCAPE_CODES");
829  NoColorArgument noColorArg;
830  noColorArg.apply();
831  CPPUNIT_ASSERT_MESSAGE("default used if not present", EscapeCodes::enabled);
832  noColorArg.m_occurrences.emplace_back(0);
833  noColorArg.apply();
834  CPPUNIT_ASSERT_MESSAGE("default negated if present", !EscapeCodes::enabled);
835  const NoColorArgument secondInstance;
836  CPPUNIT_ASSERT_EQUAL_MESSAGE("s_instance not altered by 2nd instance", &noColorArg, NoColorArgument::s_instance);
837  }
838  {
839  setenv("ENABLE_ESCAPE_CODES", " 0 ", 1);
840  const NoColorArgument noColorArg;
841  CPPUNIT_ASSERT(!EscapeCodes::enabled);
842  }
843  {
844  setenv("ENABLE_ESCAPE_CODES", " 1 ", 1);
845  const NoColorArgument noColorArg;
846  CPPUNIT_ASSERT(EscapeCodes::enabled);
847  }
848 }
void testNoColorArgument()
Tests whether NocolorArgument toggles escape codes correctly.
void testBashCompletion()
Tests bash completion.
void resetArgs()
Resets all Argument instances assigned as mainArguments() and sub arguments.
#define QT_CONFIG_ARGUMENTS
The ConfigValueArgument class is an Argument where setCombinable() is true by default.
Argument * defaultArgument() const
Returns the default argument.
void setImplicit(bool value)
Sets whether the argument is an implicit argument.
std::size_t index
The index of the occurrence.
void testParsing()
Tests parsing command line arguments.
static void apply()
Sets EscapeCodes::enabled according to the presense of the first instantiation of NoColorArgument...
void setCombinable(bool value)
Sets whether this argument can be combined.
ValueCompletionBehavior valueCompletionBehaviour() const
Returns the items to be considered when generating completion for the values.
void setUnknownArgumentBehavior(UnknownArgumentBehavior behavior)
Sets how unknown arguments are treated.
void readArgs(int argc, const char *const *argv)
Parses the specified command line arguments.
virtual const char * what() const USE_NOTHROW
Returns a C-style character string describing the cause of the Failure.
Definition: failure.cpp:44
Contains currently only ArgumentParser and related classes.
void setMainArguments(const ArgumentInitializerList &mainArguments)
Sets the main arguments for the parser.
STL namespace.
void testArgument()
Tests the behaviour of the argument class.
void testSetMainArguments()
Tests some corner cases in setMainArguments() which are not already checked in the other tests...
bool isRequired() const
Returns an indication whether the argument is mandatory.
void testCallbacks()
Tests whether callbacks are called correctly.
void setRequired(bool required)
Sets whether this argument is mandatory or not.
Contains literals to ease asserting with CPPUNIT_ASSERT_EQUAL.
Definition: testutils.h:219
void parseArgs(int argc, const char *const *argv)
Parses the specified command line arguments.
const char * firstValue() const
Returns the first parameter value of the first occurrence of the argument.
The Indentation class allows printing indentation conveniently, eg.
The NoColorArgument class allows to specify whether use of escape codes or similar technique to provi...
void addMainArgument(Argument *argument)
Adds the specified argument to the main argument.
The ArgumentParserTests class tests the ArgumentParser and Argument classes.
The OperationArgument class is an Argument where denotesOperation() is true by default.
void setConstraints(std::size_t minOccurrences, std::size_t maxOccurrences)
Sets the allowed number of occurrences.
void setAbbreviation(char abbreviation)
Sets the abbreviation of the argument.
Contains classes and functions utilizing creating of test applications.
Definition: testutils.h:12
void setDescription(const char *description)
Sets the description of the argument.
CPP_UTILITIES_EXPORT bool enabled
Controls whether the functions inside the EscapeCodes namespace actually make use of escape codes...
void setValueCompletionBehavior(ValueCompletionBehavior valueCompletionBehaviour)
Sets the items to be considered when generating completion for the values.
#define SET_APPLICATION_INFO
Sets application meta data (including SET_DEPENDENCY_INFO) used by ArgumentParser::printHelp().
void appendValueName(const char *valueName)
Appends a value name.
CPP_UTILITIES_EXPORT std::initializer_list< const char * > dependencyVersions
Specifies the dependency versions the application was linked against (used by ArgumentParser::printHe...
const std::vector< const char * > & values(std::size_t occurrence=0) const
Returns the parameter values for the specified occurrence of argument.
Contains several functions providing conversions between different data types.
CPP_UTILITIES_EXPORT void(* exitFunction)(int)
Specifies a function quit the application.
const char * executable() const
Returns the name of the current executable.
The Argument class is a wrapper for command line argument information.
void setCallback(CallbackFunction callback)
Sets a callback function which will be called by the parser if the argument could be found and no par...
void setExample(const char *example)
Sets the a usage example for the argument.
void setPreDefinedCompletionValues(const char *preDefinedCompletionValues)
Assignes the values to be used when generating completion for the values.
ApplicationUtilities::ArgumentReader & reset(const char *const *argv, const char *const *end)
Resets the ArgumentReader to continue reading new argv.
std::vector< Argument * > path
The "path" of the occurrence (the parent elements which have been specified before).
The StandardOutputCheck class asserts whether the (standard) output written in the enclosing code blo...
Definition: outputcheck.h:19
void addSubArgument(Argument *arg)
Adds arg as a secondary argument for this argument.
void setEnvironmentVariable(const char *environmentVariable)
Sets the environment variable queried when firstValue() is called.
The ArgumentOccurrence struct holds argument values for an occurrence of an argument.
bool isPresent() const
Returns an indication whether the argument could be detected when parsing.
The HelpArgument class prints help information for an argument parser when present (–help...
CPPUNIT_TEST_SUITE_REGISTRATION(ArgumentParserTests)
The Failure class is thrown by an ArgumentParser when a parsing error occurs.
Definition: failure.h:12
const ArgumentVector & mainArguments() const
Returns the main arguments.
void setDenotesOperation(bool denotesOperation)
Sets whether the argument denotes the operation.
bool isUncombinableMainArgPresent() const
Checks whether at least one uncombinable main argument is present.
void reset()
Resets occurrences (indices, values and paths).
void setRequiredValueCount(std::size_t requiredValueCount)
Sets the number of values which are required to be given for this argument.
The ArgumentReader class internally encapsulates the process of reading command line arguments...
void setSubArguments(const ArgumentInitializerList &subArguments)
Sets the secondary arguments for this arguments.
unsigned int actualArgumentCount() const
Returns the actual number of arguments that could be found when parsing.
void setName(const char *name)
Sets the name of the argument.
void read()
Reads the commands line arguments specified when constructing the object.
CPP_UTILITIES_EXPORT std::string testFilePath(const std::string &name)
Convenience function which returns the full path of the test file with the specified name...
Definition: testutils.h:105
The ArgumentParser class provides a means for handling command line arguments.
Argument * specifiedOperation() const
Returns the first operation argument specified by the user or nullptr if no operation has been specif...
void testHelp()
Tests –help output.
void setValueNames(std::initializer_list< const char *> valueNames)
Sets the names of the requried values.
std::vector< const char * > values
The parameter values which have been specified after the occurrence of the argument.