QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCCommandLineParser.cc
Go to the documentation of this file.
2#include "qgc_version.h"
3
4#include <QtCore/QCommandLineOption>
5#include <QtCore/QCoreApplication>
6
8
9QGC_LOGGING_CATEGORY(QGCCommandLineParserLog, "Utilities.QGCCommandLineParser")
10
11namespace QGCCommandLineParser {
12
13// ============================================================================
14// Option Name Constants
15// ============================================================================
16
17namespace {
18
19// --- Core options ---
20constexpr QLatin1StringView kOptSystemId = QLatin1StringView("system-id");
21constexpr QLatin1StringView kOptClearSettings = QLatin1StringView("clear-settings");
22constexpr QLatin1StringView kOptClearCache = QLatin1StringView("clear-cache");
23constexpr QLatin1StringView kOptLogging = QLatin1StringView("logging");
24constexpr QLatin1StringView kOptLogOutput = QLatin1StringView("log-output");
25constexpr QLatin1StringView kOptSimpleBoot = QLatin1StringView("simple-boot-test");
26
27#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
28// --- Desktop-only options ---
29constexpr QLatin1StringView kOptFakeMobile = QLatin1StringView("fake-mobile");
30constexpr QLatin1StringView kOptAllowMultiple = QLatin1StringView("allow-multiple");
31#endif
32
33#ifdef QGC_UNITTEST_BUILD
34// --- Test options ---
35constexpr QLatin1StringView kOptUnittest = QLatin1StringView("unittest");
36constexpr QLatin1StringView kOptUnittestStress = QLatin1StringView("unittest-stress");
37constexpr QLatin1StringView kOptUnittestOutput = QLatin1StringView("unittest-output");
38constexpr QLatin1StringView kOptUnittestLabel = QLatin1StringView("label");
39constexpr QLatin1StringView kOptListTests = QLatin1StringView("list-tests");
40#endif
41
42#ifdef Q_OS_WIN
43// --- Windows-only options ---
44constexpr QLatin1StringView kOptDesktop = QLatin1StringView("desktop");
45constexpr QLatin1StringView kOptNoWinAssertUI = QLatin1StringView("no-windows-assert-ui");
46#endif
47
48#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
49// --- Windows/macOS options ---
50constexpr QLatin1StringView kOptSwrast = QLatin1StringView("swrast");
51#endif
52
53} // anonymous namespace
54
55// ============================================================================
56// Argument Normalization
57// ============================================================================
58
62static QStringList normalizeArgs(const QStringList &args)
63{
64 QStringList out;
65 out.reserve(args.size() + 4);
66
67 for (const QString &arg : args) {
68#ifdef QGC_UNITTEST_BUILD
69 // Handle bare --unittest without a value
70 if (arg == QLatin1String("--unittest")) {
71 out << arg << QString(); // empty value token prevents "Missing value"
72 continue;
73 }
74#endif
75
76 // Convert --option:value to --option value
77 if (arg.startsWith(QLatin1String("--")) && arg.contains(QLatin1Char(':'))) {
78 const qsizetype idx = arg.indexOf(QLatin1Char(':'));
79 const QString opt = arg.left(idx);
80 const QString val = arg.mid(idx + 1);
81 out << opt;
82 if (!val.isEmpty()) {
83 out << val;
84 }
85#ifdef QGC_UNITTEST_BUILD
86 else if (opt == QLatin1String("--unittest")) {
87 out << QString();
88 }
89#endif
90 } else {
91 out << arg;
92 }
93 }
94
95 qCDebug(QGCCommandLineParserLog) << "Normalized arguments:" << out;
96 return out;
97}
98
99// ============================================================================
100// Command Line Parsing
101// ============================================================================
102
103CommandLineParseResult parseCommandLine()
104{
105 // Set application info (needed for --help and --version)
106 QCoreApplication::setApplicationName(QLatin1String(QGC_APP_NAME));
107 QCoreApplication::setApplicationVersion(QLatin1String(QGC_APP_VERSION_STR));
108
109 CommandLineParseResult out{};
110 out.parser = std::make_unique<QCommandLineParser>();
111
112 QCommandLineParser& parser = *out.parser;
113 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
114 parser.setOptionsAfterPositionalArgumentsMode(QCommandLineParser::ParseAsOptions);
115 parser.setApplicationDescription(QStringLiteral(QGC_APP_DESCRIPTION));
116
117 // --- Standard options ---
118 const QCommandLineOption helpOption = parser.addHelpOption();
119 const QCommandLineOption versionOption = parser.addVersionOption();
120
121 // --- Core options (always available) ---
122 const QCommandLineOption systemIdOpt(
123 QString(kOptSystemId),
124 QCoreApplication::translate("main", "MAVLink GCS system id."),
125 QCoreApplication::translate("main", "id"));
126 (void) parser.addOption(systemIdOpt);
127
128 const QCommandLineOption clearSettingsOpt(
129 QString(kOptClearSettings),
130 QCoreApplication::translate("main", "Clear stored application settings."));
131 (void) parser.addOption(clearSettingsOpt);
132
133 const QCommandLineOption clearCacheOpt(
134 QString(kOptClearCache),
135 QCoreApplication::translate("main", "Clear parameter and airframe caches."));
136 (void) parser.addOption(clearCacheOpt);
137
138 const QCommandLineOption loggingOpt(
139 QString(kOptLogging),
140 QCoreApplication::translate("main", "Enable logging with optional rules string."),
141 QCoreApplication::translate("main", "rules"));
142 (void) parser.addOption(loggingOpt);
143
144 const QCommandLineOption logOutputOpt(
145 QString(kOptLogOutput),
146 QCoreApplication::translate("main", "Log to console."));
147 (void) parser.addOption(logOutputOpt);
148
149 const QCommandLineOption simpleBootOpt(
150 QString(kOptSimpleBoot),
151 QCoreApplication::translate("main", "Initialize subsystems and exit."));
152 (void) parser.addOption(simpleBootOpt);
153
154#ifdef QGC_UNITTEST_BUILD
155 // --- Test options (only in test builds) ---
156 const QCommandLineOption unittestOpt(
157 QString(kOptUnittest),
158 QCoreApplication::translate("main", "Run unit tests (optional filter value)."),
159 QCoreApplication::translate("main", "filter"));
160 (void) parser.addOption(unittestOpt);
161
162 const QCommandLineOption unittestStressOpt(
163 QString(kOptUnittestStress),
164 QCoreApplication::translate("main", "Stress unit tests."),
165 QCoreApplication::translate("main", "count"));
166 (void) parser.addOption(unittestStressOpt);
167
168 const QCommandLineOption unittestOutputOpt(
169 QString(kOptUnittestOutput),
170 QCoreApplication::translate("main", "Output test results to file (JUnit XML format)."),
171 QCoreApplication::translate("main", "file"));
172 (void) parser.addOption(unittestOutputOpt);
173
174 const QCommandLineOption unittestLabelOpt(
175 QString(kOptUnittestLabel),
176 QCoreApplication::translate("main", "Filter tests by label (unit, integration, vehicle, missionmanager, etc.)."),
177 QCoreApplication::translate("main", "labels"));
178 (void) parser.addOption(unittestLabelOpt);
179
180 const QCommandLineOption listTestsOpt(
181 QString(kOptListTests),
182 QCoreApplication::translate("main", "List available unit tests and exit."));
183 (void) parser.addOption(listTestsOpt);
184#endif
185
186#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
187 // --- Desktop-only options ---
188 const QCommandLineOption fakeMobileOpt(
189 QString(kOptFakeMobile),
190 QCoreApplication::translate("main", "Run with mobile-style UI."));
191 (void) parser.addOption(fakeMobileOpt);
192
193 const QCommandLineOption allowMultipleOpt(
194 QString(kOptAllowMultiple),
195 QCoreApplication::translate("main", "Bypass single-instance guard."));
196 (void) parser.addOption(allowMultipleOpt);
197#endif
198
199#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
200 // --- Windows/macOS options ---
201 const QCommandLineOption swrastOpt(
202 QString(kOptSwrast),
203 QCoreApplication::translate("main", "Force software OpenGL."));
204 (void) parser.addOption(swrastOpt);
205#endif
206
207#ifdef Q_OS_WIN
208 // --- Windows-only options ---
209 const QCommandLineOption desktopOpt(
210 QString(kOptDesktop),
211 QCoreApplication::translate("main", "Force Desktop OpenGL."));
212 (void) parser.addOption(desktopOpt);
213
214 const QCommandLineOption quietWinAssertOpt(
215 QString(kOptNoWinAssertUI),
216 QCoreApplication::translate("main", "Disable Windows assert dialog boxes."));
217 (void) parser.addOption(quietWinAssertOpt);
218#endif
219
220 // --- Parse arguments ---
221 const QStringList normalizedArgs = normalizeArgs(QCoreApplication::arguments());
222 parser.process(normalizedArgs);
223
224 // --- Validate unknown options ---
225 out.unknownOptions = parser.unknownOptionNames();
226
227 // Check for platform-specific option errors on wrong platform
228 if (!out.unknownOptions.isEmpty()) {
229 // Check for test options in non-test build
230 if (out.unknownOptions.contains(QLatin1String("unittest")) ||
231 out.unknownOptions.contains(QLatin1String("unittest-stress")) ||
232 out.unknownOptions.contains(QLatin1String("unittest-output")) ||
233 out.unknownOptions.contains(QLatin1String("list-tests"))) {
234#ifndef QGC_UNITTEST_BUILD
235 out.statusCode = CommandLineParseResult::Status::Error;
236 out.errorString = QCoreApplication::translate("main",
237 "--unittest/--unittest-stress/--unittest-output/--list-tests options are only available in unittest builds.");
238 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
239 return out;
240#endif
241 }
242
243#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
244 // Mobile platforms don't support desktop options
245 if (out.unknownOptions.contains(QLatin1String("fake-mobile")) ||
246 out.unknownOptions.contains(QLatin1String("allow-multiple"))) {
247 out.statusCode = CommandLineParseResult::Status::Error;
248 out.errorString = QCoreApplication::translate("main",
249 "--fake-mobile/--allow-multiple are not supported on mobile platforms.");
250 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
251 return out;
252 }
253#endif
254
255#ifndef Q_OS_WIN
256 // Non-Windows platforms don't support Windows-specific options
257 if (out.unknownOptions.contains(QLatin1String("desktop")) ||
258 out.unknownOptions.contains(QLatin1String("no-windows-assert-ui"))) {
259 out.statusCode = CommandLineParseResult::Status::Error;
260 out.errorString = QCoreApplication::translate("main",
261 "--desktop/--no-windows-assert-ui are only supported on Windows.");
262 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
263 return out;
264 }
265#endif
266
267#if !defined(Q_OS_WIN) && !defined(Q_OS_MACOS)
268 // Non-Windows/macOS platforms don't support swrast
269 if (out.unknownOptions.contains(QLatin1String("swrast"))) {
270 out.statusCode = CommandLineParseResult::Status::Error;
271 out.errorString = QCoreApplication::translate("main",
272 "--swrast is only supported on Windows and macOS.");
273 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
274 return out;
275 }
276#endif
277
278 // Generic unknown options error
279 out.statusCode = CommandLineParseResult::Status::Error;
280 out.errorString = QCoreApplication::translate("main", "Unknown options: %1")
281 .arg(out.unknownOptions.join(QLatin1String(", ")));
282 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
283 return out;
284 }
285
286 // --- Validate positional arguments ---
287 out.positional = parser.positionalArguments();
288 if (!out.positional.isEmpty()) {
289 out.statusCode = CommandLineParseResult::Status::Error;
290 out.errorString = QCoreApplication::translate("main", "Unexpected positional arguments: %1")
291 .arg(out.positional.join(QLatin1String(", ")));
292 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
293 return out;
294 }
295
296 // --- Handle help/version requests ---
297 if (parser.isSet(helpOption)) {
298 out.statusCode = CommandLineParseResult::Status::HelpRequested;
299 out.helpText = parser.helpText();
300 return out;
301 }
302
303 if (parser.isSet(versionOption)) {
304 out.statusCode = CommandLineParseResult::Status::VersionRequested;
305 out.versionText = QCoreApplication::applicationVersion();
306 return out;
307 }
308
309 // --- Parse core options ---
310 if (parser.isSet(systemIdOpt)) {
311 const QString systemIdStr = parser.value(systemIdOpt);
312 bool ok = false;
313 const uint systemId = systemIdStr.toUInt(&ok);
314 if (!ok || (systemId < 1) || (systemId > 255)) {
315 out.statusCode = CommandLineParseResult::Status::Error;
316 out.errorString = QCoreApplication::translate("main", "Invalid System ID (must be 1-255): %1")
317 .arg(systemIdStr);
318 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
319 return out;
320 }
321 out.systemId = static_cast<quint8>(systemId);
322 qCDebug(QGCCommandLineParserLog) << "System ID:" << systemId;
323 }
324
325 out.clearSettingsOptions = parser.isSet(clearSettingsOpt);
326 out.clearCache = parser.isSet(clearCacheOpt);
327 if (parser.isSet(loggingOpt)) {
328 out.loggingOptions = parser.value(loggingOpt);
329 qCDebug(QGCCommandLineParserLog) << "Logging options:" << out.loggingOptions.value();
330 }
331 out.logOutput = parser.isSet(logOutputOpt);
332 out.simpleBootTest = parser.isSet(simpleBootOpt);
333
334#ifdef QGC_UNITTEST_BUILD
335 // --- Parse test options ---
336 if (parser.isSet(unittestOpt)) {
337 out.runningUnitTests = true;
338 const QStringList vals = parser.values(unittestOpt);
339 // Filter out empty values (from bare --unittest)
340 for (const QString& val : vals) {
341 if (!val.isEmpty()) {
342 out.unitTests.append(val);
343 }
344 }
345 qCDebug(QGCCommandLineParserLog) << "Unit tests:" << (out.unitTests.isEmpty() ? QStringLiteral("all") : out.unitTests.join(QLatin1String(", ")));
346 }
347
348 if (parser.isSet(unittestStressOpt)) {
349 out.runningUnitTests = true;
350 out.stressUnitTests = true;
351 const QString stress = parser.value(unittestStressOpt);
352 if (stress.isEmpty()) {
353 out.stressUnitTestsCount = 20;
354 } else {
355 bool ok = false;
356 const uint count = stress.toUInt(&ok);
357 if (!ok || (count == 0)) {
358 out.statusCode = CommandLineParseResult::Status::Error;
359 out.errorString = QCoreApplication::translate("main", "Invalid stress test count (must be > 0): %1")
360 .arg(stress);
361 qCWarning(QGCCommandLineParserLog) << out.errorString.value();
362 return out;
363 }
364 out.stressUnitTestsCount = count;
365 }
366 qCDebug(QGCCommandLineParserLog) << "Stress test iterations:" << out.stressUnitTestsCount;
367 }
368
369 if (parser.isSet(unittestOutputOpt)) {
370 out.unitTestOutput = parser.value(unittestOutputOpt);
371 qCDebug(QGCCommandLineParserLog) << "Test output file:" << out.unitTestOutput.value();
372 }
373
374 if (parser.isSet(unittestLabelOpt)) {
375 out.labelFilter = parser.value(unittestLabelOpt);
376 qCDebug(QGCCommandLineParserLog) << "Label filter:" << out.labelFilter.value();
377 }
378
379 out.listTests = parser.isSet(listTestsOpt);
380 if (out.listTests) {
381 qCDebug(QGCCommandLineParserLog) << "List tests requested";
382 }
383#endif
384
385 // --- Parse desktop options ---
386#if !defined(Q_OS_ANDROID) && !defined(Q_OS_IOS)
387 out.fakeMobile = parser.isSet(fakeMobileOpt);
388 out.allowMultiple = parser.isSet(allowMultipleOpt);
389#else
390 out.fakeMobile = false;
391 out.allowMultiple = false;
392#endif
393
394 // --- Parse graphics options ---
395#ifdef Q_OS_WIN
396 out.useDesktopGL = parser.isSet(desktopOpt);
397 out.quietWindowsAsserts = parser.isSet(quietWinAssertOpt);
398#else
399 out.useDesktopGL = false;
400 out.quietWindowsAsserts = false;
401#endif
402
403#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
404 out.useSwRast = parser.isSet(swrastOpt);
405#else
406 out.useSwRast = false;
407#endif
408
409 out.statusCode = CommandLineParseResult::Status::Ok;
410 qCDebug(QGCCommandLineParserLog) << "Command line parsing completed successfully";
411
412 return out;
413}
414
415CommandLineParseResult parse(int argc, char* argv[])
416{
417#ifdef QGC_UNITTEST_BUILD
418 overrideCommandLine(argc, argv);
419#endif
420 const QCoreApplication app(argc, argv);
421 return parseCommandLine();
422}
423
424std::optional<int> handleParseResult(const CommandLineParseResult& result)
425{
426 switch (result.statusCode) {
427 case CommandLineParseResult::Status::Error: {
428 const QString msg = result.errorString.value_or(QStringLiteral("Unknown error"));
429 QCommandLineParser::showMessageAndExit(QCommandLineParser::MessageType::Error, msg, 1);
430 Q_UNREACHABLE();
431 }
432 case CommandLineParseResult::Status::HelpRequested:
433 case CommandLineParseResult::Status::VersionRequested:
434 return 0;
435 case CommandLineParseResult::Status::Ok:
436 return std::nullopt;
437 }
438 Q_UNREACHABLE();
439}
440
441AppMode determineAppMode(const CommandLineParseResult& args)
442{
443#ifdef QGC_UNITTEST_BUILD
444 if (args.listTests) {
445 return AppMode::ListTests;
446 }
447 if (args.runningUnitTests) {
448 return AppMode::Test;
449 }
450#endif
451 if (args.simpleBootTest) {
452 return AppMode::BootTest;
453 }
454 return AppMode::Gui;
455}
456
457#ifdef QGC_UNITTEST_BUILD
458void overrideCommandLine(int& argc, char**& argv)
459{
460 constexpr bool enabled = false; // Set to true to enable
461 if constexpr (!enabled) {
462 return;
463 }
464
465 static char arg0[256];
466 static char arg1[] = "--unittest:FTPManagerTest";
467 static char arg2[] = "--logging:Vehicle.FTPManager";
468 static char* newArgv[] = { arg0, arg1, arg2, nullptr };
469
470 qstrncpy(arg0, argv[0], sizeof(arg0));
471 argc = 3;
472 argv = newArgv;
473}
474#endif
475
476} // namespace QGCCommandLineParser
#define QGC_LOGGING_CATEGORY(name, categoryStr)