diff --git a/src/config/config_interface.h b/src/config/config_interface.h index c8c3d3132..285a2df2c 100644 --- a/src/config/config_interface.h +++ b/src/config/config_interface.h @@ -74,6 +74,11 @@ std::variant getConfig( try { VerifiedCommandLineArguments cmd_source = CommandLineArguments{cmd}.verify(config_specification); + if (cmd_source.verbose_count == 1) { + spdlog::set_level(spdlog::level::debug); + } else if (cmd_source.verbose_count >= 2) { + spdlog::set_level(spdlog::level::trace); + } if (cmd_source.asks_for_help) { std::cout << config_specification.helpText() << "\n" << std::flush; return 0; diff --git a/src/config/config_specification.cpp b/src/config/config_specification.cpp index 03160ddf4..5edfb231d 100644 --- a/src/config/config_specification.cpp +++ b/src/config/config_specification.cpp @@ -66,7 +66,11 @@ std::string ConfigSpecification::helpText() const { << "\n" << " -h | --help\n" << "\n" - << " Show help.\n"; + << " Show help.\n" + << "\n" + << " -v | --verbose\n" + << "\n" + << " Increase log verbosity. Pass once for debug level, twice for trace level.\n"; auto addln = [&help_text](const std::string& line) { help_text << line << "\n"; }; for (const auto& field_spec : attribute_specifications) { diff --git a/src/config/source/command_line_arguments.cpp b/src/config/source/command_line_arguments.cpp index ea49af122..b60e54e85 100644 --- a/src/config/source/command_line_arguments.cpp +++ b/src/config/source/command_line_arguments.cpp @@ -12,9 +12,7 @@ namespace silo::config { std::string CommandLineArguments::configKeyPathToString(const ConfigKeyPath& key_path) { std::vector result; for (const auto& sublevel : key_path.getPath()) { - for (const std::string& current_string : sublevel) { - result.push_back(current_string); - } + std::ranges::copy(sublevel, std::back_inserter(result)); } return "--" + boost::join(result, "-"); } @@ -89,20 +87,52 @@ std::tuple> parseValueFromArg( return {attribute_spec.parseValueFromString(value_string), remaining_args}; } +std::vector expandArguments(const std::vector& args) { + std::vector expanded_args; + for (auto iter = args.begin(); iter != args.end(); ++iter) { + const auto& arg = *iter; + if (arg == "--") { + // Everything after "--" is a positional argument; stop expanding. + std::ranges::copy(iter, args.end(), std::back_inserter(expanded_args)); + break; + } + if (!arg.starts_with('-') || arg.starts_with("--")) { + expanded_args.push_back(arg); + } else { + for (char shorthand : arg.substr(1)) { + if (shorthand == 'v') { + expanded_args.emplace_back("--verbose"); + } else if (shorthand == 'h') { + expanded_args.emplace_back("--help"); + } else { + // Unknown short option — pass through to trigger a helpful error below. + expanded_args.push_back(fmt::format("-{}", shorthand)); + } + } + } + } + return expanded_args; +} + } // namespace VerifiedCommandLineArguments CommandLineArguments::verify( const ConfigSpecification& config_specification ) const { - // Now, given config_specification (and thus which options are - // boolean and which take arguments), we can parse the command - // line. + // First pass: expand short-option clusters into long-form equivalents so the + // second pass can treat everything uniformly. + // E.g. -vvh → --verbose --verbose --help + std::vector expanded_args = expandArguments(args); + // Second pass: process the fully-expanded argument list. // E.g. "--api-foo" => "1234" or "--api-foo=1234" std::unordered_map config_value_by_option; - std::vector positional_args; std::vector invalid_config_keys; - std::span remaining_args{args.data(), args.size()}; + std::vector positional_args; + + uint32_t verbose_count = 0; + + std::span remaining_args{expanded_args.data(), expanded_args.size()}; while (!remaining_args.empty()) { const std::string& arg = remaining_args[0]; remaining_args = remaining_args.subspan(1); @@ -111,9 +141,13 @@ VerifiedCommandLineArguments CommandLineArguments::verify( std::ranges::copy(remaining_args, std::back_inserter(positional_args)); break; } - if (arg == "-h" || arg == "--help") { + if (arg == "--help") { return VerifiedCommandLineArguments::askingForHelp(); } + if (arg == "--verbose") { + ++verbose_count; + continue; + } const auto [option, opt_value_string] = splitOption(arg); const auto ambiguous_key = stringToConfigKeyPath(option); if (auto opt = @@ -122,9 +156,7 @@ VerifiedCommandLineArguments CommandLineArguments::verify( const auto [value, new_remaining_args] = parseValueFromArg(attribute_spec, arg, opt_value_string, remaining_args); remaining_args = new_remaining_args; - // Overwrite value with the last occurrence - // (i.e. `silo --foo 4 --foo 5` will leave "--foo" - // => "5" in the map). + // Keep the first occurrence; duplicate flags are silently ignored. config_value_by_option.emplace(attribute_spec.key, value); } else { invalid_config_keys.push_back(option); @@ -135,17 +167,17 @@ VerifiedCommandLineArguments CommandLineArguments::verify( } if (!invalid_config_keys.empty()) { - const char* keys_or_options = (invalid_config_keys.size() >= 2) ? "options" : "option"; + const char* option_or_options = (invalid_config_keys.size() == 1) ? "option" : "options"; throw silo::config::ConfigException(fmt::format( "in {}: unknown {} {}", debugContext(), - keys_or_options, + option_or_options, boost::join(invalid_config_keys, ", ") )); } return VerifiedCommandLineArguments::fromConfigValuesAndPositionalArguments( - std::move(config_value_by_option), std::move(positional_args) + std::move(config_value_by_option), std::move(positional_args), verbose_count ); } diff --git a/src/config/source/command_line_arguments.test.cpp b/src/config/source/command_line_arguments.test.cpp index 66f8fa4cc..76fb267ff 100644 --- a/src/config/source/command_line_arguments.test.cpp +++ b/src/config/source/command_line_arguments.test.cpp @@ -158,3 +158,34 @@ TEST(CommandLineArguments, testPositionalArgumentsStartingWithMinus) { ASSERT_EQ(verified.positional_arguments.at(0), "-positional_argument_with_minus"); } } + +TEST(CommandLineArguments, verboseFlagShortForm) { + std::vector arguments{"-v"}; + const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({}); + ASSERT_EQ(verified.verbose_count, 1U); + ASSERT_TRUE(verified.positional_arguments.empty()); +} + +TEST(CommandLineArguments, verboseFlagLongForm) { + std::vector arguments{"--verbose"}; + const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({}); + ASSERT_EQ(verified.verbose_count, 1U); +} + +TEST(CommandLineArguments, verboseFlagRepeated) { + std::vector arguments{"-v", "--verbose", "-v"}; + const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({}); + ASSERT_EQ(verified.verbose_count, 3U); +} + +TEST(CommandLineArguments, verboseFlagCluster) { + std::vector arguments{"-vvv"}; + const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({}); + ASSERT_EQ(verified.verbose_count, 3U); +} + +TEST(CommandLineArguments, verboseFlagDoesNotCountAsUnknownOption) { + std::vector arguments{"-v"}; + // Even with an empty specification (no known options), -v should not throw. + ASSERT_NO_THROW(((void)CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({}))); +} diff --git a/src/config/verified_config_attributes.cpp b/src/config/verified_config_attributes.cpp index 8825e6dd7..4fcaeded5 100644 --- a/src/config/verified_config_attributes.cpp +++ b/src/config/verified_config_attributes.cpp @@ -97,17 +97,20 @@ std::optional> VerifiedConfigAttributes::getList( VerifiedCommandLineArguments VerifiedCommandLineArguments::askingForHelp() { VerifiedCommandLineArguments result; result.asks_for_help = true; + result.verbose_count = 0; return result; } VerifiedCommandLineArguments VerifiedCommandLineArguments::fromConfigValuesAndPositionalArguments( std::unordered_map config_values, - std::vector positional_arguments + std::vector positional_arguments, + uint32_t verbose_count ) { VerifiedCommandLineArguments result; result.config_values = std::move(config_values); result.positional_arguments = std::move(positional_arguments); result.asks_for_help = false; + result.verbose_count = verbose_count; return result; } diff --git a/src/config/verified_config_attributes.h b/src/config/verified_config_attributes.h index 93f44e19f..fbfbe7d17 100644 --- a/src/config/verified_config_attributes.h +++ b/src/config/verified_config_attributes.h @@ -15,8 +15,8 @@ namespace silo::config { /// The accessors return an option since even though invalid options are /// not present in this, the given option may also not be present. /// -/// `positional_arguments` and `asks_for_help` are only used by the command -/// line argument backend, other backends leave them empty/false. +/// `positional_arguments`, `asks_for_help`, and `verbose_count` are only used +/// by the command line argument backend, other backends leave them empty/false/0. class VerifiedConfigAttributes { public: std::unordered_map config_values; @@ -43,12 +43,16 @@ class VerifiedCommandLineArguments : public VerifiedConfigAttributes { public: std::vector positional_arguments; bool asks_for_help; + /// Number of times -v / --verbose was passed on the command line. + /// 1 → debug level, 2+ → trace level. + uint32_t verbose_count; static VerifiedCommandLineArguments askingForHelp(); static VerifiedCommandLineArguments fromConfigValuesAndPositionalArguments( std::unordered_map config_values, - std::vector positional_arguments + std::vector positional_arguments, + uint32_t verbose_count ); };