Skip to content
Merged
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
7 changes: 6 additions & 1 deletion include/cli/argument.hh
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,9 @@ struct ArgParameter {
Presence presence{Presence::optional}; ///< Whether the argument is required.
T default_value{}; ///< Default value when the argument is absent.
std::function<void(const T&)>
on_parse{}; ///< Callback invoked once after parsing completes.
on_parse{}; ///< Callback invoked once after parsing completes.
std::string_view env{}; ///< Environment variable name to fall back to when
///< the option is absent from the command line.
};

namespace detail {
Expand All @@ -400,6 +402,7 @@ struct ArgImpl : public OptionTag {
ArgImpl(ArgParameter<value_type> param)
: help(param.help),
presence(param.presence),
env(param.env),
value_(param.default_value),
on_parse_(std::move(param.on_parse)) {}

Expand Down Expand Up @@ -430,6 +433,8 @@ struct ArgImpl : public OptionTag {
// NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes)
cli::Presence presence{Presence::optional};
// NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes)
std::string_view env{};
// NOLINTNEXTLINE(cppcoreguidelines-non-private-member-variables-in-classes)
value_type default_value{};

static constexpr auto name = Name;
Expand Down
33 changes: 33 additions & 0 deletions include/cli/parser.hh
Original file line number Diff line number Diff line change
Expand Up @@ -717,6 +717,11 @@ struct Parser {
}
});

if (auto err = apply_env_fallback(result.value); err.has_error()) {
result.error = std::move(err.error);
return;
}

if (auto err = validate_required(result.value); err.has_error()) {
result.error = std::move(err.error);
}
Expand Down Expand Up @@ -923,6 +928,34 @@ struct Parser {
return res;
}

auto apply_env_fallback(T& val) -> ActionResult<void> {
ActionResult<void> res = ActionResult<void>::ok();
for_each_field(val, [&](auto& f) -> auto {
using F = std::remove_cvref_t<decltype(f)>;
if constexpr (std::derived_from<F, OptionTag>) {
if (res.has_error() || f.provided() || f.env.empty()) return;
#ifdef _WIN32
char* raw_buf = nullptr;
std::size_t raw_len = 0;
_dupenv_s(&raw_buf, &raw_len, std::string(f.env).c_str());
std::unique_ptr<char, decltype(&free)> raw_guard(raw_buf, &free);
const char* raw = raw_buf;
#else
const char* raw = std::getenv(std::string(f.env).c_str());
#endif
if (!raw) return;
f.notify_option_seen();
if constexpr (F::nargs.min == 0 && F::nargs.max == 0) {
res = f.invoke_flag(0);
} else {
res = f.invoke_action(std::string_view(raw), 0);
}
if (!res.has_error()) f.fire_on_parse();
}
});
return res;
}

auto finalize_special_error(ParseError error) -> ParseError {
if (error.code == ErrorCode::help_requested && error.detail.empty()) {
error.detail = format_help(ColorMode::never);
Expand Down
101 changes: 101 additions & 0 deletions tests/test_parse.cc
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
#include <gtest/gtest.h>

#include <cstdlib>
#include <string_view>
#include <vector>

#ifdef _WIN32
#include <stdlib.h>
static auto setenv(const char* name, const char* value, int) -> int {
return _putenv_s(name, value);
}
static auto unsetenv(const char* name) -> void { _putenv_s(name, ""); }
#endif

#include "cli/argument.hh"
#include "cli/parser.hh"

Expand Down Expand Up @@ -308,3 +317,95 @@ TEST(ParseClusterNargs2, FlagsThenValueNextTokens) {
(std::vector<std::string>{"file1", "file2"}));
EXPECT_EQ(result.value.files.value(), (std::vector<std::string>{"file3"}));
}

// ---- env fallback ----------------------------------------------------------

namespace {

struct EnvArgs {
cli::StringOption<"output", 'o'> output{{.env = "TEST_OUTPUT"}};
cli::IntOption<"count", 'c'> count{{.env = "TEST_COUNT"}};
cli::Flag<"verbose", 'v'> verbose{{.env = "TEST_VERBOSE"}};
};

} // namespace

TEST(ParseEnv, FallsBackToEnvWhenOptionAbsent) {
setenv("TEST_OUTPUT", "from_env", 1);
auto args = argv({"prog"});
auto result =
cli::Parser<EnvArgs>{}.parse(std::span<const std::string_view>(args), 1);
unsetenv("TEST_OUTPUT");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value.output.value().has_value());
EXPECT_EQ(*result.value.output.value(), "from_env");
}

TEST(ParseEnv, CliTakesPrecedenceOverEnv) {
setenv("TEST_OUTPUT", "from_env", 1);
auto args = argv({"prog", "--output", "from_cli"});
auto result =
cli::Parser<EnvArgs>{}.parse(std::span<const std::string_view>(args), 1);
unsetenv("TEST_OUTPUT");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value.output.value().has_value());
EXPECT_EQ(*result.value.output.value(), "from_cli");
}

TEST(ParseEnv, EnvSatisfiesRequiredOption) {
struct RequiredEnvArgs {
cli::StringOption<"output"> output{
{.presence = cli::required, .env = "TEST_OUTPUT"}};
};
setenv("TEST_OUTPUT", "env_val", 1);
auto args = argv({"prog"});
auto result = cli::Parser<RequiredEnvArgs>{}.parse(
std::span<const std::string_view>(args), 1);
unsetenv("TEST_OUTPUT");
ASSERT_TRUE(result.has_value());
EXPECT_EQ(*result.value.output.value(), "env_val");
}

TEST(ParseEnv, MissingEnvStillFailsForRequired) {
struct RequiredEnvArgs {
cli::StringOption<"output"> output{
{.presence = cli::required, .env = "TEST_OUTPUT_ABSENT"}};
};
unsetenv("TEST_OUTPUT_ABSENT");
auto args = argv({"prog"});
auto result = cli::Parser<RequiredEnvArgs>{}.parse(
std::span<const std::string_view>(args), 1);
ASSERT_TRUE(result.has_error());
EXPECT_EQ(result.error.code, cli::ErrorCode::missing_required);
}

TEST(ParseEnv, IntOptionFromEnv) {
setenv("TEST_COUNT", "42", 1);
auto args = argv({"prog"});
auto result =
cli::Parser<EnvArgs>{}.parse(std::span<const std::string_view>(args), 1);
unsetenv("TEST_COUNT");
ASSERT_TRUE(result.has_value());
ASSERT_TRUE(result.value.count.value().has_value());
EXPECT_EQ(*result.value.count.value(), 42);
}

TEST(ParseEnv, FlagFromEnv) {
setenv("TEST_VERBOSE", "1", 1);
auto args = argv({"prog"});
auto result =
cli::Parser<EnvArgs>{}.parse(std::span<const std::string_view>(args), 1);
unsetenv("TEST_VERBOSE");
ASSERT_TRUE(result.has_value());
EXPECT_TRUE(result.value.verbose.value());
}

TEST(ParseEnv, InvalidEnvValueReportsError) {
setenv("TEST_COUNT", "not_a_number", 1);
auto args = argv({"prog"});
auto result =
cli::Parser<EnvArgs>{}.parse(std::span<const std::string_view>(args), 1);
unsetenv("TEST_COUNT");
ASSERT_TRUE(result.has_error());
EXPECT_EQ(result.error.code, cli::ErrorCode::invalid_value);
}
Loading