Skip to content

Added support for environment variables#694

Open
liuzicheng1987 wants to merge 15 commits into
mainfrom
f/env
Open

Added support for environment variables#694
liuzicheng1987 wants to merge 15 commits into
mainfrom
f/env

Conversation

@liuzicheng1987

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for reading from and writing to environment variables using rfl::env::read and rfl::env::write. The changes include the implementation of the Reader and Writer classes for environment variables, a new ToAllCaps case-transformation processor, comprehensive documentation, and a suite of unit tests. The review feedback highlights several critical issues that need to be addressed: portability concerns on Windows and macOS due to the direct use of POSIX-specific functions like setenv, unsetenv, and environ; missing locale headers for floating-point parsing; a bug in the substring length calculation when parsing environment variables; and a design flaw where empty environment variables are incorrectly treated as missing, which prevents empty strings from being parsed correctly.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if (pos_eq == std::string::npos) {
continue;
}
const auto name = env_entry.substr(_obj.prefix.size(), pos_eq);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a critical bug in the substring length calculation. The second argument of std::string::substr is the length of the substring, not the end index. Passing pos_eq (which is the index of '=' in the entire env_entry string) will result in a substring that goes past the key and includes the '=' character and part of the value.

It should be pos_eq - _obj.prefix.size() to correctly extract only the key name.

          const auto name = env_entry.substr(_obj.prefix.size(), pos_eq - _obj.prefix.size());


#include "../Result.hpp"

extern char** environ;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using extern char** environ; directly is not portable. On Windows (MSVC), the environment is accessed via _environ (declared in <stdlib.h>), and on macOS, direct access to environ in shared libraries can lead to linker errors, where *_NSGetEnviron() from <crt_externs.h> should be used instead.

We can define a portable helper function to retrieve the environment pointer across Windows, macOS, and other POSIX systems.

#if defined(__APPLE__)
#include <crt_externs.h>
inline char** get_environ() noexcept { return *_NSGetEnviron(); }
#elif defined(_WIN32)
#include <stdlib.h>
inline char** get_environ() noexcept { return _environ; }
#else
extern char** environ;
inline char** get_environ() noexcept { return environ; }
#endif

return true;
}
const auto prefix = _var.path.empty() ? std::string("") : _var.path + "_";
for (char** env = environ; *env != nullptr; ++env) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the portable get_environ() helper instead of direct access to environ to ensure cross-platform compatibility.

    for (char** env = get_environ(); *env != nullptr; ++env) {

}

} else {
for (char** env = environ; *env != nullptr; ++env) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the portable get_environ() helper instead of direct access to environ to ensure cross-platform compatibility.

      for (char** env = get_environ(); *env != nullptr; ++env) {

#include <optional>
#include <string>
#include <string_view>
#include <type_traits>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The floating-point parsing function parse_value uses LC_NUMERIC, _create_locale, newlocale, etc., which require <clocale> and <locale.h>. These headers are currently not included, which will cause compilation failures on many platforms.

#include <clocale>
#include <locale.h>
#include <type_traits>

Comment thread src/rfl/env/Writer.cpp
value.visit([&](const auto& _v) {
using T = std::remove_cvref_t<decltype(_v)>;
if constexpr (std::is_same_v<T, OutputVarType>) {
setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the portable portable_setenv helper instead of POSIX-only setenv to ensure Windows compatibility.

Suggested change
setenv((obj.prefix + key).c_str(), _v.value.c_str(), 1);
portable_setenv((obj.prefix + key).c_str(), _v.value.c_str());

Comment on lines +15 to +17

namespace test_env {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

setenv and unsetenv are POSIX-only and will fail to compile on Windows. To make the tests portable, we should define portable helpers using _putenv_s on Windows.

namespace test_env {

inline void portable_setenv(const std::string& name, const std::string& value) {
#ifdef _WIN32
  _putenv_s(name.c_str(), value.c_str());
#else
  setenv(name.c_str(), value.c_str(), 1);
#endif
}

inline void portable_unsetenv(const std::string& name) {
#ifdef _WIN32
  _putenv_s(name.c_str(), "");
#else
  unsetenv(name.c_str());
#endif
}

class ScopedEnvironment {

Comment on lines +47 to +55
for (const auto& [name, value] : current) {
if (!_snapshot.contains(name)) {
unsetenv(name.c_str());
}
}

for (const auto& [name, value] : _snapshot) {
setenv(name.c_str(), value.c_str(), 1);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the portable portable_setenv and portable_unsetenv helpers instead of POSIX-only setenv and unsetenv to ensure Windows compatibility.

Suggested change
for (const auto& [name, value] : current) {
if (!_snapshot.contains(name)) {
unsetenv(name.c_str());
}
}
for (const auto& [name, value] : _snapshot) {
setenv(name.c_str(), value.c_str(), 1);
}
for (const auto& [name, value] : current) {
if (!_snapshot.contains(name)) {
portable_unsetenv(name);
}
}
for (const auto& [name, value] : _snapshot) {
portable_setenv(name, value);
}

Comment on lines +61 to +63
for (const auto& [name, value] : current) {
unsetenv(name.c_str());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Use the portable portable_unsetenv helper instead of POSIX-only unsetenv to ensure Windows compatibility.

Suggested change
for (const auto& [name, value] : current) {
unsetenv(name.c_str());
}
for (const auto& [name, value] : current) {
portable_unsetenv(name);
}

Comment on lines +160 to +164
bool is_empty(const InputVarType& _var) const noexcept {
const auto str = std::getenv(_var.path.c_str());
if (str != nullptr) {
return std::string(str).empty();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Treating an empty environment variable (str != nullptr && std::string(str).empty()) as "empty/missing" has two major drawbacks:

  1. It prevents users from explicitly setting a required string field to an empty string (""), as it will be treated as missing and fail parsing or fall back to a default value.
  2. It prevents arrays from containing empty strings, as read_array will prematurely terminate when it encounters an empty string element (e.g., TAGS_1="").

It is highly recommended to only treat a variable as missing if it is actually unset (i.e., std::getenv returns nullptr).

  bool is_empty(const InputVarType& _var) const noexcept {
    const auto str = std::getenv(_var.path.c_str());
    if (str != nullptr) {
      return false;
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant