瀏覽代碼

ENH: commandlineparser: API, documentation and test enhancements:

- Deprecated arguments
- Custom argument name prefixes
- Print short and long arg name in one line in help text
- Print default parameter values in help text
- API to group arguments in the help text
- QSettings support: merging of argument values with multiple parameters, custom "disable settings" command
- Changed some methods to be "const"
- Added test cases
- Added header documentation
Sascha Zelzer 14 年之前
父節點
當前提交
0a11fa766c
共有 3 個文件被更改,包括 780 次插入80 次删除
  1. 270 10
      Libs/Core/Testing/Cpp/ctkCommandLineParserTest1.cpp
  2. 294 58
      Libs/Core/ctkCommandLineParser.cpp
  3. 216 12
      Libs/Core/ctkCommandLineParser.h

+ 270 - 10
Libs/Core/Testing/Cpp/ctkCommandLineParserTest1.cpp

@@ -1,6 +1,7 @@
 
 // Qt includes
 #include <QDebug>
+#include <QSettings>
 
 // CTK includes
 #include "ctkCommandLineParser.h"
@@ -130,11 +131,10 @@ int ctkCommandLineParserTest1(int, char*[])
 
   QString expectedHelpString;
   QTextStream streamExpectedHelpString(&expectedHelpString);
