FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
arg_parser.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2024-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7/**
8 * @file arg_parser.cpp
9 * @brief Implementation of the command-line argument parser for the application.
10 */
11
12#include "arg_parser.h"
13
14#include <algorithm>
15#include <cctype>
16#include <cmath>
17#include <cstdint>
18#include <exception>
19#include <filesystem>
20#include <format>
21#include <iostream>
22#include <limits>
23#include <unordered_map>
24#include <utility>
25#include <vector>
26
27namespace
28{
29 /**
30 * @brief Checks if the given file has a valid log file extension.
31 *
32 * @param filePath The path of the log file.
33 * @return true if the file has a valid extension, false otherwise.
34 */
35 bool isValidLogFileExtension(const std::string& filePath) noexcept
36 {
37 static const std::vector<std::string> VALID_EXTENSIONS = {".log", ".txt"};
38 std::string extension = std::filesystem::path(filePath).extension().string();
39 std::ranges::transform(extension, extension.begin(), tolower);
40 return std::ranges::find(VALID_EXTENSIONS, extension) != VALID_EXTENSIONS.end();
41 }
42
43 /**
44 * @brief Parses the logging level from a string representation.
45 *
46 * @param level The string representation of the logging level.
47 * @return std::optional<Level> The corresponding logging level, or `std::nullopt` if invalid.
48 */
49 std::optional<fers_log_level_t> parseLogLevel(const std::string& level) noexcept
50 {
51 static const std::unordered_map<std::string, fers_log_level_t> LEVEL_MAP = {
52 {"TRACE", FERS_LOG_TRACE}, {"DEBUG", FERS_LOG_DEBUG}, {"INFO", FERS_LOG_INFO},
53 {"WARNING", FERS_LOG_WARNING}, {"ERROR", FERS_LOG_ERROR}, {"FATAL", FERS_LOG_FATAL}};
54
55 if (const auto it = LEVEL_MAP.find(level); it != LEVEL_MAP.end())
56 {
57 return it->second;
58 }
59 return std::nullopt;
60 }
61
62 bool isUnsignedDecimal(const std::string& value) noexcept
63 {
64 return !value.empty() &&
65 std::ranges::all_of(value, [](const unsigned char ch) { return std::isdigit(ch) != 0; });
66 }
67
68 std::expected<std::uint64_t, std::string> parseUnsigned64(const std::string& value,
69 const std::string& field_name) noexcept
70 {
71 if (!isUnsignedDecimal(value))
72 {
73 return std::unexpected(field_name + " must be an unsigned decimal integer");
74 }
75 try
76 {
77 std::size_t consumed = 0;
78 const auto parsed = std::stoull(value, &consumed, 10);
79 if (consumed != value.size())
80 {
81 return std::unexpected(field_name + " must be an unsigned decimal integer");
82 }
83 return static_cast<std::uint64_t>(parsed);
84 }
85 catch (const std::exception&)
86 {
87 return std::unexpected(field_name + " is out of range");
88 }
89 }
90
91 std::expected<double, std::string> parsePositiveReal(const std::string& value,
92 const std::string& field_name) noexcept
93 {
94 try
95 {
96 std::size_t consumed = 0;
97 const double parsed = std::stod(value, &consumed);
98 if (consumed != value.size() || !std::isfinite(parsed) || parsed <= 0.0)
99 {
100 return std::unexpected(field_name + " must be a positive real number");
101 }
102 return parsed;
103 }
104 catch (const std::exception&)
105 {
106 return std::unexpected(field_name + " must be a positive real number");
107 }
108 }
109
110 std::expected<void, std::string> handleVita49Endpoint(const std::string& value, core::Config& config) noexcept
111 {
112 const std::size_t separator = value.rfind(':');
113 if (separator == std::string::npos || separator == 0 || separator + 1 == value.size())
114 {
115 return std::unexpected("Invalid VITA49 endpoint: expected host:port");
116 }
117
118 const std::string host = value.substr(0, separator);
119 const std::string port_text = value.substr(separator + 1);
120 if (host.find(':') != std::string::npos)
121 {
122 return std::unexpected("Invalid VITA49 endpoint: expected host:port");
123 }
124
125 const auto parsed_port = parseUnsigned64(port_text, "VITA49 port");
126 if (!parsed_port)
127 {
128 return std::unexpected(parsed_port.error());
129 }
130 if (*parsed_port == 0 || *parsed_port > std::numeric_limits<std::uint16_t>::max())
131 {
132 return std::unexpected("VITA49 port must be in the range 1..65535");
133 }
134
135 config.vita49_enabled = true;
136 config.vita49_host = host;
137 config.vita49_port = static_cast<std::uint16_t>(*parsed_port);
138 return {};
139 }
140
141 std::expected<void, std::string> handleVita49Fullscale(const std::string& value, core::Config& config) noexcept
142 {
143 const auto parsed = parsePositiveReal(value, "VITA49 fullscale");
144 if (!parsed)
145 {
146 return std::unexpected(parsed.error());
147 }
148 config.vita49_fullscale = *parsed;
149 return {};
150 }
151
152 std::expected<void, std::string> handleVita49Epoch(const std::string& value, core::Config& config) noexcept
153 {
154 constexpr std::uint64_t nanoseconds_per_second = 1'000'000'000ULL;
155 constexpr std::uint64_t max_vrt_utc_epoch_ns =
156 static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max()) * nanoseconds_per_second +
157 (nanoseconds_per_second - 1ULL);
158 const auto parsed = parseUnsigned64(value, "VITA49 epoch");
159 if (!parsed)
160 {
161 return std::unexpected(parsed.error());
162 }
163 if (*parsed > max_vrt_utc_epoch_ns)
164 {
165 return std::unexpected("VITA49 epoch must fit the VRT 32-bit UTC seconds timestamp field");
166 }
167 config.vita49_epoch_unix_nanoseconds = *parsed;
168 return {};
169 }
170
171 std::expected<void, std::string> handleVita49MaxUdpPayload(const std::string& value, core::Config& config) noexcept
172 {
173 const auto parsed = parseUnsigned64(value, "VITA49 max UDP payload");
174 if (!parsed)
175 {
176 return std::unexpected(parsed.error());
177 }
178 if (*parsed < 64u || *parsed > 65'507u)
179 {
180 return std::unexpected("VITA49 max UDP payload must be between 64 and 65507 bytes");
181 }
182 config.vita49_max_udp_payload = static_cast<std::uint16_t>(*parsed);
183 return {};
184 }
185
186 std::expected<void, std::string> handleVita49QueueDepth(const std::string& value, core::Config& config) noexcept
187 {
188 const auto parsed = parseUnsigned64(value, "VITA49 queue depth");
189 if (!parsed)
190 {
191 return std::unexpected(parsed.error());
192 }
193 if (*parsed == 0u || *parsed > std::numeric_limits<std::uint32_t>::max())
194 {
195 return std::unexpected("VITA49 queue depth must be in the range 1..4294967295");
196 }
197 config.vita49_queue_depth = static_cast<std::uint32_t>(*parsed);
198 return {};
199 }
200
201 using ConfigValueHandler = std::expected<void, std::string> (*)(const std::string&, core::Config&) noexcept;
202
203 std::optional<ConfigValueHandler> valueOptionHandler(const std::string& arg) noexcept
204 {
205 if (arg == "--vita49")
206 {
207 return handleVita49Endpoint;
208 }
209 if (arg == "--vita49-fullscale")
210 {
211 return handleVita49Fullscale;
212 }
213 if (arg == "--vita49-epoch")
214 {
215 return handleVita49Epoch;
216 }
217 if (arg == "--vita49-max-udp-payload")
218 {
219 return handleVita49MaxUdpPayload;
220 }
221 if (arg == "--vita49-queue-depth")
222 {
223 return handleVita49QueueDepth;
224 }
225 return std::nullopt;
226 }
227
228 std::expected<void, std::string> validateVita49Options(const core::Config& config) noexcept
229 {
230 if (!config.vita49_enabled)
231 {
232 if (config.vita49_fullscale.has_value())
233 {
234 return std::unexpected("--vita49-fullscale requires --vita49");
235 }
236 if (config.vita49_epoch_unix_nanoseconds.has_value())
237 {
238 return std::unexpected("--vita49-epoch requires --vita49");
239 }
240 if (config.vita49_max_udp_payload.has_value())
241 {
242 return std::unexpected("--vita49-max-udp-payload requires --vita49");
243 }
244 if (config.vita49_queue_depth.has_value())
245 {
246 return std::unexpected("--vita49-queue-depth requires --vita49");
247 }
248 return {};
249 }
250 if (!config.vita49_fullscale.has_value())
251 {
252 return std::unexpected("--vita49-fullscale is required when --vita49 is used");
253 }
254 return {};
255 }
256
257 /**
258 * @brief Handles the log-level argument and sets the logging level.
259 *
260 * @param arg The log-level argument string.
261 * @param config The configuration object to update.
262 * @return std::expected<void, std::string> An expected object with an error message if the log level is invalid.
263 */
264 std::expected<void, std::string> handleLogLevel(const std::string& arg, core::Config& config) noexcept
265 {
266 const std::string level_str = arg.substr(12);
267 if (const auto level = parseLogLevel(level_str))
268 {
269 config.log_level = *level;
270 return {};
271 }
272
273 std::cerr << "[ERROR] Invalid log level '" << level_str << "'\n";
274 return std::unexpected("Invalid log level: " + level_str);
275 }
276
277 /**
278 * @brief Handles the log-file argument and sets the log file path.
279 *
280 * @param arg The log-file argument string.
281 * @param config The configuration object to update.
282 * @return std::expected<void, std::string> An expected object with an error message if the log file path is
283 * invalid.
284 */
285 std::expected<void, std::string> handleLogFile(const std::string& arg, core::Config& config) noexcept
286 {
287 std::string const log_file_path = arg.substr(11);
288 if (isValidLogFileExtension(log_file_path))
289 {
290 config.log_file = log_file_path;
291 return {};
292 }
293
294 std::cerr << "[ERROR] Invalid log file extension. Must be .log or .txt\n";
295 return std::unexpected("Invalid log file extension: " + log_file_path);
296 }
297
298 /**
299 * @brief Handles the number of threads argument and sets the number of threads.
300 *
301 * @param arg The number of threads argument string.
302 * @param config The configuration object to update.
303 * @return std::expected<void, std::string> An expected object with an error message if the number of threads is
304 * invalid.
305 */
306 std::expected<void, std::string> handleNumThreads(const std::string& arg, core::Config& config) noexcept
307 {
308 try
309 {
310 const int requested_threads = std::stoi(arg.substr(3));
311 if (requested_threads <= 0)
312 {
313 return std::unexpected("Number of threads must be greater than 0");
314 }
315
316 config.num_threads = static_cast<unsigned>(requested_threads);
317 if (const unsigned max_threads = std::thread::hardware_concurrency();
318 max_threads > 0 && config.num_threads > max_threads)
319 {
320 std::cerr << "[WARNING] Thread count exceeds available processors. Clamping.\n";
321 config.num_threads = max_threads;
322 }
323 return {};
324 }
325 catch (const std::exception&)
326 {
327 return std::unexpected("Invalid number of threads specified.");
328 }
329 }
330
331 /**
332 * @brief Handles the command-line argument and updates the configuration.
333 *
334 * @param arg The command-line argument string.
335 * @param config The configuration object to update.
336 * @param scriptFileSet A flag indicating if the script file has been set.
337 * @param programName The name of the program executable.
338 * @return std::expected<void, std::string> An expected object with an error message if the argument is invalid.
339 */
340 std::expected<void, std::string> handleArgument(const std::string& arg, core::Config& config, bool& scriptFileSet,
341 const char* programName) noexcept
342 {
343 if (arg == "--help" || arg == "-h")
344 {
345 core::showHelp(programName);
346 return std::unexpected("Help requested.");
347 }
348 if (arg == "--version" || arg == "-v")
349 {
351 return std::unexpected("Version requested.");
352 }
353 if (arg.rfind("--log-level=", 0) == 0)
354 {
355 return handleLogLevel(arg, config);
356 }
357 if (arg.rfind("--log-file=", 0) == 0)
358 {
359 return handleLogFile(arg, config);
360 }
361 if (arg.rfind("-n=", 0) == 0)
362 {
363 return handleNumThreads(arg, config);
364 }
365 if (arg.rfind("--out-dir=", 0) == 0)
366 {
367 config.output_dir = arg.substr(10);
368 return {};
369 }
370 if (arg == "--no-validate")
371 {
372 config.validate = false;
373 return {};
374 }
375 if (arg == "--kml")
376 {
377 config.generate_kml = true;
378 return {};
379 }
380 if (arg.rfind("--kml=", 0) == 0)
381 {
382 config.generate_kml = true;
383 config.kml_file = arg.substr(6);
384 return {};
385 }
386 if (arg[0] != '-' && !scriptFileSet)
387 {
388 config.script_file = arg;
389 scriptFileSet = true;
390 return {};
391 }
392
393 std::cerr << "[ERROR] Unrecognized option: '" << arg << "'\n";
394 return std::unexpected("Unrecognized argument: " + arg);
395 }
396}
397
398namespace core
399{
400 void showHelp(const char* programName) noexcept
401 {
402 const char* version = fers_get_version();
403
404 std::cout << "/------------------------------------------------\\\n"
405 << "| FERS - The Flexible Extensible Radar Simulator |\n"
406 << std::format("| Version {:<40}|\n", version)
407 << "\\------------------------------------------------/\n"
408 << "Usage: " << programName << R"( <scriptfile> [options]
409
410Options:
411 --help, -h Show this help message and exit
412 --version, -v Show version information and exit
413 --no-validate Disable XML schema validation before running.
414 --kml[=<file>] Generate a KML visualization of the scenario and exit. If a filename
415 is provided, it will be used. Otherwise, it defaults to the scenario
416 name with a .kml extension in the output directory.
417 --out-dir=<dir> Set the output directory for simulation results and default KML output.
418 Defaults to the directory containing the script file.
419 --vita49 host:port Stream receiver output as the FERS VITA 49.2 UDP profile.
420 --vita49-fullscale <x> Set required positive fixed ADC full-scale for VITA int16 IQ output.
421 --vita49-epoch <ns> Set optional deterministic VITA epoch as Unix nanoseconds.
422 --vita49-max-udp-payload <bytes>
423 Set optional VITA UDP payload cap, 64..65507 bytes.
424 --vita49-queue-depth <packets>
425 Set optional VITA sender queue depth, greater than zero.
426 --log-level=<level> Set the logging level (TRACE, DEBUG, INFO, WARNING, ERROR, FATAL)
427 --log-file=<file> Log output to the specified .log or .txt file as well as the console.
428 -n=<threads> Number of threads to use
429
430Arguments:
431 <scriptfile> Path to the simulation script file (XML)
432
433Example:
434 )" << programName
435 << R"( simulation.fersxml --out-dir=./results --log-level=DEBUG -n=4
436
437This program runs radar simulations based on an XML script file.
438Make sure the script file follows the correct format to avoid errors.
439)";
440 }
441
442 void showVersion() noexcept
443 {
444 const char* version = fers_get_version();
445
446 std::cout << '\n'
447 << "/------------------------------------------------\\\n"
448 << "| FERS - The Flexible Extensible Radar Simulator |\n"
449 << std::format("| Version {:<40}|\n", version)
450 << "| Authors: Marc Brooker, Michael Inggs, |\n"
451 << "| and FERS Contributors |\n"
452 << "\\------------------------------------------------/\n\n";
453 }
454
455 std::expected<Config, std::string> parseArguments(const int argc, char* argv[]) noexcept
456 {
457 Config config;
458 bool script_file_set = false;
459
460 if (argc < 2)
461 {
462 showHelp(argv[0]);
463 return std::unexpected("No arguments provided.");
464 }
465
466 for (int i = 1; i < argc; ++i)
467 {
468 const std::string arg = argv[i];
469 const auto require_value = [&](const std::string& option) -> std::expected<std::string, std::string>
470 {
471 if (i + 1 >= argc)
472 {
473 return std::unexpected(option + " requires a value");
474 }
475 ++i;
476 return std::string{argv[i]};
477 };
478
479 if (const auto handler = valueOptionHandler(arg))
480 {
481 const auto value = require_value(arg);
482 if (!value)
483 {
484 return std::unexpected(value.error());
485 }
486 if (const auto result = (*handler)(*value, config); !result)
487 {
488 return std::unexpected(result.error());
489 }
490 continue;
491 }
492
493 if (const auto result = handleArgument(arg, config, script_file_set, argv[0]); !result)
494 {
495 return std::unexpected(result.error());
496 }
497 }
498
499 if (!script_file_set)
500 {
501 return std::unexpected("No script file provided.");
502 }
503 if (const auto result = validateVita49Options(config); !result)
504 {
505 return std::unexpected(result.error());
506 }
507 return config;
508 }
509}
@ FERS_LOG_FATAL
Fatal logging for unrecoverable failures.
Definition api.h:199
@ FERS_LOG_DEBUG
Debug-level diagnostic logging.
Definition api.h:195
@ FERS_LOG_ERROR
Error logging for failed operations.
Definition api.h:198
@ FERS_LOG_INFO
Informational logging.
Definition api.h:196
@ FERS_LOG_TRACE
Trace-level diagnostic logging.
Definition api.h:194
@ FERS_LOG_WARNING
Warning logging for recoverable issues.
Definition api.h:197
const char * fers_get_version(void)
Returns the library version string.
Definition api.cpp:377
Command-line argument parsing utilities for the application.
void showVersion() noexcept
Displays the version information.
std::expected< Config, std::string > parseArguments(const int argc, char *argv[]) noexcept
Parses command-line arguments.
void showHelp(const char *programName) noexcept
Displays the help message.
Configuration structure for the application.
Definition arg_parser.h:31