mruby-env: add ENV object for environment variable access

ENV is a plain Object with singleton methods and Enumerable,
matching CRuby's behavior. C methods wrap getenv/setenv/unsetenv
with platform support for POSIX, macOS, and Windows.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-03-07 22:48:43 +09:00
parent 3d53864991
commit 3272955bf1
5 changed files with 628 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
MRuby::Gem::Specification.new('mruby-env') do |spec|
spec.license = 'MIT'
spec.authors = ['mruby developers']
spec.summary = 'ENV object for environment variable access'
spec.add_dependency('mruby-enumerator', :core => 'mruby-enumerator')
end
+153
View File
@@ -0,0 +1,153 @@
class << ENV
def each(&block)
return to_enum(:each) unless block
keys.each do |k|
v = self[k]
block.call([k, v]) if v
end
self
end
alias each_pair each
def each_key(&block)
return to_enum(:each_key) unless block
keys.each(&block)
self
end
def each_value(&block)
return to_enum(:each_value) unless block
__values.each(&block)
self
end
def values
__values
end
def delete(key, &block)
val = __delete(key)
if val.nil? && block
block.call(key)
else
val
end
end
def clear
__clear
self
end
def fetch(key, *args, &block)
val = self[key]
return val unless val.nil?
if block
block.call(key)
elsif args.length > 0
args[0]
else
raise KeyError, "key not found: \"#{key}\""
end
end
def store(key, val)
self[key] = val
end
def to_h(&block)
h = {}
each do |k, v|
if block
k, v = block.call(k, v)
end
h[k] = v
end
h
end
def to_a
each_pair.to_a
end
def inspect
to_h.inspect
end
def empty?
size == 0
end
def has_value?(val)
__values.include?(val)
end
alias value? has_value?
def assoc(key)
val = self[key]
val.nil? ? nil : [key, val]
end
def rassoc(val)
each do |k, v|
return [k, v] if v == val
end
nil
end
def key(val)
each do |k, v|
return k if v == val
end
nil
end
def replace(hash)
clear
hash.each do |k, v|
self[k] = v
end
self
end
def update(hash, &block)
hash.each do |k, v|
if block && key?(k)
self[k] = block.call(k, self[k], v)
else
self[k] = v
end
end
self
end
alias merge! update
def select(&block)
return to_enum(:select) unless block
to_h.select(&block)
end
alias filter select
def reject(&block)
return to_enum(:reject) unless block
to_h.reject(&block)
end
def slice(*keys)
h = {}
keys.each do |k|
v = self[k]
h[k] = v unless v.nil?
end
h
end
alias include? key?
alias has_key? key?
alias member? key?
alias length size
def freeze
raise TypeError, "cannot freeze ENV"
end
end
+223
View File
@@ -0,0 +1,223 @@
/*
** env.c - ENV object for environment variable access
*/
#include <mruby.h>
#include <mruby/string.h>
#include <mruby/array.h>
#include <mruby/class.h>
#include <mruby/error.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <stdlib.h>
#define environ _environ
#else
#ifdef __APPLE__
#include <crt_externs.h>
#define environ (*_NSGetEnviron())
#else
extern char **environ;
#endif
#endif
static void
env_check_key(mrb_state *mrb, mrb_value key)
{
mrb_ensure_string_type(mrb, key);
if (memchr(RSTRING_PTR(key), '=', RSTRING_LEN(key))) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "bad environment variable name: contains '='");
}
if (memchr(RSTRING_PTR(key), '\0', RSTRING_LEN(key))) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "bad environment variable name: contains null byte");
}
}
/* ENV[] */
static mrb_value
mrb_env_aref(mrb_state *mrb, mrb_value self)
{
mrb_value key;
const char *val;
mrb_get_args(mrb, "o", &key);
env_check_key(mrb, key);
val = getenv(RSTRING_PTR(key));
if (val == NULL) return mrb_nil_value();
return mrb_str_new_cstr(mrb, val);
}
/* ENV[]= */
static mrb_value
mrb_env_aset(mrb_state *mrb, mrb_value self)
{
mrb_value key, val;
mrb_get_args(mrb, "oo", &key, &val);
env_check_key(mrb, key);
if (mrb_nil_p(val)) {
#ifdef _WIN32
_putenv_s(RSTRING_PTR(key), "");
#else
unsetenv(RSTRING_PTR(key));
#endif
return mrb_nil_value();
}
mrb_ensure_string_type(mrb, val);
if (memchr(RSTRING_PTR(val), '\0', RSTRING_LEN(val))) {
mrb_raise(mrb, E_ARGUMENT_ERROR, "bad environment variable value: contains null byte");
}
#ifdef _WIN32
_putenv_s(RSTRING_PTR(key), RSTRING_PTR(val));
#else
setenv(RSTRING_PTR(key), RSTRING_PTR(val), 1);
#endif
return val;
}
/* ENV.__delete(key) - returns old value or nil */
static mrb_value
mrb_env_delete(mrb_state *mrb, mrb_value self)
{
mrb_value key;
const char *val;
mrb_value result;
mrb_get_args(mrb, "o", &key);
env_check_key(mrb, key);
val = getenv(RSTRING_PTR(key));
if (val == NULL) return mrb_nil_value();
result = mrb_str_new_cstr(mrb, val);
#ifdef _WIN32
_putenv_s(RSTRING_PTR(key), "");
#else
unsetenv(RSTRING_PTR(key));
#endif
return result;
}
/* ENV.keys */
static mrb_value
mrb_env_keys(mrb_state *mrb, mrb_value self)
{
mrb_value ary = mrb_ary_new(mrb);
char **env;
int ai = mrb_gc_arena_save(mrb);
for (env = environ; *env != NULL; env++) {
char *eq = strchr(*env, '=');
if (eq) {
mrb_ary_push(mrb, ary, mrb_str_new(mrb, *env, (mrb_int)(eq - *env)));
mrb_gc_arena_restore(mrb, ai);
}
}
return ary;
}
/* ENV.__values */
static mrb_value
mrb_env_values(mrb_state *mrb, mrb_value self)
{
mrb_value ary = mrb_ary_new(mrb);
char **env;
int ai = mrb_gc_arena_save(mrb);
for (env = environ; *env != NULL; env++) {
char *eq = strchr(*env, '=');
if (eq) {
mrb_ary_push(mrb, ary, mrb_str_new_cstr(mrb, eq + 1));
mrb_gc_arena_restore(mrb, ai);
}
}
return ary;
}
/* ENV.size */
static mrb_value
mrb_env_size(mrb_state *mrb, mrb_value self)
{
mrb_int count = 0;
char **env;
for (env = environ; *env != NULL; env++) {
count++;
}
return mrb_fixnum_value(count);
}
/* ENV.key? */
static mrb_value
mrb_env_has_key(mrb_state *mrb, mrb_value self)
{
mrb_value key;
mrb_get_args(mrb, "o", &key);
env_check_key(mrb, key);
return mrb_bool_value(getenv(RSTRING_PTR(key)) != NULL);
}
/* ENV.to_s */
static mrb_value
mrb_env_to_s(mrb_state *mrb, mrb_value self)
{
return mrb_str_new_lit(mrb, "ENV");
}
/* ENV.__clear */
static mrb_value
mrb_env_clear(mrb_state *mrb, mrb_value self)
{
while (*environ) {
char *eq = strchr(*environ, '=');
if (eq) {
mrb_int len = (mrb_int)(eq - *environ);
char *key = (char*)mrb_malloc(mrb, len + 1);
memcpy(key, *environ, len);
key[len] = '\0';
#ifdef _WIN32
_putenv_s(key, "");
#else
unsetenv(key);
#endif
mrb_free(mrb, key);
}
else {
break;
}
}
return self;
}
void
mrb_mruby_env_gem_init(mrb_state *mrb)
{
mrb_value env = mrb_obj_new(mrb, mrb->object_class, 0, NULL);
struct RObject *eobj = mrb_obj_ptr(env);
mrb_define_global_const(mrb, "ENV", env);
mrb_define_singleton_method_id(mrb, eobj, MRB_OPSYM(aref), mrb_env_aref, MRB_ARGS_REQ(1));
mrb_define_singleton_method_id(mrb, eobj, MRB_OPSYM(aset), mrb_env_aset, MRB_ARGS_REQ(2));
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(__delete), mrb_env_delete, MRB_ARGS_REQ(1));
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(keys), mrb_env_keys, MRB_ARGS_NONE());
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(__values), mrb_env_values, MRB_ARGS_NONE());
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(size), mrb_env_size, MRB_ARGS_NONE());
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM_Q(key), mrb_env_has_key, MRB_ARGS_REQ(1));
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(to_s), mrb_env_to_s, MRB_ARGS_NONE());
mrb_define_singleton_method_id(mrb, eobj, MRB_SYM(__clear), mrb_env_clear, MRB_ARGS_NONE());
/* include Enumerable in ENV's singleton class */
{
struct RClass *sc = mrb_singleton_class_ptr(mrb, env);
mrb_include_module(mrb, sc, mrb_module_get_id(mrb, MRB_SYM(Enumerable)));
}
}
void
mrb_mruby_env_gem_final(mrb_state *mrb)
{
}
+243
View File
@@ -0,0 +1,243 @@
##
# ENV test
assert('ENV class') do
assert_equal Object, ENV.class
end
assert('ENV.to_s') do
assert_equal "ENV", ENV.to_s
end
assert('ENV[] and ENV[]=') do
ENV["MRUBY_ENV_TEST_A"] = "hello"
assert_equal "hello", ENV["MRUBY_ENV_TEST_A"]
ENV["MRUBY_ENV_TEST_A"] = nil
assert_nil ENV["MRUBY_ENV_TEST_A"]
end
assert('ENV[] returns nil for missing key') do
assert_nil ENV["MRUBY_ENV_TEST_NONEXISTENT"]
end
assert('ENV.store') do
ENV.store("MRUBY_ENV_TEST_B", "world")
assert_equal "world", ENV["MRUBY_ENV_TEST_B"]
ENV.delete("MRUBY_ENV_TEST_B")
end
assert('ENV.delete') do
ENV["MRUBY_ENV_TEST_C"] = "val"
assert_equal "val", ENV.delete("MRUBY_ENV_TEST_C")
assert_nil ENV["MRUBY_ENV_TEST_C"]
end
assert('ENV.delete returns nil for missing key') do
assert_nil ENV.delete("MRUBY_ENV_TEST_NONEXISTENT")
end
assert('ENV.delete with block') do
result = ENV.delete("MRUBY_ENV_TEST_NONEXISTENT") { |k| "missing: #{k}" }
assert_equal "missing: MRUBY_ENV_TEST_NONEXISTENT", result
end
assert('ENV.key?') do
ENV["MRUBY_ENV_TEST_D"] = "yes"
assert_true ENV.key?("MRUBY_ENV_TEST_D")
assert_false ENV.key?("MRUBY_ENV_TEST_NONEXISTENT")
ENV.delete("MRUBY_ENV_TEST_D")
end
assert('ENV.include?/has_key?/member?') do
ENV["MRUBY_ENV_TEST_E"] = "val"
assert_true ENV.include?("MRUBY_ENV_TEST_E")
assert_true ENV.has_key?("MRUBY_ENV_TEST_E")
assert_true ENV.member?("MRUBY_ENV_TEST_E")
ENV.delete("MRUBY_ENV_TEST_E")
end
assert('ENV.keys') do
ENV["MRUBY_ENV_TEST_F"] = "val"
k = ENV.keys
assert_kind_of Array, k
assert_true k.include?("MRUBY_ENV_TEST_F")
ENV.delete("MRUBY_ENV_TEST_F")
end
assert('ENV.values') do
ENV["MRUBY_ENV_TEST_G"] = "unique_val_42"
v = ENV.values
assert_kind_of Array, v
assert_true v.include?("unique_val_42")
ENV.delete("MRUBY_ENV_TEST_G")
end
assert('ENV.size/length') do
s1 = ENV.size
ENV["MRUBY_ENV_TEST_H"] = "val"
assert_equal s1 + 1, ENV.size
assert_equal ENV.size, ENV.length
ENV.delete("MRUBY_ENV_TEST_H")
end
assert('ENV.empty?') do
assert_equal [true, false].include?(ENV.empty?), true
end
assert('ENV.has_value?/value?') do
ENV["MRUBY_ENV_TEST_I"] = "unique_find_me"
assert_true ENV.has_value?("unique_find_me")
assert_true ENV.value?("unique_find_me")
assert_false ENV.has_value?("MRUBY_ENV_NEVER_EXISTS_VALUE")
ENV.delete("MRUBY_ENV_TEST_I")
end
assert('ENV.each/each_pair') do
ENV["MRUBY_ENV_TEST_J"] = "iter_val"
found = false
ENV.each do |k, v|
if k == "MRUBY_ENV_TEST_J" && v == "iter_val"
found = true
end
end
assert_true found
ENV.delete("MRUBY_ENV_TEST_J")
end
assert('ENV.each_key') do
ENV["MRUBY_ENV_TEST_K"] = "val"
found = false
ENV.each_key { |k| found = true if k == "MRUBY_ENV_TEST_K" }
assert_true found
ENV.delete("MRUBY_ENV_TEST_K")
end
assert('ENV.each_value') do
ENV["MRUBY_ENV_TEST_L"] = "val_each_v"
found = false
ENV.each_value { |v| found = true if v == "val_each_v" }
assert_true found
ENV.delete("MRUBY_ENV_TEST_L")
end
assert('ENV.fetch') do
ENV["MRUBY_ENV_TEST_M"] = "fetch_val"
assert_equal "fetch_val", ENV.fetch("MRUBY_ENV_TEST_M")
assert_equal "default", ENV.fetch("MRUBY_ENV_TEST_NONE", "default")
assert_equal "block", ENV.fetch("MRUBY_ENV_TEST_NONE") { "block" }
assert_raise(KeyError) { ENV.fetch("MRUBY_ENV_TEST_NONE") }
ENV.delete("MRUBY_ENV_TEST_M")
end
assert('ENV.to_h') do
ENV["MRUBY_ENV_TEST_N"] = "to_h_val"
h = ENV.to_h
assert_kind_of Hash, h
assert_equal "to_h_val", h["MRUBY_ENV_TEST_N"]
ENV.delete("MRUBY_ENV_TEST_N")
end
assert('ENV.to_a') do
ENV["MRUBY_ENV_TEST_O"] = "to_a_val"
a = ENV.to_a
assert_kind_of Array, a
assert_true a.include?(["MRUBY_ENV_TEST_O", "to_a_val"])
ENV.delete("MRUBY_ENV_TEST_O")
end
assert('ENV.assoc') do
ENV["MRUBY_ENV_TEST_P"] = "assoc_val"
assert_equal ["MRUBY_ENV_TEST_P", "assoc_val"], ENV.assoc("MRUBY_ENV_TEST_P")
assert_nil ENV.assoc("MRUBY_ENV_TEST_NONEXISTENT")
ENV.delete("MRUBY_ENV_TEST_P")
end
assert('ENV.rassoc') do
ENV["MRUBY_ENV_TEST_Q"] = "rassoc_unique_val"
result = ENV.rassoc("rassoc_unique_val")
assert_equal "MRUBY_ENV_TEST_Q", result[0]
assert_equal "rassoc_unique_val", result[1]
assert_nil ENV.rassoc("MRUBY_ENV_NEVER_EXISTS_VALUE")
ENV.delete("MRUBY_ENV_TEST_Q")
end
assert('ENV.key') do
ENV["MRUBY_ENV_TEST_R"] = "key_search_val"
assert_equal "MRUBY_ENV_TEST_R", ENV.key("key_search_val")
assert_nil ENV.key("MRUBY_ENV_NEVER_EXISTS_VALUE")
ENV.delete("MRUBY_ENV_TEST_R")
end
assert('ENV.inspect') do
s = ENV.inspect
assert_kind_of String, s
end
assert('ENV.select/filter') do
ENV["MRUBY_ENV_TEST_S"] = "sel_val"
result = ENV.select { |k, v| k == "MRUBY_ENV_TEST_S" }
assert_kind_of Hash, result
assert_equal "sel_val", result["MRUBY_ENV_TEST_S"]
ENV.delete("MRUBY_ENV_TEST_S")
end
assert('ENV.reject') do
ENV["MRUBY_ENV_TEST_T"] = "rej_val"
result = ENV.reject { |k, v| k != "MRUBY_ENV_TEST_T" }
assert_equal({"MRUBY_ENV_TEST_T" => "rej_val"}, result)
ENV.delete("MRUBY_ENV_TEST_T")
end
assert('ENV.replace') do
ENV["MRUBY_ENV_TEST_U1"] = "old1"
ENV.replace({"MRUBY_ENV_TEST_U2" => "new2"})
assert_nil ENV["MRUBY_ENV_TEST_U1"]
assert_equal "new2", ENV["MRUBY_ENV_TEST_U2"]
ENV.delete("MRUBY_ENV_TEST_U2")
end
assert('ENV.update/merge!') do
ENV["MRUBY_ENV_TEST_V"] = "orig"
ENV.update({"MRUBY_ENV_TEST_V" => "updated", "MRUBY_ENV_TEST_W" => "new"})
assert_equal "updated", ENV["MRUBY_ENV_TEST_V"]
assert_equal "new", ENV["MRUBY_ENV_TEST_W"]
ENV.delete("MRUBY_ENV_TEST_V")
ENV.delete("MRUBY_ENV_TEST_W")
end
assert('ENV.update with block') do
ENV["MRUBY_ENV_TEST_X"] = "old"
ENV.update({"MRUBY_ENV_TEST_X" => "new"}) { |k, o, n| "#{o}_#{n}" }
assert_equal "old_new", ENV["MRUBY_ENV_TEST_X"]
ENV.delete("MRUBY_ENV_TEST_X")
end
assert('ENV.slice') do
ENV["MRUBY_ENV_TEST_Y"] = "s1"
ENV["MRUBY_ENV_TEST_Z"] = "s2"
h = ENV.slice("MRUBY_ENV_TEST_Y", "MRUBY_ENV_TEST_Z", "MRUBY_ENV_TEST_NONEXISTENT")
assert_equal({"MRUBY_ENV_TEST_Y" => "s1", "MRUBY_ENV_TEST_Z" => "s2"}, h)
ENV.delete("MRUBY_ENV_TEST_Y")
ENV.delete("MRUBY_ENV_TEST_Z")
end
assert('ENV.freeze raises TypeError') do
assert_raise(TypeError) { ENV.freeze }
end
assert('ENV raises TypeError for non-string key') do
assert_raise(TypeError) { ENV[123] }
end
assert('ENV raises TypeError for non-string value') do
assert_raise(TypeError) { ENV["MRUBY_ENV_TEST_ERR"] = 123 }
end
assert('ENV.each returns enumerator without block') do
e = ENV.each
assert_kind_of Enumerator, e
end
assert('ENV is Enumerable') do
assert_true ENV.is_a?(Enumerable)
end
+3
View File
@@ -12,4 +12,7 @@ MRuby::GemBox.new do |conf|
# Use Dir class
conf.gem :core => "mruby-dir"
# Use ENV object
conf.gem :core => "mruby-env"
end