Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/config/config_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ std::variant<C, int32_t> 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;
Expand Down
6 changes: 5 additions & 1 deletion src/config/config_specification.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
62 changes: 47 additions & 15 deletions src/config/source/command_line_arguments.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ namespace silo::config {
std::string CommandLineArguments::configKeyPathToString(const ConfigKeyPath& key_path) {
std::vector<std::string> 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, "-");
}
Expand Down Expand Up @@ -89,20 +87,52 @@ std::tuple<ConfigValue, std::span<const std::string>> parseValueFromArg(
return {attribute_spec.parseValueFromString(value_string), remaining_args};
}

std::vector<std::string> expandArguments(const std::vector<std::string>& args) {
std::vector<std::string> 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<std::string> expanded_args = expandArguments(args);

// Second pass: process the fully-expanded argument list.
// E.g. "--api-foo" => "1234" or "--api-foo=1234"
std::unordered_map<ConfigKeyPath, ConfigValue> config_value_by_option;
std::vector<std::string> positional_args;
std::vector<std::string> invalid_config_keys;
std::span<const std::string> remaining_args{args.data(), args.size()};
std::vector<std::string> positional_args;

uint32_t verbose_count = 0;

std::span<const std::string> 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);
Expand All @@ -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 =
Expand All @@ -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);
Expand All @@ -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
);
}

Expand Down
31 changes: 31 additions & 0 deletions src/config/source/command_line_arguments.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,34 @@ TEST(CommandLineArguments, testPositionalArgumentsStartingWithMinus) {
ASSERT_EQ(verified.positional_arguments.at(0), "-positional_argument_with_minus");
}
}

TEST(CommandLineArguments, verboseFlagShortForm) {
std::vector<std::string> 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<std::string> arguments{"--verbose"};
const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({});
ASSERT_EQ(verified.verbose_count, 1U);
}

TEST(CommandLineArguments, verboseFlagRepeated) {
std::vector<std::string> arguments{"-v", "--verbose", "-v"};
const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({});
ASSERT_EQ(verified.verbose_count, 3U);
}

TEST(CommandLineArguments, verboseFlagCluster) {
std::vector<std::string> arguments{"-vvv"};
const auto verified = CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({});
ASSERT_EQ(verified.verbose_count, 3U);
}

TEST(CommandLineArguments, verboseFlagDoesNotCountAsUnknownOption) {
std::vector<std::string> arguments{"-v"};
// Even with an empty specification (no known options), -v should not throw.
ASSERT_NO_THROW(((void)CommandLineArguments{{arguments.begin(), arguments.end()}}.verify({})));
}
5 changes: 4 additions & 1 deletion src/config/verified_config_attributes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,20 @@ std::optional<std::vector<std::string>> VerifiedConfigAttributes::getList(
VerifiedCommandLineArguments VerifiedCommandLineArguments::askingForHelp() {
VerifiedCommandLineArguments result;
result.asks_for_help = true;
result.verbose_count = 0;
return result;
}

VerifiedCommandLineArguments VerifiedCommandLineArguments::fromConfigValuesAndPositionalArguments(
std::unordered_map<ConfigKeyPath, ConfigValue> config_values,
std::vector<std::string> positional_arguments
std::vector<std::string> 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;
}

Expand Down
10 changes: 7 additions & 3 deletions src/config/verified_config_attributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConfigKeyPath, ConfigValue> config_values;
Expand All @@ -43,12 +43,16 @@ class VerifiedCommandLineArguments : public VerifiedConfigAttributes {
public:
std::vector<std::string> 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<ConfigKeyPath, ConfigValue> config_values,
std::vector<std::string> positional_arguments
std::vector<std::string> positional_arguments,
uint32_t verbose_count
);
};

Expand Down
Loading