C++ Utilities  4.14.2
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.\nDid you mean get or --help?"s, string(e.what()));
200  }
201 
202  // error about unknown argument: mistake in final argument
203  const char *argv18[] = { "tageditor", "get", "album", "title", "diskpos", "--verbose", "--fi" };
204  try {
205  parser.resetArgs();
206  parser.parseArgs(7, argv18);
207  CPPUNIT_FAIL("Exception expected.");
208  } catch (const Failure &e) {
209  CPPUNIT_ASSERT_EQUAL("The specified argument \"--fi\" is unknown.\nDid you mean --files or --no-color?"s, string(e.what()));
210  }
211 
212  // warning about unknown argument
213  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Warn);
214  // redirect stderr to check whether warnings are printed correctly
215  stringstream buffer;
216  streambuf *regularCerrBuffer = cerr.rdbuf(buffer.rdbuf());
217  parser.resetArgs();
218  EscapeCodes::enabled = false;
219  try {
220  parser.parseArgs(6, argv3);
221  } catch (...) {
222  cerr.rdbuf(regularCerrBuffer);
223  throw;
224  }
225  cerr.rdbuf(regularCerrBuffer);
226  CPPUNIT_ASSERT_EQUAL("Warning: The specified argument \"album\" is unknown and will be ignored.\n"s
227  "Warning: The specified argument \"title\" is unknown and will be ignored.\n"s
228  "Warning: The specified argument \"diskpos\" is unknown and will be ignored.\n"s
229  "Warning: The specified argument \"--files\" is unknown and will be ignored.\n"s
230  "Warning: The specified argument \"somefile\" is unknown and will be ignored.\n"s,
231  buffer.str());
232  // none of the arguments should be present now
233  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
234  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
235  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
236  CPPUNIT_ASSERT(!fieldsArg.isPresent());
237  CPPUNIT_ASSERT(!filesArg.isPresent());
238 
239  // combined abbreviations like "-vf"
240  const char *argv4[] = { "tageditor", "-i", "-vf", "test" };
241  parser.setUnknownArgumentBehavior(UnknownArgumentBehavior::Fail);
242  parser.resetArgs();
243  parser.parseArgs(4, argv4);
244  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
245  CPPUNIT_ASSERT(displayFileInfoArg.isPresent());
246  CPPUNIT_ASSERT(verboseArg.isPresent());
247  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
248  CPPUNIT_ASSERT(!filesArg.isPresent());
249  CPPUNIT_ASSERT(fileArg.isPresent());
250  CPPUNIT_ASSERT(!strcmp(fileArg.values().at(0), "test"));
251  CPPUNIT_ASSERT_THROW(fileArg.values().at(1), out_of_range);
252 
253  // constraint checking: no multiple occurrences (not resetting verboseArg on purpose)
254  displayFileInfoArg.reset(), fileArg.reset();
255  try {
256  parser.parseArgs(4, argv4);
257  CPPUNIT_FAIL("Exception expected.");
258  } catch (const Failure &e) {
259  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
260  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" mustn't be specified more than 1 time."));
261  }
262 
263  // constraint checking: no contraint (not resetting verboseArg on purpose)
264  displayFileInfoArg.reset(), fileArg.reset();
265  verboseArg.setConstraints(0, Argument::varValueCount);
266  parser.parseArgs(4, argv4);
267  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
268 
269  // constraint checking: mandatory argument
270  verboseArg.setRequired(true);
271  parser.resetArgs();
272  parser.parseArgs(4, argv4);
273  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
274 
275  // contraint checking: error about missing mandatory argument
276  const char *argv5[] = { "tageditor", "-i", "-f", "test" };
277  displayFileInfoArg.reset(), fileArg.reset(), verboseArg.reset();
278  try {
279  parser.parseArgs(4, argv5);
280  CPPUNIT_FAIL("Exception expected.");
281  } catch (const Failure &e) {
282  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
283  CPPUNIT_ASSERT(!strcmp(e.what(), "The argument \"verbose\" must be specified at least 1 time."));
284  }
285  verboseArg.setRequired(false);
286 
287  // combined abbreviation with nesting "-pf"
288  const char *argv10[] = { "tageditor", "-pf", "test" };
289  parser.resetArgs();
290  parser.parseArgs(3, argv10);
291  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
292  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
293  CPPUNIT_ASSERT(!fileArg.isPresent());
294  CPPUNIT_ASSERT(filesArg.isPresent());
295  CPPUNIT_ASSERT_EQUAL(1_st, filesArg.values(0).size());
296  CPPUNIT_ASSERT(!strcmp(filesArg.values(0).front(), "test"));
297  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
298 
299  // constraint checking: no complains about missing -i
300  const char *argv6[] = { "tageditor", "-g" };
301  parser.resetArgs();
302  parser.parseArgs(2, argv6);
303  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
304 
305  // constraint checking: dependend arguments (-f requires -i or -p)
306  const char *argv7[] = { "tageditor", "-f", "test" };
307  parser.resetArgs();
308  try {
309  parser.parseArgs(3, argv7);
310  CPPUNIT_FAIL("Exception expected.");
311  } catch (const Failure &e) {
312  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
313  CPPUNIT_ASSERT_EQUAL("The specified argument \"-f\" is unknown.\nDid you mean get or --help?"s, string(e.what()));
314  }
315 
316  // equation sign syntax
317  const char *argv11[] = { "tageditor", "-if=test-v" };
318  parser.resetArgs();
319  parser.parseArgs(2, argv11);
320  CPPUNIT_ASSERT(!filesArg.isPresent());
321  CPPUNIT_ASSERT(fileArg.isPresent());
322  CPPUNIT_ASSERT(!verboseArg.isPresent());
323  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
324  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
325  const char *argv15[] = { "tageditor", "-i", "--file=test", "-v" };
326  parser.resetArgs();
327  parser.parseArgs(4, argv15);
328  CPPUNIT_ASSERT(!filesArg.isPresent());
329  CPPUNIT_ASSERT(fileArg.isPresent());
330  CPPUNIT_ASSERT(verboseArg.isPresent());
331  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
332  CPPUNIT_ASSERT_EQUAL("test"s, string(fileArg.values(0).front()));
333 
334  // specifying value directly after abbreviation
335  const char *argv12[] = { "tageditor", "-iftest" };
336  parser.resetArgs();
337  parser.parseArgs(2, argv12);
338  CPPUNIT_ASSERT(!filesArg.isPresent());
339  CPPUNIT_ASSERT(fileArg.isPresent());
340  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
341  CPPUNIT_ASSERT(!strcmp(fileArg.values(0).front(), "test"));
342 
343  // specifying top-level argument after abbreviation
344  const char *argv17[] = { "tageditor", "-if=test-v", "--no-color" };
345  parser.resetArgs();
346  parser.parseArgs(3, argv17);
347  CPPUNIT_ASSERT(!filesArg.isPresent());
348  CPPUNIT_ASSERT(fileArg.isPresent());
349  CPPUNIT_ASSERT(!verboseArg.isPresent());
350  CPPUNIT_ASSERT(noColorArg.isPresent());
351  CPPUNIT_ASSERT_EQUAL(1_st, fileArg.values(0).size());
352  CPPUNIT_ASSERT_EQUAL("test-v"s, string(fileArg.values(0).front()));
353 
354  // default argument
355  const char *argv8[] = { "tageditor" };
356  parser.resetArgs();
357  parser.parseArgs(1, argv8);
358  CPPUNIT_ASSERT(qtConfigArgs.qtWidgetsGuiArg().isPresent());
359  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
360  CPPUNIT_ASSERT(!verboseArg.isPresent());
361  CPPUNIT_ASSERT(!displayTagInfoArg.isPresent());
362  CPPUNIT_ASSERT(!filesArg.isPresent());
363  CPPUNIT_ASSERT(!fileArg.isPresent());
364  if (getenv("PATH")) {
365  CPPUNIT_ASSERT(fileArg.firstValue());
366  CPPUNIT_ASSERT(!strcmp(fileArg.firstValue(), getenv("PATH")));
367  } else {
368  CPPUNIT_ASSERT(!fileArg.firstValue());
369  }
370 
371  // constraint checking: required value count with sufficient number of provided parameters
372  const char *argv13[] = { "tageditor", "get", "--fields", "album=test", "title", "diskpos", "--files", "somefile" };
373  verboseArg.setRequired(false);
374  parser.resetArgs();
375  parser.parseArgs(8, argv13);
376  // this should still work without complaints
377  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
378  CPPUNIT_ASSERT(!displayFileInfoArg.isPresent());
379  CPPUNIT_ASSERT(!verboseArg.isPresent());
380  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
381  CPPUNIT_ASSERT(fieldsArg.isPresent());
382  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
383  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "title"));
384  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(2), "diskpos"));
385  CPPUNIT_ASSERT_THROW(fieldsArg.values().at(3), out_of_range);
386  CPPUNIT_ASSERT(filesArg.isPresent());
387  CPPUNIT_ASSERT(!strcmp(filesArg.values().at(0), "somefile"));
388  CPPUNIT_ASSERT(!notAlbumArg.isPresent());
389 
390  // constraint checking: required value count with insufficient number of provided parameters
391  const char *argv9[] = { "tageditor", "-p", "album", "title", "diskpos" };
392  fieldsArg.setRequiredValueCount(4);
393  parser.resetArgs();
394  try {
395  parser.parseArgs(5, argv9);
396  CPPUNIT_FAIL("Exception expected.");
397  } catch (const Failure &e) {
398  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
399  CPPUNIT_ASSERT_EQUAL(
400  "Not all parameter for argument \"fields\" provided. You have to provide the following parameter: title album artist trackpos"s,
401  string(e.what()));
402  }
403 
404  // constraint checking: truncated argument not wrongly detected
405  const char *argv16[] = { "tageditor", "--hel", "-p", "album", "title", "diskpos" };
406  fieldsArg.setRequiredValueCount(Argument::varValueCount);
407  parser.resetArgs();
408  try {
409  parser.parseArgs(6, argv16);
410  CPPUNIT_FAIL("Exception expected.");
411  } catch (const Failure &e) {
412  CPPUNIT_ASSERT(!qtConfigArgs.qtWidgetsGuiArg().isPresent());
413  CPPUNIT_ASSERT_EQUAL("The specified argument \"--hel\" is unknown.\nDid you mean --help or get?"s, string(e.what()));
414  }
415 
416  // nested operations
417  const char *argv14[] = { "tageditor", "get", "fields", "album=test", "-f", "somefile" };
418  parser.resetArgs();
419  fieldsArg.setDenotesOperation(true);
420  parser.parseArgs(6, argv14);
421  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
422  CPPUNIT_ASSERT(fieldsArg.isPresent());
423  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "album=test"));
424 
425  // implicit flag still works when argument doesn't denote operation
426  parser.resetArgs();
427  fieldsArg.setDenotesOperation(false);
428  parser.parseArgs(6, argv14);
429  CPPUNIT_ASSERT(displayTagInfoArg.isPresent());
430  CPPUNIT_ASSERT(fieldsArg.isPresent());
431  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(0), "fields"));
432  CPPUNIT_ASSERT(!strcmp(fieldsArg.values().at(1), "album=test"));
433 }
434 
439 {
440  ArgumentParser parser;
441  Argument callbackArg("with-callback", 't', "callback test");
442  callbackArg.setRequiredValueCount(2);
443  callbackArg.setCallback([](const ArgumentOccurrence &occurrence) {
444  CPPUNIT_ASSERT_EQUAL(0_st, occurrence.index);
445  CPPUNIT_ASSERT(occurrence.path.empty());
446  CPPUNIT_ASSERT_EQUAL(2_st, occurrence.values.size());
447  CPPUNIT_ASSERT(!strcmp(occurrence.values[0], "val1"));
448  CPPUNIT_ASSERT(!strcmp(occurrence.values[1], "val2"));
449  throw 42;
450  });
451  Argument noCallbackArg("no-callback", 'l', "callback test");
452  noCallbackArg.setRequiredValueCount(2);
453  parser.setMainArguments({ &callbackArg, &noCallbackArg });
454 
455  // test whether callback is invoked when argument with callback is specified
456  const char *argv[] = { "test", "-t", "val1", "val2" };
457  try {
458  parser.parseArgs(4, argv);
459  } catch (int i) {
460  CPPUNIT_ASSERT_EQUAL(i, 42);
461  }
462 
463  // test whether callback is not invoked when argument with callback is not specified
464  callbackArg.reset();
465  const char *argv2[] = { "test", "-l", "val1", "val2" };
466  parser.parseArgs(4, argv2);
467 }
468 
472 static bool exitCalled = false;
473 
479 {
480  ArgumentParser parser;
481  HelpArgument helpArg(parser);
482  Argument verboseArg("verbose", 'v', "be verbose");
483  verboseArg.setCombinable(true);
484  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
485  filesArg.setRequiredValueCount(Argument::varValueCount);
486  filesArg.setCombinable(true);
487  Argument nestedSubArg("nested-sub", '\0', "nested sub arg");
488  Argument subArg("sub", '\0', "sub arg");
489  subArg.setSubArguments({ &nestedSubArg });
490  Argument displayFileInfoArg("display-file-info", 'i', "displays general file information");
491  displayFileInfoArg.setDenotesOperation(true);
492  displayFileInfoArg.setSubArguments({ &filesArg, &verboseArg, &subArg });
493  Argument fieldsArg("fields", '\0', "specifies the fields");
494  fieldsArg.setRequiredValueCount(Argument::varValueCount);
495  fieldsArg.setPreDefinedCompletionValues("title album artist trackpos");
496  fieldsArg.setImplicit(true);
497  Argument valuesArg("values", '\0', "specifies the fields");
498  valuesArg.setRequiredValueCount(Argument::varValueCount);
499  valuesArg.setPreDefinedCompletionValues("title album artist trackpos");
500  valuesArg.setImplicit(false);
501  valuesArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues | ValueCompletionBehavior::AppendEquationSign);
502  Argument selectorsArg("selectors", '\0', "has some more pre-defined values");
503  selectorsArg.setRequiredValueCount(Argument::varValueCount);
504  selectorsArg.setPreDefinedCompletionValues("tag=id3v1 tag=id3v2 tag=matroska target=file target=track");
505  selectorsArg.setImplicit(false);
506  selectorsArg.setValueCompletionBehavior(ValueCompletionBehavior::PreDefinedValues);
507  selectorsArg.setCallback(
508  [&selectorsArg](const ArgumentOccurrence &) { selectorsArg.setPreDefinedCompletionValues("tag=matroska tag=mp4 tag=vorbis"); });
509  Argument getArg("get", 'g', "gets tag values");
510  getArg.setSubArguments({ &fieldsArg, &filesArg });
511  Argument setArg("set", 's', "sets tag values");
512  setArg.setSubArguments({ &valuesArg, &filesArg, &selectorsArg });
513 
514  parser.setMainArguments({ &helpArg, &displayFileInfoArg, &getArg, &setArg });
515 
516  // fail due to operation flags not set
517  const char *const argv1[] = { "se" };
518  ArgumentReader reader(parser, argv1, argv1 + 1, true);
519  {
520  const OutputCheck c("COMPREPLY=()\n");
521  CPPUNIT_ASSERT(reader.read());
522  parser.printBashCompletion(1, argv1, 0, reader);
523  }
524 
525  // correct operation arg flags
526  getArg.setDenotesOperation(true);
527  setArg.setDenotesOperation(true);
528  {
529  const OutputCheck c("COMPREPLY=('set' )\n");
530  reader.reset(argv1, argv1 + 1).read();
531  parser.printBashCompletion(1, argv1, 0, reader);
532  }
533 
534  // argument at current cursor position already specified -> the completion should just return the argument
535  const char *const argv2[] = { "set" };
536  parser.resetArgs();
537  {
538  const OutputCheck c("COMPREPLY=('set' )\n");
539  reader.reset(argv2, argv2 + 1).read();
540  parser.printBashCompletion(1, argv2, 0, reader);
541  }
542 
543  // advance the cursor position -> the completion should propose the next argument
544  parser.resetArgs();
545  {
546  const OutputCheck c("COMPREPLY=('--files' '--selectors' '--values' )\n");
547  reader.reset(argv2, argv2 + 1).read();
548  parser.printBashCompletion(1, argv2, 1, reader);
549  }
550 
551  // nested operations should be proposed as operations
552  parser.resetArgs();
553  filesArg.setDenotesOperation(true);
554  {
555  const OutputCheck c("COMPREPLY=('files' '--selectors' '--values' )\n");
556  reader.reset(argv2, argv2 + 1).read();
557  parser.printBashCompletion(1, argv2, 1, reader);
558  }
559 
560  // specifying no args should propose all main arguments
561  parser.resetArgs();
562  filesArg.setDenotesOperation(false);
563  {
564  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
565  reader.reset(nullptr, nullptr).read();
566  parser.printBashCompletion(0, nullptr, 0, reader);
567  }
568 
569  // pre-defined values
570  const char *const argv3[] = { "get", "--fields" };
571  parser.resetArgs();
572  {
573  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--files' )\n");
574  reader.reset(argv3, argv3 + 2).read();
575  parser.printBashCompletion(2, argv3, 2, reader);
576  }
577 
578  // pre-defined values with equation sign, one letter already present
579  const char *const argv4[] = { "set", "--values", "a" };
580  parser.resetArgs();
581  {
582  const OutputCheck c("COMPREPLY=('album=' 'artist=' ); compopt -o nospace\n");
583  reader.reset(argv4, argv4 + 3).read();
584  parser.printBashCompletion(3, argv4, 2, reader);
585  }
586 
587  // pre-defined values containing equation sign, equation sign already present
588  const char *const argv12[] = { "set", "--selectors", "tag=id3" };
589  parser.resetArgs();
590  {
591  const OutputCheck c("COMPREPLY=('tag=id3v1' 'tag=id3v2' )\n");
592  reader.reset(argv12, argv12 + 3).read();
593  parser.printBashCompletion(3, argv12, 2, reader);
594  }
595 
596  // recombining pre-defined values containing equation sign, equation sign already present
597  const char *const argv13[] = { "set", "--selectors", "tag", "=", "id3" };
598  parser.resetArgs();
599  {
600  const OutputCheck c("COMPREPLY=('id3v1' 'id3v2' )\n");
601  reader.reset(argv13, argv13 + 5).read();
602  parser.printBashCompletion(5, argv13, 4, reader);
603  }
604  parser.resetArgs();
605  {
606  const OutputCheck c("COMPREPLY=('id3v1' 'id3v2' 'matroska' )\n");
607  reader.reset(argv13, argv13 + 5).read();
608  parser.printBashCompletion(5, argv13, 3, reader);
609  }
610 
611  // computing pre-defined values just in time using callback
612  selectorsArg.setValueCompletionBehavior(selectorsArg.valueCompletionBehaviour() | ValueCompletionBehavior::InvokeCallback);
613  parser.resetArgs();
614  {
615  const OutputCheck c("COMPREPLY=('matroska' 'mp4' 'vorbis' )\n");
616  reader.reset(argv13, argv13 + 5).read();
617  parser.printBashCompletion(5, argv13, 3, reader);
618  }
619 
620  // pre-defined values for implicit argument
621  parser.resetArgs();
622  {
623  const OutputCheck c("COMPREPLY=('title' 'album' 'artist' 'trackpos' '--fields' '--files' )\n");
624  reader.reset(argv3, argv3 + 1).read();
625  parser.printBashCompletion(1, argv3, 2, reader);
626  }
627 
628  // file names
629  string iniFilePath = TestUtilities::testFilePath("test.ini");
630  iniFilePath.resize(iniFilePath.size() - 4);
631  string mkvFilePath = TestUtilities::testFilePath("test 'with quote'.mkv");
632  mkvFilePath.resize(mkvFilePath.size() - 17);
633  parser.resetArgs();
634  const char *const argv5[] = { "get", "--files", iniFilePath.c_str() };
635  {
636  // order for file names is not specified
637  const OutputCheck c("COMPREPLY=('" % mkvFilePath % " '\"'\"'with quote'\"'\"'.mkv' '" % iniFilePath + ".ini' ); compopt -o filenames\n",
638  "COMPREPLY=('" % iniFilePath % ".ini' '" % mkvFilePath + " '\"'\"'with quote'\"'\"'.mkv' ); compopt -o filenames\n");
639  reader.reset(argv5, argv5 + 3).read();
640  parser.printBashCompletion(3, argv5, 2, reader);
641  }
642 
643  // sub arguments
644  const char *const argv6[] = { "set", "--" };
645  parser.resetArgs();
646  {
647  const OutputCheck c("COMPREPLY=('--files' '--selectors' '--values' )\n");
648  reader.reset(argv6, argv6 + 2).read();
649  parser.printBashCompletion(2, argv6, 1, reader);
650  }
651 
652  // nested sub arguments
653  const char *const argv7[] = { "-i", "--sub", "--" };
654  parser.resetArgs();
655  {
656  const OutputCheck c("COMPREPLY=('--files' '--nested-sub' '--verbose' )\n");
657  reader.reset(argv7, argv7 + 3).read();
658  parser.printBashCompletion(3, argv7, 2, reader);
659  }
660 
661  // started pre-defined values with equation sign, one letter already present, last value matches
662  const char *const argv8[] = { "set", "--values", "t" };
663  parser.resetArgs();
664  {
665  const OutputCheck c("COMPREPLY=('title=' 'trackpos=' ); compopt -o nospace\n");
666  reader.reset(argv8, argv8 + 3).read();
667  parser.printBashCompletion(3, argv8, 2, reader);
668  }
669 
670  // combined abbreviations
671  const char *const argv9[] = { "-gf" };
672  parser.resetArgs();
673  {
674  const OutputCheck c("COMPREPLY=('-gf' )\n");
675  reader.reset(argv9, argv9 + 1).read();
676  parser.printBashCompletion(1, argv9, 0, reader);
677  }
678  parser.resetArgs();
679  {
680  const OutputCheck c([](const string &actualOutput) { CPPUNIT_ASSERT_EQUAL(0_st, actualOutput.find("COMPREPLY=('--fields' ")); });
681  reader.reset(argv9, argv9 + 1).read();
682  parser.printBashCompletion(1, argv9, 1, reader);
683  }
684 
685  // override exit function to prevent readArgs() from terminating the test run
686  exitFunction = [](int) { exitCalled = true; };
687 
688  // call completion via readArgs() with current word index
689  const char *const argv10[] = { "/some/path/tageditor", "--bash-completion-for", "0" };
690  parser.resetArgs();
691  {
692  const OutputCheck c("COMPREPLY=('display-file-info' 'get' 'set' '--help' )\n");
693  parser.readArgs(3, argv10);
694  }
695  CPPUNIT_ASSERT(!strcmp("/some/path/tageditor", parser.executable()));
696  CPPUNIT_ASSERT(exitCalled);
697 
698  // call completion via readArgs() without current word index
699  const char *const argv11[] = { "/some/path/tageditor", "--bash-completion-for", "ge" };
700  parser.resetArgs();
701  {
702  const OutputCheck c("COMPREPLY=('get' )\n");
703  parser.readArgs(3, argv11);
704  }
705 }
706 
711 {
712  // identation
713  Indentation indent;
714  indent = indent + 3;
715  CPPUNIT_ASSERT_EQUAL(static_cast<unsigned char>(4 + 3), indent.level);
716 
717  // setup parser
718  ArgumentParser parser;
719  HelpArgument helpArg(parser);
720  helpArg.setRequired(true);
721  OperationArgument verboseArg("verbose", 'v', "be verbose", "actually not an operation");
722  verboseArg.setCombinable(true);
723  ConfigValueArgument nestedSubArg("nested-sub", '\0', "nested sub arg", { "value1", "value2" });
724  nestedSubArg.setRequiredValueCount(Argument::varValueCount);
725  Argument subArg("foo", 'f', "dummy");
726  subArg.setName("sub");
727  subArg.setAbbreviation('\0');
728  subArg.setDescription("sub arg");
729  subArg.setExample("sub arg example");
730  subArg.setRequired(true);
731  subArg.addSubArgument(&nestedSubArg);
732  Argument filesArg("files", 'f', "specifies the path of the file(s) to be opened");
733  filesArg.setCombinable(true);
734  filesArg.addSubArgument(&subArg);
735  filesArg.setSubArguments({ &subArg }); // test re-assignment btw
736  Argument envArg("env", '\0', "env");
737  envArg.setEnvironmentVariable("FILES");
738  envArg.setRequiredValueCount(2);
739  envArg.appendValueName("file");
740  parser.addMainArgument(&helpArg);
741  parser.addMainArgument(&verboseArg);
742  parser.addMainArgument(&filesArg);
743  parser.addMainArgument(&envArg);
744  dependencyVersions2 = { "somelib", "some other lib" };
745 
746  // parse args and assert output
747  const char *const argv[] = { "app", "-h" };
748  {
749  const OutputCheck c("\e[1m" APP_NAME ", version " APP_VERSION "\n"
750  "\e[0mLinked against: somelib, some other lib\n"
751  "\n\e[0m"
752  "Available operations:\n"
753  "\e[1mverbose, -v\e[0m\n"
754  " be verbose\n"
755  " example: actually not an operation\n"
756  "\n"
757  "Available top-level options:\n"
758  "\e[1m--files, -f\e[0m\n"
759  " specifies the path of the file(s) to be opened\n"
760  " \e[1m--sub\e[0m\n"
761  " sub arg\n"
762  " particularities: mandatory if parent argument is present\n"
763  " \e[1m--nested-sub\e[0m [value1] [value2] ...\n"
764  " nested sub arg\n"
765  " example: sub arg example\n"
766  "\n"
767  "\e[1m--env\e[0m [file] [value 2]\n"
768  " env\n"
769  " default environment variable: FILES\n"
770  "\n"
771  "Project website: " APP_URL "\n");
772  EscapeCodes::enabled = true;
773  parser.parseArgs(2, argv);
774  }
775 
776  verboseArg.setDenotesOperation(false);
777  {
778  const OutputCheck c(APP_NAME ", version " APP_VERSION "\n"
779  "Linked against: somelib, some other lib\n"
780  "\n"
781  "Available arguments:\n"
782  "--verbose, -v\n"
783  " be verbose\n"
784  " example: actually not an operation\n"
785  "\n"
786  "--files, -f\n"
787  " specifies the path of the file(s) to be opened\n"
788  " --sub\n"
789  " sub arg\n"
790  " particularities: mandatory if parent argument is present\n"
791  " --nested-sub [value1] [value2] ...\n"
792  " nested sub arg\n"
793  " example: sub arg example\n"
794  "\n"
795  "--env [file] [value 2]\n"
796  " env\n"
797  " default environment variable: FILES\n"
798  "\n"
799  "Project website: " APP_URL "\n");
800  EscapeCodes::enabled = false;
801  parser.resetArgs();
802  parser.parseArgs(2, argv);
803  }
804 }
805 
810 {
811  ArgumentParser parser;
812  HelpArgument helpArg(parser);
813  Argument subArg("sub-arg", 's', "mandatory sub arg");
814  subArg.setRequired(true);
815  helpArg.addSubArgument(&subArg);
816  parser.addMainArgument(&helpArg);
817  parser.setMainArguments({});
818  CPPUNIT_ASSERT_MESSAGE("clear main args", parser.mainArguments().empty());
819  parser.setMainArguments({ &helpArg });
820  CPPUNIT_ASSERT_MESSAGE("no default due to required sub arg", !parser.defaultArgument());
821  subArg.setConstraints(0, 20);
822  parser.setMainArguments({ &helpArg });
823  CPPUNIT_ASSERT_MESSAGE("default if no required sub arg", &helpArg == parser.defaultArgument());
824 }
825 
830 {
831  // assume escape codes are enabled by default
832  EscapeCodes::enabled = true;
833 
834  // ensure the initialization is not skipped
835  NoColorArgument::s_instance = nullptr;
836 
837  {
838  unsetenv("ENABLE_ESCAPE_CODES");
839  NoColorArgument noColorArg;
840  noColorArg.apply();
841  CPPUNIT_ASSERT_MESSAGE("default used if not present", EscapeCodes::enabled);
842  noColorArg.m_occurrences.emplace_back(0);
843  noColorArg.apply();
844  CPPUNIT_ASSERT_MESSAGE("default negated if present", !EscapeCodes::enabled);
845  const NoColorArgument secondInstance;
846  CPPUNIT_ASSERT_EQUAL_MESSAGE("s_instance not altered by 2nd instance", &noColorArg, NoColorArgument::s_instance);
847  }
848  {
849  setenv("ENABLE_ESCAPE_CODES", " 0 ", 1);
850  const NoColorArgument noColorArg;
851  CPPUNIT_ASSERT(!EscapeCodes::enabled);
852  }
853  {
854  setenv("ENABLE_ESCAPE_CODES", " 1 ", 1);
855  const NoColorArgument noColorArg;
856  CPPUNIT_ASSERT(EscapeCodes::enabled);
857  }
858 }
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.
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
bool read()
Reads the commands line arguments specified when constructing the object.
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
CPP_UTILITIES_EXPORT std::vector< const char * > dependencyVersions2
Specifies the dependency versions the application was linked against (used by ArgumentParser::printHe...
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.
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.