Added support for environment variables#694
Conversation
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) { |
| } | ||
|
|
||
| } else { | ||
| for (char** env = environ; *env != nullptr; ++env) { |
| #include <optional> | ||
| #include <string> | ||
| #include <string_view> | ||
| #include <type_traits> |
There was a problem hiding this comment.
| 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); |
|
|
||
| namespace test_env { | ||
|
|
There was a problem hiding this comment.
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 {| 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); | ||
| } |
There was a problem hiding this comment.
Use the portable portable_setenv and portable_unsetenv helpers instead of POSIX-only setenv and unsetenv to ensure Windows compatibility.
| 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); | |
| } |
| for (const auto& [name, value] : current) { | ||
| unsetenv(name.c_str()); | ||
| } |
There was a problem hiding this comment.
| 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(); | ||
| } |
There was a problem hiding this comment.
Treating an empty environment variable (str != nullptr && std::string(str).empty()) as "empty/missing" has two major drawbacks:
- 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. - It prevents arrays from containing empty strings, as
read_arraywill 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;
}
No description provided.