-  streamExpectedHelpString << "  --help-string..........This is an help string\n"
-                            << "  --help-string-med\n"
-                            << "  -hs2\n"
-                            << "  --help-string-long.....This is an help string too !\n"
-                            << "  -hs3...................This is an help string too for sure !?\n";
+  streamExpectedHelpString << "  --help-string..............This is an help string\n"
+                           << "  --help-string-med\n"
+                           << "  -hs2, --help-string-long...This is an help string too !\n"
+                           << "  -hs3.......................This is an help string too for sure !?\n";
 
   if (expectedHelpString != parser4.helpText('.'))
     {
@@ -145,11 +145,10 @@ int ctkCommandLineParserTest1(int, char*[])
 
   QString expectedHelpString2;
   QTextStream streamExpectedHelpString2(&expectedHelpString2);
-  streamExpectedHelpString2 << "  --help-string          This is an help string\n"
-                           << "  --help-string-med\n"
-                           << "  -hs2\n"
-                           << "  --help-string-long     This is an help string too !\n"
-                           << "  -hs3                   This is an help string too for sure !?\n";
+  streamExpectedHelpString2 << "  --help-string              This is an help string\n"
+                            << "  --help-string-med\n"
+                            << "  -hs2, --help-string-long   This is an help string too !\n"
+                            << "  -hs3                       This is an help string too for sure !?\n";
   if (expectedHelpString2 != parser4.helpText())
     {
     qCritical() << "Test4 - Problem with helpText() - helpText:\n" << parser4.helpText()
@@ -442,5 +441,266 @@ int ctkCommandLineParserTest1(int, char*[])
     return EXIT_FAILURE;
     }
 
+  // Test11 - Check if setArgumentPrefix works as expected
+  ctkCommandLineParser parser11;
+  parser11.setArgumentPrefix("--", "/");
+  parser11.addArgument("test-string", "", QVariant::String);
+  parser11.addArgument("", "i", QVariant::Int);
+
+  QStringList arguments11;
+  arguments11 << "ctkCommandLineParserTest1";
+  arguments11 << "--test-string" << "Unix-style";
+  arguments11 << "test-string"; // unknown argument
+  arguments11 << "/i" << "5";
+
+  ok = false;
+  parsedArgs = parser11.parseArguments(arguments11, &ok);
+  if (!ok)
+  {
+    qCritical() << "Test11 - Failed to parse arguments: " << parser11.errorString();
+    return EXIT_FAILURE;
+  }
+
+  if (!parser11.unparsedArguments().contains("test-string"))
+  {
+    qCritical() << "Test11 - argument test-string should be unknown.";
+    return EXIT_FAILURE;
+  }
+
+  if (!parser11.argumentParsed("test-string") || !parser11.argumentParsed("i"))
+  {
+    qCritical() << "Test11 - Problem with argumentParsed().";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["test-string"].toString() != "Unix-style")
+  {
+    qCritical() << "Test11 - Failed argument: test-string, got: " << parsedArgs["test-string"].toString()
+        << ", expected: " << "Unix-style";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["i"].toInt() != 5)
+  {
+    qCritical() << "Test11 - Failed argument: i, got: " << parsedArgs["i"].toInt()
+        << ", expected: " << 5;
+    return EXIT_FAILURE;
+  }
+
+  // Test12 - Check if the returned hash map contains the correct entries
+  ctkCommandLineParser parser12;
+  parser12.addArgument("--test-list", "-l", QVariant::StringList);
+
+  QStringList arguments12;
+  arguments12 << "ctkCommandLineParserTest1";
+
+  parsedArgs = parser12.parseArguments(arguments12);
+  if (!parsedArgs.isEmpty())
+  {
+    qCritical() << "Test12 - Returned hash map should be empty.";
+    return EXIT_FAILURE;
+  }
+
+  arguments12 << "--test-list" << "--test-list2";
+  parsedArgs = parser12.parseArguments(arguments12);
+  if (parsedArgs.size() != 1 || !parsedArgs.contains("--test-list"))
+  {
+    qCritical() << "Test12 - Returned hash map contains wrong elements.";
+    return EXIT_FAILURE;
+  }
+
+  // Test13 - Check that supplying a default value works
+  ctkCommandLineParser parser13;
+  parser13.addArgument("", "-d", QVariant::Int, "Argument with default value", 3);
+  parsedArgs = parser13.parseArguments(QStringList(), &ok);
+  if (!parsedArgs.contains("-d"))
+  {
+    qCritical() << "Test13 - Returned hash map does not contain argument with default value.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["-d"].toInt() != 3)
+  {
+    qCritical() << "Test13 - Returned hash map contains wrong argument parameter.";
+    return EXIT_FAILURE;
+  }
+
+  // ==================== QSettings tests ====================
+
+  QSettings settings;
+  settings.setValue("long-settings-argument", 5);
+  settings.setValue("s", "settings-short");
+  settings.setValue("invalid", QVariant());
+
+  // Test14 - Check that QSettings are used
+  ctkCommandLineParser parser14(&settings);
+  parser14.setArgumentPrefix("--", "-");
+  parser14.addArgument("long-settings-argument", "", QVariant::Int);
+  parser14.addArgument("", "s", QVariant::String, "A short argument", "my-short");
+
+  QStringList arguments14;
+  arguments14 << "ctkCommandLineParserTest1";
+  arguments14 << "-s" << "short";
+  arguments14 << "unknown";
+
+  parsedArgs = parser14.parseArguments(arguments14);
+
+  //  Check that QSettings are ignored
+  if (parsedArgs.size() != 1 || parsedArgs["s"] != "short")
+  {
+    qCritical() << "Test14 - Parsed arguments must only contain -s short.";
+    return EXIT_FAILURE;
+  }
+
+  // Now use QSettings
+  parser14.enableSettings("disable-settings");
+  parser14.addArgument("disable-settings", "", QVariant::Bool);
+  parsedArgs = parser14.parseArguments(arguments14);
+
+  if (!parser14.settingsEnabled())
+  {
+    qCritical() << "Test14 - Disabling settings failed.";
+    return EXIT_FAILURE;
+  }
+
+  if (parser14.unparsedArguments().size() != 1 ||
+      !parser14.unparsedArguments().contains("unknown"))
+  {
+    qCritical() << "Test14 - Parsing unknown arguments failed.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs.contains("invalid"))
+  {
+    qCritical() << "Test14 - Invalid QSettings value found.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs.size() != 2)
+  {
+    qCritical() << "Test14 - Wrong number of parsed arguments.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["s"] != "short")
+  {
+    qCritical() << "Test14 - QSettings values must not overwrite user values.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["long-settings-argument"].toInt() != 5)
+  {
+    qCritical() << "Test14 - Missing value from QSettings.";
+    return EXIT_FAILURE;
+  }
+
+  arguments14.clear();
+  arguments14 << "ctkCommandLineParserTest1";
+  parsedArgs = parser14.parseArguments(arguments14);
+
+  if (parsedArgs.size() != 2)
+  {
+    qCritical() << "Test14 - Only QSettings values corresponding to arguments must be included.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["s"] != "settings-short")
+  {
+    qCritical() << "Test14 - QSettings value should be used as default parameter.";
+    return EXIT_FAILURE;
+  }
+
+  // Disable QSettings via command line argument
+  arguments14.clear();
+  arguments14 << "ctkCommandLineParserTest1";
+  arguments14 << "--disable-settings";
+  arguments14 << "--long-settings-argument" << "12";
+
+  parsedArgs = parser14.parseArguments(arguments14);
+
+  if (parsedArgs["long-settings-argument"].toInt() != 12)
+  {
+    qCritical() << "Test14 - Wrong parameter for argument: long-settings-argument.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs["s"] != "my-short")
+  {
+    qCritical() << parsedArgs;
+    qCritical() << "Test14 - Default value for argument -s not used.";
+    return EXIT_FAILURE;
+  }
+
+  // Test15 - Check that merging with QSettings works
+  settings.clear();
+  settings.setValue("--list-argument", "z");
+
+  ctkCommandLineParser parser15(&settings);
+  parser15.enableSettings();
+  parser15.addArgument("--list-argument", "", QVariant::StringList);
+
+  QStringList arguments15;
+  arguments15 << "ctkCommandLineParserTest1";
+  arguments15 << "--list-argument" << "a" << "b";
+
+  // Test with enabled merging
+  ok = false;
+  parsedArgs = parser15.parseArguments(arguments15, &ok);
+  if (!ok)
+  {
+    qCritical() << "Test15 - parsing arguments failed.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs.contains("--list-argument"))
+  {
+    QStringList list = parsedArgs["--list-argument"].toStringList();
+    if (list.size() != 3)
+    {
+      qCritical() << "Test15 - Parameter merging failed.";
+      return EXIT_FAILURE;
+    }
+    if (!list.contains("a") || !list.contains("b") || !list.contains("z"))
+    {
+      qCritical() << "Test15 - Merged list contains wrong elements.";
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    qCritical() << "Test15 - --list-argument was not parsed.";
+    return EXIT_FAILURE;
+  }
+
+  // Test with disabled merging
+  parser15.mergeSettings(false);
+  ok = false;
+  parsedArgs = parser15.parseArguments(arguments15, &ok);
+  if (!ok)
+  {
+    qCritical() << "Test15 - parsing arguments failed.";
+    return EXIT_FAILURE;
+  }
+
+  if (parsedArgs.contains("--list-argument"))
+  {
+    QStringList list = parsedArgs["--list-argument"].toStringList();
+    if (list.size() != 2)
+    {
+      qCritical() << "Test15 - Disabling merging failed.";
+      return EXIT_FAILURE;
+    }
+    if (!list.contains("a") || !list.contains("b"))
+    {
+      qCritical() << "Test15 - List contains wrong elements.";
+      return EXIT_FAILURE;
+    }
+  }
+  else
+  {
+    qCritical() << "Test15 - --list-argument was not parsed.";
+    return EXIT_FAILURE;
+  }
+
   return EXIT_SUCCESS;
 }

+ 294 - 58
Libs/Core/ctkCommandLineParser.cpp

@@ -1,9 +1,13 @@
+// STL includes
+#include <stdexcept>
 
 // Qt includes 
 #include <QHash>
 #include <QStringList>
 #include <QTextStream>
 #include <QDebug>
+#include <QSettings>
+#include <QPointer>
 
 // CTK includes
 #include "ctkCommandLineParser.h"
@@ -15,11 +19,15 @@ class CommandLineParserArgumentDescription
 {
 public:
   CommandLineParserArgumentDescription(
-    const QString& longArg, const QString& shortArg, QVariant::Type type,
-    const QString& argHelp, const QVariant& defaultValue, bool ignoreRest)
-      : LongArg(longArg), ShortArg(shortArg), ArgHelp(argHelp),
-  IgnoreRest(ignoreRest), NumberOfParametersToProcess(0),
-  Value(type)
+    const QString& longArg, const QString& longArgPrefix,
+    const QString& shortArg, const QString& shortArgPrefix,
+    QVariant::Type type, const QString& argHelp,
+    const QVariant& defaultValue, bool ignoreRest,
+    bool deprecated)
+      : LongArg(longArg), LongArgPrefix(longArgPrefix),
+      ShortArg(shortArg), ShortArgPrefix(shortArgPrefix),
+      ArgHelp(argHelp), IgnoreRest(ignoreRest), NumberOfParametersToProcess(0),
+      Deprecated(deprecated), DefaultValue(defaultValue), Value(type), ValueType(type)
   {
     if (defaultValue.isValid())
     {
@@ -66,14 +74,19 @@ public:
   QString helpText(int fieldWidth, const char charPad);
 
   QString LongArg;
+  QString LongArgPrefix;
   QString ShortArg;
+  QString ShortArgPrefix;
   QString ArgHelp;
   bool    IgnoreRest;
   int     NumberOfParametersToProcess;
   QString RegularExpression;
   QString ExactMatchFailedMessage;
+  bool    Deprecated;
 
-  QVariant Value;
+  QVariant       DefaultValue;
+  QVariant       Value;
+  QVariant::Type ValueType;
 };
 
 // --------------------------------------------------------------------------
@@ -137,30 +150,39 @@ QString CommandLineParserArgumentDescription::helpText(int fieldWidth, const cha
   stream.setFieldAlignment(QTextStream::AlignLeft);
   stream.setPadChar(charPad);
 
-  if (this->LongArg.isEmpty() && !this->ArgHelp.isEmpty())
-    {
-    stream.setFieldWidth(fieldWidth);
-    }
-
+  QString shortAndLongArg;
   if (!this->ShortArg.isEmpty())
-    {
-    stream  << QString("  %1").arg(this->ShortArg);
-    if(!this->LongArg.isEmpty())
-      {
-      stream << "\n";
-      }
-    }
+  {
+    shortAndLongArg += QString("  %1%2").arg(this->ShortArgPrefix).arg(this->ShortArg);
+  }
 
   if (!this->LongArg.isEmpty())
+  {
+    if (this->ShortArg.isEmpty())
     {
-    if(!this->ArgHelp.isEmpty())
-      {
-      stream.setFieldWidth(fieldWidth);
-      }
-    stream  << QString("  %1").arg(this->LongArg);
+      shortAndLongArg.append("  ");
     }
+    else
+    {
+      shortAndLongArg.append(", ");
+    }
+
+    shortAndLongArg += QString("%1%2").arg(this->LongArgPrefix).arg(this->LongArg);
+  }
+
+  if(!this->ArgHelp.isEmpty())
+  {
+    stream.setFieldWidth(fieldWidth);
+  }
+
+  stream  << shortAndLongArg;
   stream.setFieldWidth(0);
-  stream << this->ArgHelp << "\n";
+  stream << this->ArgHelp;
+  if (!this->DefaultValue.isNull())
+  {
+    stream << " (default: " << this->DefaultValue.toString() << ")";
+  }
+  stream << "\n";
   return text;
 }
 
@@ -173,7 +195,10 @@ QString CommandLineParserArgumentDescription::helpText(int fieldWidth, const cha
 class ctkCommandLineParser::ctkInternal
 {
 public:
-  ctkInternal():Debug(false),FieldWidth(0){}
+  ctkInternal(QSettings* settings)
+    : Debug(false), FieldWidth(0), UseQSettings(false),
+      Settings(settings), MergeSettings(true)
+  {}
 
   ~ctkInternal() { qDeleteAll(ArgumentDescriptionList); }
   
@@ -181,12 +206,21 @@ public:
   
   QList<CommandLineParserArgumentDescription*>          ArgumentDescriptionList;
   QHash<QString, CommandLineParserArgumentDescription*> ArgNameToArgumentDescriptionMap;
+  QMap<QString, QList<CommandLineParserArgumentDescription*> > GroupToArgumentDescriptionListMap;
   
   QStringList UnparsedArguments; 
   QStringList ProcessedArguments;
   QString     ErrorString;
   bool        Debug;
   int         FieldWidth;
+  QString     LongPrefix;
+  QString     ShortPrefix;
+  QString     CurrentGroup;
+  bool        UseQSettings;
+  QPointer<QSettings> Settings;
+  QString     DisableQSettingsLongArg;
+  QString     DisableQSettingsShortArg;
+  bool        MergeSettings;
 };
 
 // --------------------------------------------------------------------------
@@ -196,9 +230,23 @@ public:
 CommandLineParserArgumentDescription*
   ctkCommandLineParser::ctkInternal::argumentDescription(const QString& argument)
 {
-  if (this->ArgNameToArgumentDescriptionMap.contains(argument))
+  QString unprefixedArg = argument;
+  if (!LongPrefix.isEmpty() && argument.startsWith(LongPrefix))
+  {
+    unprefixedArg = argument.mid(LongPrefix.length());
+  }
+  else if (!ShortPrefix.isEmpty() && argument.startsWith(ShortPrefix))
+  {
+    unprefixedArg = argument.mid(ShortPrefix.length());
+  }
+  else if (!LongPrefix.isEmpty() && !ShortPrefix.isEmpty())
+  {
+    return 0;
+  }
+
+  if (this->ArgNameToArgumentDescriptionMap.contains(unprefixedArg))
     {
-    return this->ArgNameToArgumentDescriptionMap[argument];
+    return this->ArgNameToArgumentDescriptionMap[unprefixedArg];
     }
   return 0;
 }
@@ -207,9 +255,9 @@ CommandLineParserArgumentDescription*
 // ctkCommandLineParser methods
 
 // --------------------------------------------------------------------------
-ctkCommandLineParser::ctkCommandLineParser()
+ctkCommandLineParser::ctkCommandLineParser(QSettings* settings)
 {
-  this->Internal = new ctkInternal();
+  this->Internal = new ctkInternal(settings);
 }
 
 // --------------------------------------------------------------------------
@@ -226,9 +274,20 @@ QHash<QString, QVariant> ctkCommandLineParser::parseArguments(const QStringList&
   this->Internal->UnparsedArguments.clear();
   this->Internal->ProcessedArguments.clear();
   this->Internal->ErrorString.clear();
+  foreach (CommandLineParserArgumentDescription* desc,
+           this->Internal->ArgumentDescriptionList)
+  {
+    desc->Value = QVariant(desc->ValueType);
+    if (desc->DefaultValue.isValid())
+    {
+      desc->Value = desc->DefaultValue;
+    }
+  }
 
   bool ignoreRest = false;
+  bool useSettings = this->Internal->UseQSettings;
   CommandLineParserArgumentDescription * currentArgDesc = 0;
+  QList<CommandLineParserArgumentDescription*> parsedArgDescriptions;
   for(int i = 1; i < arguments.size(); ++i)
     {
 
@@ -242,6 +301,14 @@ QHash<QString, QVariant> ctkCommandLineParser::parseArguments(const QStringList&
       continue;
       }
 
+    // Skip if the argument does not start with the defined prefix
+    if (!(argument.startsWith(this->Internal->LongPrefix)
+      || argument.startsWith(this->Internal->ShortPrefix)))
+    {
+      this->Internal->UnparsedArguments << argument;
+      continue;
+    }
+
     // Skip if argument has already been parsed ...
     if (this->Internal->ProcessedArguments.contains(argument))
       {
@@ -255,6 +322,24 @@ QHash<QString, QVariant> ctkCommandLineParser::parseArguments(const QStringList&
     // Is there a corresponding argument description ?
     if (currentArgDesc)
       {
+
+      // If the argument is deprecated, print the help text but continue processing
+      if (currentArgDesc->Deprecated)
+      {
+        qWarning().nospace() << "Deprecated argument " << argument << ": "  << currentArgDesc->ArgHelp;
+      }
+      else
+      {
+        parsedArgDescriptions.push_back(currentArgDesc);
+      }
+
+      // Is the argument the special "disable QSettings" argument?
+      if ((!currentArgDesc->LongArg.isEmpty() && currentArgDesc->LongArg == this->Internal->DisableQSettingsLongArg)
+        || (!currentArgDesc->ShortArg.isEmpty() && currentArgDesc->ShortArg == this->Internal->DisableQSettingsShortArg))
+      {
+        useSettings = false;
+      }
+
       this->Internal->ProcessedArguments << currentArgDesc->ShortArg << currentArgDesc->LongArg;
       int numberOfParametersToProcess = currentArgDesc->NumberOfParametersToProcess;
       ignoreRest = currentArgDesc->IgnoreRest;
@@ -351,6 +436,20 @@ QHash<QString, QVariant> ctkCommandLineParser::parseArguments(const QStringList&
 
   if (ok) *ok = true;
 
+  QSettings* settings = 0;
+  if (this->Internal->UseQSettings && useSettings)
+  {
+    if (this->Internal->Settings)
+    {
+      settings = this->Internal->Settings;
+    }
+    else
+    {
+      // Use a default constructed QSettings instance
+      settings = new QSettings();
+    }
+  }
+
   QHash<QString, QVariant> parsedArguments;
   QListIterator<CommandLineParserArgumentDescription*> it(this->Internal->ArgumentDescriptionList);
   while (it.hasNext())
@@ -366,19 +465,71 @@ QHash<QString, QVariant> ctkCommandLineParser::parseArguments(const QStringList&
       key = desc->ShortArg;
     }
 
-    parsedArguments.insert(key, desc->Value);
+    if (parsedArgDescriptions.contains(desc))
+    {
+      // The argument was supplied on the command line, so use the given value
+
+      if (this->Internal->MergeSettings && settings)
+      {
+        // Merge with QSettings
+        QVariant settingsVal = settings->value(key);
+
+        if (desc->ValueType == QVariant::StringList &&
+            settingsVal.canConvert(QVariant::StringList))
+        {
+          QStringList stringList = desc->Value.toStringList();
+          stringList.append(settingsVal.toStringList());
+          parsedArguments.insert(key, stringList);
+        }
+        else
+        {
+          // do a normal insert
+          parsedArguments.insert(key, desc->Value);
+        }
+      }
+      else
+      {
+        // No merging, just insert all user values
+        parsedArguments.insert(key, desc->Value);
+      }
+    }
+    else
+    {
+      if (settings)
+      {
+        // If there is a valid QSettings entry for the argument, use the value
+        QVariant settingsVal = settings->value(key, desc->Value);
+        if (!settingsVal.isNull())
+        {
+          parsedArguments.insert(key, settingsVal);
+        }
+      }
+      else
+      {
+        // Just insert the arguments with valid default values
+        if (!desc->Value.isNull())
+        {
+          parsedArguments.insert(key, desc->Value);
+        }
+      }
+    }
   }
+
+  // If we created a default QSettings instance, delete it
+  if (settings && !this->Internal->Settings)
+    delete settings;
+
   return parsedArguments;
 }
 
 // -------------------------------------------------------------------------
-QString ctkCommandLineParser::errorString()
+QString ctkCommandLineParser::errorString() const
 {
   return this->Internal->ErrorString;
 }
 
 // -------------------------------------------------------------------------
-const QStringList& ctkCommandLineParser::unparsedArguments()
+const QStringList& ctkCommandLineParser::unparsedArguments() const
 {
   return this->Internal->UnparsedArguments;
 }
@@ -386,46 +537,59 @@ const QStringList& ctkCommandLineParser::unparsedArguments()
 // --------------------------------------------------------------------------
 void ctkCommandLineParser::addArgument(const QString& longarg, const QString& shortarg,
                                        QVariant::Type type, const QString& argHelp,
-                                       const QVariant& defaultValue, bool ignoreRest)
+                                       const QVariant& defaultValue, bool ignoreRest,
+                                       bool deprecated)
 {
-  Q_ASSERT(!defaultValue.isValid() || defaultValue.type() == type);
+  Q_ASSERT_X(!(longarg.isEmpty() && shortarg.isEmpty()), "addArgument",
+             "both long and short argument names are empty");
+  if (longarg.isEmpty() && shortarg.isEmpty()) { return; }
+
+  Q_ASSERT_X(!defaultValue.isValid() || defaultValue.type() == type, "addArgument",
+             "defaultValue type does not match");
+  if (defaultValue.isValid() && defaultValue.type() != type)
+    throw std::logic_error("The QVariant type of defaultValue does not match the specified type");
 
   /* Make sure it's not already added */
   bool added = this->Internal->ArgNameToArgumentDescriptionMap.contains(longarg);
-  Q_ASSERT(!added);
+  Q_ASSERT_X(!added, "addArgument", "long argument already added");
   if (added) { return; }
 
   added = this->Internal->ArgNameToArgumentDescriptionMap.contains(shortarg);
-  Q_ASSERT(!added);
+  Q_ASSERT_X(!added, "addArgument", "short argument already added");
   if (added) { return; }
 
   CommandLineParserArgumentDescription* argDesc =
-    new CommandLineParserArgumentDescription(longarg, shortarg, type,
-                                             argHelp, defaultValue, ignoreRest);
-
-  Q_ASSERT(!(longarg.isEmpty() && shortarg.isEmpty()));
-  if (longarg.isEmpty() && shortarg.isEmpty()) { return; }
+    new CommandLineParserArgumentDescription(longarg, this->Internal->LongPrefix,
+                                             shortarg, this->Internal->ShortPrefix, type,
+                                             argHelp, defaultValue, ignoreRest, deprecated);
 
+  int argWidth = 0;
   if (!longarg.isEmpty())
   {
     this->Internal->ArgNameToArgumentDescriptionMap[longarg] = argDesc;
-    int argWidth = longarg.length() + 7;
-    if (argWidth > this->Internal->FieldWidth)
-    {
-      this->Internal->FieldWidth = argWidth;
-    }
+    argWidth += longarg.length() + this->Internal->LongPrefix.length();
   }
   if (!shortarg.isEmpty())
   {
     this->Internal->ArgNameToArgumentDescriptionMap[shortarg] = argDesc;
-    int argWidth = shortarg.length() + 7;
-    if (argWidth > this->Internal->FieldWidth)
-    {
-      this->Internal->FieldWidth = argWidth;
-    }
+    argWidth += shortarg.length() + this->Internal->ShortPrefix.length() + 2;
+  }
+  argWidth += 5;
+
+  // Set the field width for the arguments
+  if (argWidth > this->Internal->FieldWidth)
+  {
+    this->Internal->FieldWidth = argWidth;
   }
 
   this->Internal->ArgumentDescriptionList << argDesc;
+  this->Internal->GroupToArgumentDescriptionListMap[this->Internal->CurrentGroup] << argDesc;
+}
+
+void ctkCommandLineParser::addDeprecatedArgument(
+    const QString& longarg, const QString& shortarg, const QString& argHelp)
+{
+  addArgument(longarg, shortarg, QVariant::StringList, argHelp, QVariant(), false, true);
 }
 
 // --------------------------------------------------------------------------
@@ -449,29 +613,101 @@ bool ctkCommandLineParser::setExactMatchRegularExpression(
 }
 
 // --------------------------------------------------------------------------
-QString ctkCommandLineParser::helpText(const char charPad)
+int ctkCommandLineParser::fieldWidth() const
+{
+  return this->Internal->FieldWidth;
+}
+
+// --------------------------------------------------------------------------
+void ctkCommandLineParser::beginGroup(const QString& description)
+{
+  this->Internal->CurrentGroup = description;
+}
+
+// --------------------------------------------------------------------------
+void ctkCommandLineParser::endGroup()
+{
+  this->Internal->CurrentGroup.clear();
+}
+
+// --------------------------------------------------------------------------
+void ctkCommandLineParser::enableSettings(const QString& disableLongArg, const QString& disableShortArg)
+{
+  this->Internal->UseQSettings = true;
+  this->Internal->DisableQSettingsLongArg = disableLongArg;
+  this->Internal->DisableQSettingsShortArg = disableShortArg;
+}
+
+// --------------------------------------------------------------------------
+void ctkCommandLineParser::mergeSettings(bool merge)
+{
+  this->Internal->MergeSettings = merge;
+}
+
+bool ctkCommandLineParser::settingsEnabled() const
+{
+  return this->Internal->UseQSettings;
+}
+
+// --------------------------------------------------------------------------
+QString ctkCommandLineParser::helpText(const char charPad) const
 {
   QString text;
   QTextStream stream(&text);
 
-  // Loop over argument descriptions
-  foreach(CommandLineParserArgumentDescription* argDesc,
-          this->Internal->ArgumentDescriptionList)
+  QList<CommandLineParserArgumentDescription*> deprecatedArgs;
+
+  // Loop over grouped argument descriptions
+  QMapIterator<QString, QList<CommandLineParserArgumentDescription*> > it(
+      this->Internal->GroupToArgumentDescriptionListMap);
+  while(it.hasNext())
+  {
+    it.next();
+    if (!it.key().isEmpty())
     {
-    stream << argDesc->helpText(this->Internal->FieldWidth, charPad);
+      stream << "\n" << it.key() << "\n";
     }
+    foreach(CommandLineParserArgumentDescription* argDesc, it.value())
+    {
+      if (argDesc->Deprecated)
+      {
+        deprecatedArgs << argDesc;
+      }
+      else
+      {
+        stream << argDesc->helpText(this->Internal->FieldWidth, charPad);
+      }
+    }
+  }
+
+  if (!deprecatedArgs.empty())
+  {
+    stream << "\nDeprecated arguments:\n";
+    foreach(CommandLineParserArgumentDescription* argDesc, deprecatedArgs)
+    {
+      stream << argDesc->helpText(this->Internal->FieldWidth, charPad);
+    }
+  }
+
   return text;
 }
 
 // --------------------------------------------------------------------------
-bool ctkCommandLineParser::argumentAdded(const QString& argument)
+bool ctkCommandLineParser::argumentAdded(const QString& argument) const
 {
   return this->Internal->ArgNameToArgumentDescriptionMap.contains(argument);
 }
 
 // --------------------------------------------------------------------------
-bool ctkCommandLineParser::argumentParsed(const QString& argument)
+bool ctkCommandLineParser::argumentParsed(const QString& argument) const
 {
   return this->Internal->ProcessedArguments.contains(argument);
 }
 
+// --------------------------------------------------------------------------
+void ctkCommandLineParser::setArgumentPrefix(const QString& longPrefix, const QString& shortPrefix)
+{
+  this->Internal->LongPrefix = longPrefix;
+  this->Internal->ShortPrefix = shortPrefix;
+}
+

+ 216 - 12
Libs/Core/ctkCommandLineParser.h

@@ -6,15 +6,99 @@
 #include <QStringList>
 #include <QVariant>
 
+class QSettings;
+
 // CTK includes
 #include "CTKCoreExport.h"
 
-// --------------------------------------------------------------------------
+/**
+ * The CTK command line parser.
+ *
+ * Use this class to add information about the command line arguments
+ * your program understands and to easily parse them from a given list
+ * of strings.
+ *
+ * This parser provides the following features:
+ *
+ * <ul>
+ * <li>Add arguments by supplying a long name and/or a short name.
+ *     Arguments are validated using a regular expression. They can have
+ *     a default value and a help string.</li>
+ * <li>Deprecated arguments.</li>
+ * <li>Custom regular expressions for argument validation.</li>
+ * <li>Set different argument name prefixes for native platform look and feel.</li>
+ * <li>QSettings support. Default values for arguments can be read from
+ *     a QSettings object.</li>
+ * <li>Create a help text for the command line arguments with support for
+ *     grouping arguments.</li>
+ * </ul>
+ *
+ * Here is an example how to use this class inside a main function:
+ *
+ * \code
+ * #include <ctkCommandLineParser.h>
+ * #include <QCoreApplication>
+ * #include <QTextStream>
+ *
+ * int main(int argc, char** argv)
+ * {
+ *   QCoreApplication app(argc, argv);
+ *   // This is used by QSettings
+ *   QCoreApplication::setOrganizationName("MyOrg");
+ *   QCoreApplication::setApplicationName("MyApp");
+ *
+ *   ctkCommandLineParser parser;
+ *   // Use Unix-style argument names
+ *   parser.setArgumentPrefix("--", "-");
+ *   // Enable QSettings support
+ *   parser.enableSettings("disable-settings");
+ *
+ *   // Add command line argument names
+ *   parser.addArgument("disable-settings", "", QVariant::Bool, "Do not use QSettings");
+ *   parser.addArgument("help", "h", QVariant::Bool, "Show this help text");
+ *   parser.addArgument("search-paths", "s", QVariant::StringList, "A list of paths to search");
+ *
+ *   // Parse the command line arguments
+ *   bool ok = false;
+ *   QHash<QString, QVariant> parsedArgs = parser.parseArguments(QCoreApplication::arguments(), &ok);
+ *   if (!ok)
+ *   {
+ *     QTextStream(stderr, QIODevice::WriteOnly) << "Error parsing arguments: "
+ *                                               << parser.errorString() << "\n";
+ *     return EXIT_FAILURE;
+ *   }
+ *
+ *   // Show a help message
+ *   if (parsedArgs.contains("help") || parsedArgs.contains("h"))
+ *   {
+ *     QTextStream(stdout, QIODevice::WriteOnly) << parser.helpText();
+ *     return EXIT_SUCCESS;
+ *   }
+ *
+ *   // Do something
+ *
+ *   return EXIT_SUCCESS;
+ * }
+ * \endcode
+ */
 class CTK_CORE_EXPORT ctkCommandLineParser
 {
 public:
 
-  ctkCommandLineParser();
+  /**
+   * Constructs a parser instance.
+   *
+   * If QSettings support is enabled by a call to <code>enableSettings()</code>
+   * the provided QSettings instance will be used. If the QSettings instance is
+   * zero, a default constructed QSettings instance will be used when parsing
+   * the command line arguments. Using a default constructed instance is usually
+   * what you want, if you have called <code>QCoreApplication::setOrganizationName()</code>
+   * and <code>QCoreApplication::setApplicationName()</code>.
+   *
+   * @param settings A QSettings instance which should be used.
+   */
+  ctkCommandLineParser(QSettings* settings = 0);
+
   ~ctkCommandLineParser();
   
   /**
@@ -46,7 +130,7 @@ public:
    * @return The error description, empty if no error occured.
    * @see parseArguments(const QStringList&, bool*)
    */
-  QString errorString();
+  QString errorString() const;
   
   /**
    * This method returns all unparsed arguments, i.e. all arguments
@@ -57,7 +141,7 @@ public:
    *
    * @return A list containing unparsed arguments.
    */
-  const QStringList& unparsedArguments();
+  const QStringList& unparsedArguments() const;
   
   /**
    * Checks if the given argument has been added via a call
@@ -69,7 +153,7 @@ public:
    * @return <code>true</code> if the argument was added, <code>false</code>
    *         otherwise.
    */
-  bool argumentAdded(const QString& argument);
+  bool argumentAdded(const QString& argument) const;
 
   /**
    * Checks if the given argument has been parsed successfully by a previous
@@ -79,7 +163,7 @@ public:
    * @return <code>true</code> if the argument was parsed, <code>false</code>
    *         otherwise.
    */
-  bool argumentParsed(const QString& argument);
+  bool argumentParsed(const QString& argument) const;
 
   /**
    * Adds a command line argument. An argument can have a long name
@@ -102,7 +186,12 @@ public:
    *
    * Optionally, a help string and a default value can be provided for the argument. If
    * the QVariant type of the default value does not match <code>type</code>, an
-   * exception is thrown.
+   * exception is thrown. Arguments with default values are always returned by
+   * <code>parseArguments()</code>.
+   *
+   * You can also declare an argument deprecated, by setting <code>deprecated</code>
+   * to <code>true</code>. Alternatively you can add a deprecated argument by calling
+   * <code>addDeprecatedArgument()</code>.
    *
    * If the long or short argument has already been added, or if both are empty strings,
    * the method call has no effect.
@@ -113,14 +202,32 @@ public:
    * @param argHelp A help string describing the argument.
    * @param defaultValue A default value for the argument.
    * @param ignoreRest All arguments after the current one will be ignored.
+   * @param deprecated Declares the argument deprecated.
    *
    * @see setExactMatchRegularExpression()
+   * @see addDeprecatedArgument()
    * @throws std::logic_error If the QVariant type of <code>defaultValue</code>
    *         does not match <code>type</code>, a <code>std::logic_error</code> is thrown.
    */
   void addArgument(const QString& longarg, const QString& shortarg,
                    QVariant::Type type, const QString& argHelp = QString(),
-                   const QVariant& defaultValue = QVariant(), bool ignoreRest = false);
+                   const QVariant& defaultValue = QVariant(),
+                   bool ignoreRest = false, bool deprecated = false);
+
+  /**
+   * Adds a deprecated command line argument. If a deprecated argument is provided
+   * on the command line, <code>argHelp</code> is displayed in the console and
+   * processing continues with the next argument.
+   *
+   * Deprecated arguments are grouped separately at the end of the help text
+   * returned by <code>helpText()</code>.
+   *
+   * @param longarg The long argument name.
+   * @param shortarg The short argument name.
+   * @param argHelp A help string describing alternatives to the deprecated argument.
+   */
+  void addDeprecatedArgument(const QString& longarg, const QString& shortarg,
+                             const QString& argHelp);
 
   /**
    * Sets a custom regular expression for validating argument parameters. The method
@@ -139,17 +246,114 @@ public:
   bool setExactMatchRegularExpression(const QString& argument, const QString& expression,
                                       const QString& exactMatchFailedMessage);
 
-  int fieldWidth();
+  /**
+   * The field width for the argument names without the help text.
+   *
+   * @return The argument names field width in the help text.
+   */
+  int fieldWidth() const;
 
   /**
    * Creates a help text containing properly formatted argument names and help strings
-   * provided by calls to <code>addArgument()</code>.
+   * provided by calls to <code>addArgument()</code>. The arguments can be grouped by
+   * using <code>beginGroup()</code> and <code>endGroup()</code>.
    *
    * @param charPad The padding character.
    * @return The formatted help text.
    */
-  QString helpText(const char charPad = ' ');
-  
+  QString helpText(const char charPad = ' ') const;
+
+  /**
+   * Sets the argument prefix for long and short argument names. This can be used
+   * to create native command line arguments without changing the calls to
+   * <code>addArgument()</code>. For example on Unix-based systems, long argument
+   * names start with "--" and short names with "-", while on Windows argument names
+   * always start with "/".
+   *
+   * Note that all methods in ctkCommandLineParser which take an argument name
+   * expect the name as it was supplied to <code>addArgument</code>.
+   *
+   * Example usage:
+   *
+   * \code
+   * ctkCommandLineParser parser;
+   * parser.setArgumentPrefix("--", "-");
+   * parser.addArgument("long-argument", "l", QVariant::String);
+   * QStringList args;
+   * args << "program name" << "--long-argument Hi";
+   * parser.parseArguments(args);
+   * \endcode
+   *
+   * @param longPrefix The prefix for long argument names.
+   * @param shortPrefix The prefix for short argument names.
+   */
+  void setArgumentPrefix(const QString& longPrefix, const QString& shortPrefix);
+
+  /**
+   * Begins a new group for documenting arguments. All newly added arguments via
+   * <code>addArgument()</code> will be put in the new group. You can close the
+   * current group by calling <code>endGroup()</code> or be opening a new group.
+   *
+   * Note that groups cannot be nested and all arguments which do not belong to
+   * a group will be listed at the top of the text created by <code>helpText()</code>.
+   *
+   * @param description The description of the group
+   */
+  void beginGroup(const QString& description);
+
+  /**
+   * Ends the current group.
+   *
+   * @see beginGroup(const QString&)
+   */
+  void endGroup();
+
+  /**
+   * Enables QSettings support in ctkCommandLineParser. If an argument name is found
+   * in the QSettings instance with a valid QVariant, the value is considered as
+   * a default value and overwrites default values registered with
+   * <code>addArgument()</code>. User supplied values on the command line overwrite
+   * values in the QSettings instance, except for arguments with multiple parameters
+   * which are merged with QSettings values. Call <code>mergeSettings(false)</code>
+   * to disable merging.
+   *
+   * See <code>ctkCommandLineParser(QSettings*)</code> for information about how to
+   * supply a QSettings instance.
+   *
+   * Additionally, a long and short argument name can be specified which will disable
+   * QSettings support if supplied on the command line. The argument name must be
+   * registered as a regular argument via <code>addArgument()</code>.
+   *
+   * @param disableLongArg Long argument name.
+   * @param disalbeShortArg Short argument name.
+   *
+   * @see ctkCommandLineParser(QSettings*)
+   */
+  void enableSettings(const QString& disableLongArg = "",
+                      const QString& disableShortArg = "");
+
+  /**
+   * Controlls the merging behavior of user values and QSettings values.
+   *
+   * If merging is on (the default), user supplied values for an argument
+   * which can take more than one parameter are merged with values stored
+   * in the QSettings instance. If merging is off, the user values overwrite
+   * the QSettings values.
+   *
+   * @param merge <code>true</code> enables QSettings merging, <code>false</code>
+   *        disables it.
+   */
+  void mergeSettings(bool merge);
+
+  /**
+   * Can be used to check if QSettings support has been enabled by a call to
+   * <code>enableSettings()</code>.
+   *
+   * @return <code>true</code> if QSettings support is enabled, <code>false</code>
+   *         otherwise.
+   */
+  bool settingsEnabled() const;
+
 private:
   class ctkInternal;
   ctkInternal * Internal;