C++ Utilities  4.12.0
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_SUITE_END();
43 
44 public:
45  void setUp();
46  void tearDown();
47 
48  void testArgument();
49  void testParsing();
50  void testCallbacks();
51  void testBashCompletion();
52  void testHelp();
53  void testSetMainArguments();
54 
55 private:
56  void callback();
57 };
58 
60 
62 {
63 }
64 
66 {
67 }
68 
73 {
74  Argument argument("test", 't', "some description");
75  CPPUNIT_ASSERT_EQUAL(argument.isRequired(), false);
76  argument.setConstraints(1, 10);
77  CPPUNIT_ASSERT_EQUAL(argument.isRequired(), true);
78  Argument subArg("sub", 's', "sub arg");
79  argument.addSubArgument(&subArg);
80  CPPUNIT_ASSERT_EQUAL(subArg.parents().at(0), &argument);
81  CPPUNIT_ASSERT(!subArg.conflictsWithArgument());
82  CPPUNIT_ASSERT(!argument.firstValue());
83  argument.setEnvironmentVariable("FOO_ENV_VAR");
84  setenv("FOO_ENV_VAR", "foo", true);
85  CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "foo"));
86  ArgumentOccurrence occurrence(0, vector<Argument *>(), nullptr);
87  occurrence.values.emplace_back("bar");
88  argument.m_occurrences.emplace_back(move(occurrence));
89  CPPUNIT_ASSERT(!strcmp(argument.firstValue(), "bar"));
90 }
91 
96 {
97  // setup parser with some test argument definitions
98  ArgumentParser parser;
100  QT_CONFIG_ARGUMENTS qtConfigArgs;
101  HelpArgument helpArg(parser);
102  Argument verboseArg("verbose", 'v', "be verbose");
103  verboseArg.setCombinable(true);
104  Argument fileArg("file", 'f', "specifies the path of the file to be opened");
105  fileArg.setValueNames({ "path" });
106  fileArg.setRequiredValueCount(1);
107  fileArg.setEnvironmentVariable("PATH");
108  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
109  filesArg.setValueNames({ "path 1", "path 2" });
110  filesArg.setRequiredValueCount(Argument::varValueCount);
111  Argument outputFileArg("output-file", 'o', "specifies the path of the output file");
112  outputFileArg.setValueNames({ "path" });
113  outputFileArg.setRequiredValueCount(1);
114  outputFileArg.setRequired(true);
115  outputFileArg.setCombinable(true);
116  Argument printFieldNamesArg("print-field-names", '\0', "prints available field names");
117  Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
118  Argument notAlbumArg("album", 'a', "should not be confused with album value");
119  displayFileInfoArg.setDenotesOperation(true);
120  displayFileInfoArg.setSubArguments({ &fileArg, &verboseArg, &notAlbumArg });
121  Argument fieldsArg("fields", '\0', "specifies the fields");
122  fieldsArg.setRequiredValueCount(Argument::varValueCount);
123  fieldsArg.setValueNames({ "title", "album", "artist", "trackpos" });
124  fieldsArg.setImplicit(true);
125  Argument displayTagInfoArg("get", 'p', "displays the values of all specified tag fields (displays all fields if none specified)");
126  displayTagInfoArg.setDenotesOperation(true);
127  displayTagInfoArg.setSubArguments({ &fieldsArg, &filesArg, &verboseArg, &notAlbumArg });
128  NoColorArgument noColorArg;
129  parser.setMainArguments({ &qtConfigArgs.qtWidgetsGuiArg(), &printFieldNamesArg, &displayTagInfoArg, &displayFileInfoArg, &helpArg, &noColorArg });
130 
131  // error about uncombinable arguments
132  const char *argv[] = { "tageditor", "get", "album", "title", "diskpos", "-f", "somefile" };
133  // try to parse, this should fail
134  try {
135  parser.parseArgs(7, argv);
136  CPPUNIT_FAIL("Exception expected.");
137  } catch (const Failure &e) {
138  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"files\" can not be combined with \"fields\"."));
139  }
140 
141  // arguments read correctly after successful parse
142  filesArg.setCombinable(true);
143  parser.resetArgs();
144  parser.parseArgs(7, argv);
145  // check results
146  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
147  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
148  CPPUNIT_ASSERT(!strcmp(parser.executable(), "tageditor"));
149  CPPUNIT_ASSERT(!verboseArg.isPresent());
150  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
151  CPPUNIT_ASSERT(fieldsArg.isPresent());
152  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album"));
153  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
154  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
155  CPPUNIT_ASSERT_THROW(displayTagInfoArg.values().at(3), out_of_range);
156 
157  // skip empty args
158  const char *argv2[] = { "tageditor", "", "-p", "album", "title", "diskpos", "", "--files", "somefile" };
159  // reparse the args
160  parser.resetArgs();
161  parser.parseArgs(9, argv2);
162  // check results again
163  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
164  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
165  CPPUNIT_ASSERT(!verboseArg.isPresent());
166  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
167  CPPUNIT_ASSERT(fieldsArg.isPresent());
168  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album"));
169  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
170  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
171  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(3), ""));
172  CPPUNIT_ASSERT_THROW(fieldsArg.values().at(4), out_of_range);
173  CPPUNIT_ASSERT(filesArg.isPresent());
174  CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
175 
176  // error about unknown argument: forget get/-p
177  const char *argv3[] = { "tageditor", "album", "title", "diskpos", "--files", "somefile" };
178  try {
179  parser.resetArgs();
180  parser.parseArgs(6, argv3);
181  CPPUNIT_FAIL("Exception expected.");
182  } catch (const Failure &e) {
183  CPPUNIT_ASSERT_EQUAL("The specified argument \"album\" is unknown."s, string(e.what()));
184  }
185 
186  // warning about unknown argument
187  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
188  // redirect stderr to check whether warnings are printed correctly
189  stringstream buffer;
190  streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
191  parser.resetArgs();
192  EscapeCodes::enabled = false;
193  try {
194  parser.parseArgs(6, argv3);
195  } catch (...) {
196  cerr.rdbuf(regularCerrBuffer);
197  throw;
198  }
199  cerr.rdbuf(regularCerrBuffer);
200  CPPUNIT_ASSERT_EQUAL("Warning: The specified argument \"album\" is unknown and will be ignored.\n"s
201  "Warning: The specified argument \"title\" is unknown and will be ignored.\n"s
202  "Warning: The specified argument \"diskpos\" is unknown and will be ignored.\n"s
203  "Warning: The specified argument \"--files\" is unknown and will be ignored.\n"s
204  "Warning: The specified argument \"somefile\" is unknown and will be ignored.\n"s,
205  buffer.str());
206  // none of the arguments should be present now
207  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
208  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
209  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
210  CPPUNIT_ASSERT(!fieldsArg.isPresent());
211  CPPUNIT_ASSERT(!filesArg.isPresent());
212 
213  // combined abbreviations like "-vf"
214  const char *argv4[] = { "tageditor", "-i", "-vf", "test" };
215  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail);
216  parser.resetArgs();
217  parser.parseArgs(4, argv4);
218  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
219  CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
220  CPPUNIT_ASSERT(verboseArg.isPresent());
221  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
222  CPPUNIT_ASSERT(!filesArg.isPresent());
223  CPPUNIT_ASSERT(fileArg.isPresent());
224  CPPUNIT_ASSERT(!strcmp(fileArg.values().at(0), "test"));
225  CPPUNIT_ASSERT_THROW(fileArg.values().at(1), out_of_range);
226 
227  // constraint checking: no multiple occurrences (not resetting verboseArg on purpose)
228  displayFileInfoArg.reset(), fileArg.reset();
229  try {
230  parser.parseArgs(4, argv4);
231  CPPUNIT_FAIL("Exception expected.");
232  } catch (const Failure &e) {
233  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
234  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time."));
235  }
236 
237  // constraint checking: no contraint (not resetting verboseArg on purpose)
238  displayFileInfoArg.reset(), fileArg.reset();
239  verboseArg.setConstraints(0, Argument::varValueCount);
240  parser.parseArgs(4, argv4);
241  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
242 
243  // constraint checking: mandatory argument
244  verboseArg.setRequired(true);
245  parser.resetArgs();
246  parser.parseArgs(4, argv4);
247  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
248 
249  // contraint checking: error about missing mandatory argument
250  const char *argv5[] = { "tageditor", "-i", "-f", "test" };
251  displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
252  try {
253  parser.parseArgs(4, argv5);
254  CPPUNIT_FAIL("Exception expected.");
255  } catch (const Failure &e) {
256  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
257  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time."));
258  }
259  verboseArg.setRequired(false);
260 
261  // combined abbreviation with nesting "-pf"
262  const char *argv10[] = { "tageditor", "-pf", "test" };
263  parser.resetArgs();
264  parser.parseArgs(3, argv10);
265  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
266  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
267  CPPUNIT_ASSERT(!fileArg.isPresent());
268  CPPUNIT_ASSERT(filesArg.isPresent());
269  CPPUNIT_ASSERT_EQUAL(1_st, filesArg.values(0).size());
270  CPPUNIT_ASSERT(!strcmp(filesArg.values(0).front(), "test"));
271  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
272 
273  // constraint checking: no complains about missing -i
274  const char *argv6[] = { "tageditor", "-g" };
275  parser.resetArgs();
276  parser.parseArgs(2, argv6);
277  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
278 
279  // constraint checking: dependend arguments (-f requires -i or -p)
280  const char *argv7[] = { "tageditor", "-f", "test" };
281  parser.resetArgs();
282  try {
283  parser.parseArgs(3, argv7);
284  CPPUNIT_FAIL("Exception expected.");
285  } catch (const Failure &e) {
286  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
287  CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown."s, string(e.what()));
288  }
289 
290  // equation sign syntax
291  const char *argv11[] = { "tageditor", "-if=test-v" };
292  parser.resetArgs();
293  parser.parseArgs(2, argv11);
294  CPPUNIT_ASSERT(!filesArg.isPresent());
295  CPPUNIT_ASSERT(fileArg.isPresent());
296  CPPUNIT_ASSERT(!verboseArg.isPresent());
297  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
298  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
299  const char *argv15[] = { "tageditor", "-i", "--file=test", "-v" };
300  parser.resetArgs();
301  parser.parseArgs(4, argv15);
302  CPPUNIT_ASSERT(!filesArg.isPresent());
303  CPPUNIT_ASSERT(fileArg.isPresent());
304  CPPUNIT_ASSERT(verboseArg.isPresent());
305  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
306  CPPUNIT_ASSERT_EQUAL("test"s, string(fileArg.values(0).front()));
307 
308  // specifying value directly after abbreviation
309  const char *argv12[] = { "tageditor", "-iftest" };
310  parser.resetArgs();
311  parser.parseArgs(2, argv12);
312  CPPUNIT_ASSERT(!filesArg.isPresent());
313  CPPUNIT_ASSERT(fileArg.isPresent());
314  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
315  CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test"));
316 
317  // specifying top-level argument after abbreviation
318  const char *argv17[] = { "tageditor", "-if=test-v", "--no-color" };
319  parser.resetArgs();
320  parser.parseArgs(3, argv17);
321  CPPUNIT_ASSERT(!filesArg.isPresent());
322  CPPUNIT_ASSERT(fileArg.isPresent());
323  CPPUNIT_ASSERT(!verboseArg.isPresent());
324  CPPUNIT_ASSERT(noColorArg.isPresent());
325  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
326  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
327 
328  // default argument
329  const char *argv8[] = { "tageditor" };
330  parser.resetArgs();
331  parser.parseArgs(1, argv8);
332  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
333  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
334  CPPUNIT_ASSERT(!verboseArg.isPresent());
335  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
336  CPPUNIT_ASSERT(!filesArg.isPresent());
337  CPPUNIT_ASSERT(!fileArg.isPresent());
338  if (getenv("PATH")) {
339  CPPUNIT_ASSERT(fileArg.firstValue());
340  CPPUNIT_ASSERT(!strcmp(fileArg.firstValue(), getenv("PATH")));
341  } else {
342  CPPUNIT_ASSERT(!fileArg.firstValue());
343  }
344 
345  // constraint checking: required value count with sufficient number of provided parameters
346  const char *argv13[] = { "tageditor", "get", "--fields", "album=test", "title", "diskpos", "--files", "somefile" };
347  verboseArg.setRequired(false);
348  parser.resetArgs();
349  parser.parseArgs(8, argv13);
350  // this should still work without complaints
351  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
352  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
353  CPPUNIT_ASSERT(!verboseArg.isPresent());
354  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
355  CPPUNIT_ASSERT(fieldsArg.isPresent());
356  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
357  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
358  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
359  CPPUNIT_ASSERT_THROW(fieldsArg.values().at(3), out_of_range);
360  CPPUNIT_ASSERT(filesArg.isPresent());
361  CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
362  CPPUNIT_ASSERT(!notAlbumArg.isPresent());
363 
364  // constraint checking: required value count with insufficient number of provided parameters
365  const char *argv9[] = { "tageditor", "-p", "album", "title", "diskpos" };
366  fieldsArg.setRequiredValueCount(4);
367  parser.resetArgs();
368  try {
369  parser.parseArgs(5, argv9);
370  CPPUNIT_FAIL("Exception expected.");
371  } catch (const Failure &e) {
372  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
373  CPPUNIT_ASSERT_EQUAL(
374  "Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos"s,
375  string(e.what()));
376  }
377 
378  // constraint checking: truncated argument not wrongly detected
379  const char *argv16[] = { "tageditor", "--hel", "-p", "album", "title", "diskpos" };
380  fieldsArg.setRequiredValueCount(Argument::varValueCount);
381  parser.resetArgs();
382  try {
383  parser.parseArgs(6, argv16);
384  CPPUNIT_FAIL("Exception expected.");
385  } catch (const Failure &e) {
386  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
387  CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown."s, string(e.what()));
388  }
389 
390  // nested operations
391  const char *argv14[] = { "tageditor", "get", "fields", "album=test", "-f", "somefile" };
392  parser.resetArgs();
393  fieldsArg.setDenotesOperation(true);
394  parser.parseArgs(6, argv14);
395  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
396  CPPUNIT_ASSERT(fieldsArg.isPresent());
397  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
398 
399  // implicit flag still works when argument doesn't denote operation
400  parser.resetArgs();
401  fieldsArg.setDenotesOperation(false);
402  parser.parseArgs(6, argv14);
403  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
404  CPPUNIT_ASSERT(fieldsArg.isPresent());
405  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "fields"));
406  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "album=test"));
407 }
408 
413 {
414  ArgumentParser parser;
415  Argument callbackArg("with-callback", 't', "callback test");
416  callbackArg.setRequiredValueCount(2);
417  callbackArg.setCallback([](const ArgumentOccurrence &occurrence) {
418  CPPUNIT_ASSERT_EQUAL(0_st, occurrence.index);
419  CPPUNIT_ASSERT(occurrence.path.empty());
420  CPPUNIT_ASSERT_EQUAL(2_st, occurrence.values.size());
421  CPPUNIT_ASSERT(!strcmp(occurrence.values[0], "val1"));
422  CPPUNIT_ASSERT(!strcmp(occurrence.values[1], "val2"));
423  throw 42;
424  });
425  Argument noCallbackArg("no-callback", 'l', "callback test");
426  noCallbackArg.setRequiredValueCount(2);
427  parser.setMainArguments({ &callbackArg, &noCallbackArg });
428 
429  // test whether callback is invoked when argument with callback is specified
430  const char *argv[] = { "test", "-t", "val1", "val2" };
431  try {
432  parser.parseArgs(4, argv);
433  } catch (int i) {
434  CPPUNIT_ASSERT_EQUAL(i, 42);
435  }
436 
437  // test whether callback is not invoked when argument with callback is not specified
438  callbackArg.reset();
439  const char *argv2[] = { "test", "-l", "val1", "val2" };
440  parser.parseArgs(4, argv2);
441 }
442 
446 static bool exitCalled = false;
447 
453 {
454  ArgumentParser parser;
455  HelpArgument helpArg(parser);
456  Argument verboseArg("verbose", 'v', "be verbose");
457  verboseArg.setCombinable(true);
458  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
459  filesArg.setRequiredValueCount(Argument::varValueCount);
460  filesArg.setCombinable(true);
461  Argument nestedSubArg("nested-sub", '\0', "nested sub arg");
462  Argument subArg("sub", '\0', "sub arg");
463  subArg.setSubArguments({ &nestedSubArg });
464  Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
465  displayFileInfoArg.setDenotesOperation(true);
466  displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg });
467  Argument fieldsArg("fields", '\0', "specifies the fields");
468  fieldsArg.setRequiredValueCount(Argument::varValueCount);
469  fieldsArg.setPreDefinedCompletionValues("title album artist trackpos");
470  fieldsArg.setImplicit(true);
471  Argument valuesArg("values", '\0', "specifies the fields");
472  valuesArg.setRequiredValueCount(Argument::varValueCount);
473  valuesArg.setPreDefinedCompletionValues("title album artist trackpos");
474  valuesArg.setImplicit(false);
475  valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
476  Argument getArg("get", 'g', "gets tag values");
477  getArg.setSubArguments({ &fieldsArg, &filesArg });
478  Argument setArg("set", 's', "sets tag values");
479  setArg.setSubArguments({ &valuesArg, &filesArg });
480 
481  parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg });
482 
483  // fail due to operation flags not set
484  const char *const argv1[] = { "se" };
485  ArgumentReader reader(parser, argv1, argv1 + 1, true);
486  {
487  const OutputCheck c("COMPREPLY=()\n");
488  reader.read();
489  parser.printBashCompletion(1, argv1, 0, reader);
490  }
491 
492  // correct operation arg flags
493  getArg.setDenotesOperation(true);
494  setArg.setDenotesOperation(true);
495  {
496  const OutputCheck c("COMPREPLY=('set' )\n");
497  reader.reset(argv1, argv1 + 1).read();
498  parser.printBashCompletion(1, argv1, 0, reader);
499  }
500 
501  // argument at current cursor position already specified -> the completion should just return the argument
502  const char *const argv2[] = { "set" };
503  parser.resetArgs();
504  {
505  const OutputCheck c("COMPREPLY=('set' )\n");
506  reader.reset(argv2, argv2 + 1).read();
507  parser.printBashCompletion(1, argv2, 0, reader);
508  }
509 
510  // advance the cursor position -> the completion should propose the next argument
511  parser.resetArgs();
512  {
513  const OutputCheck c("COMPREPLY=('--files' '--values' )\n");
514  reader.reset(argv2, argv2 + 1).read();
515  parser.printBashCompletion(1, argv2, 1, reader);
516  }
517 
518  // nested operations should be proposed as operations
519  parser.resetArgs();
520  filesArg.setDenotesOperation(true);
521  {
522  const OutputCheck c("COMPREPLY=('files' '--values' )\n");
523  reader.reset(argv2, argv2 + 1).read();
524  parser.printBashCompletion(1, argv2, 1, reader);
525  }
526 
527  // specifying no args should propose all main arguments
528  parser.resetArgs();
529  filesArg.setDenotesOperation(false);
530  {
531  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
532  reader.reset(nullptr, nullptr).read();
533  parser.printBashCompletion(0, nullptr, 0, reader);
534  }
535 
536  // pre-defined values
537  const char *const argv3[] = { "get", "--fields" };
538  parser.resetArgs();
539  {
540  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n");
541  reader.reset(argv3, argv3 + 2).read();
542  parser.printBashCompletion(2, argv3, 2, reader);
543  }
544 
545  // pre-defined values with equation sign, one letter already present
546  const char *const argv4[] = { "set", "--values", "a" };
547  parser.resetArgs();
548  {
549  const OutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n");
550  reader.reset(argv4, argv4 + 3).read();
551  parser.printBashCompletion(3, argv4, 2, reader);
552  }
553 
554  // pre-defined values for implicit argument
555  parser.resetArgs();
556  {
557  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n");
558  reader.reset(argv3, argv3 + 1).read();
559  parser.printBashCompletion(1, argv3, 2, reader);
560  }
561 
562  // file names
563  string iniFilePath = TestUtilities::testFilePath("test.ini");
564  iniFilePath.resize(iniFilePath.size() - 4);
565  string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv");
566  mkvFilePath.resize(mkvFilePath.size() - 17);
567  parser.resetArgs();
568  const char *const argv5[] = { "get", "--files", iniFilePath.c_str() };
569  {
570  // order for file names is not specified
571  const OutputCheck c("COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n",
572  "COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n");
573  reader.reset(argv5, argv5 + 3).read();
574  parser.printBashCompletion(3, argv5, 2, reader);
575  }
576 
577  // sub arguments
578  const char *const argv6[] = { "set", "--" };
579  parser.resetArgs();
580  {
581  const OutputCheck c("COMPREPLY=('--files' '--values' )\n");
582  reader.reset(argv6, argv6 + 2).read();
583  parser.printBashCompletion(2, argv6, 1, reader);
584  }
585 
586  // nested sub arguments
587  const char *const argv7[] = { "-i", "--sub", "--" };
588  parser.resetArgs();
589  {
590  const OutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n");
591  reader.reset(argv7, argv7 + 3).read();
592  parser.printBashCompletion(3, argv7, 2, reader);
593  }
594 
595  // started pre-defined values with equation sign, one letter already present, last value matches
596  const char *const argv8[] = { "set", "--values", "t" };
597  parser.resetArgs();
598  {
599  const OutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n");
600  reader.reset(argv8, argv8 + 3).read();
601  parser.printBashCompletion(3, argv8, 2, reader);
602  }
603 
604  // combined abbreviations
605  const char *const argv9[] = { "-gf" };
606  parser.resetArgs();
607  {
608  const OutputCheck c("COMPREPLY=('-gf' )\n");
609  reader.reset(argv9, argv9 + 1).read();
610  parser.printBashCompletion(1, argv9, 0, reader);
611  }
612  parser.resetArgs();
613  {
614  const OutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); });
615  reader.reset(argv9, argv9 + 1).read();
616  parser.printBashCompletion(1, argv9, 1, reader);
617  }
618 
619  // override exit function to prevent readArgs() from terminating the test run
620  exitFunction = [](int) { exitCalled = true; };
621 
622  // call completion via readArgs() with current word index
623  const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" };
624  parser.resetArgs();
625  {
626  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
627  parser.readArgs(3, argv10);
628  }
629  CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable()));
630  CPPUNIT_ASSERT(exitCalled);
631 
632  // call completion via readArgs() without current word index
633  const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" };
634  parser.resetArgs();
635  {
636  const OutputCheck c("COMPREPLY=('get' )\n");
637  parser.readArgs(3, argv11);
638  }
639 }
640 
645 {
646  // identation
647  Indentation indent;
648  indent = indent + 3;
649  CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(4 + 3), indent.level);
650 
651  // setup parser
652  ArgumentParser parser;
653  HelpArgument helpArg(parser);
654  helpArg.setRequired(true);
655  OperationArgument verboseArg("verbose", 'v', "be verbose", "actually not an operation");
656  verboseArg.setCombinable(true);
657  ConfigValueArgument nestedSubArg("nested-sub", '\0', "nested sub arg", { "value1", "value2" });
658  nestedSubArg.setRequiredValueCount(Argument::varValueCount);
659  Argument subArg("sub", '\0', "sub arg");
660  subArg.setRequired(true);
661  subArg.addSubArgument(&nestedSubArg);
662  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
663  filesArg.setCombinable(true);
664  filesArg.addSubArgument(&subArg);
665  filesArg.setSubArguments({ &subArg }); // test re-assignment btw
666  Argument envArg("env", '\0', "env");
667  envArg.setEnvironmentVariable("FILES");
668  envArg.setRequiredValueCount(2);
669  envArg.appendValueName("file");
670  parser.addMainArgument(&helpArg);
671  parser.addMainArgument(&verboseArg);
672  parser.addMainArgument(&filesArg);
673  parser.addMainArgument(&envArg);
674  dependencyVersions = { "somelib", "some other lib" };
675 
676  // parse args and assert output
677  const char *const argv[] = { "app", "-h" };
678  {
679  const OutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n"
680  "\e[0mLinked against: somelib, some other lib\n"
681  "\n\e[0m"
682  "Available arguments:\n"
683  "\e[1m--help, -h\e[0m\n"
684  " shows this information\n"
685  " particularities: mandatory\n"
686  "\n"
687  "\e[1mverbose, -v\e[0m\n"
688  " be verbose\n"
689  " example: actually not an operation\n"
690  "\n"
691  "\e[1m--files, -f\e[0m\n"
692  " specifies the path of the file(s) to be opened\n"
693  " \e[1m--sub\e[0m\n"
694  " sub arg\n"
695  " particularities: mandatory if parent argument is present\n"
696  " \e[1m--nested-sub\e[0m [value1] [value2] ...\n"
697  " nested sub arg\n"
698  "\n"
699  "\e[1m--env\e[0m [file] [value 2]\n"
700  " env\n"
701  " default environment variable: FILES\n"
702  "\n"
703  "Project website: " APP_URL "\n");
704  EscapeCodes::enabled = true;
705  parser.parseArgs(2, argv);
706  }
707 }
708 
713 {
714  ArgumentParser parser;
715  HelpArgument helpArg(parser);
716  Argument subArg("sub-arg", 's', "mandatory sub arg");
717  subArg.setRequired(true);
718  helpArg.addSubArgument(&subArg);
719  parser.addMainArgument(&helpArg);
720  parser.setMainArguments({});
721  CPPUNIT_ASSERT_MESSAGE("clear main args", parser.mainArguments().empty());
722  parser.setMainArguments({ &helpArg });
723  CPPUNIT_ASSERT_MESSAGE("no default due to required sub arg", !parser.defaultArgument());
724  subArg.setConstraints(0, 20);
725  parser.setMainArguments({ &helpArg });
726  CPPUNIT_ASSERT_MESSAGE("default if no required sub arg", &helpArg == parser.defaultArgument());
727 }
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.
void setCombinable(bool value)
Sets whether this argument can be combined.
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.
Contains classes and functions utilizing creating of test applications.
Definition: testutils.h:12
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 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.
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.
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.
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.