From 3272955bf193bfc34f78f929b5ff58b636eda094 Mon Sep 17 00:00:00 2001 From: "Yukihiro \"Matz\" Matsumoto" Date: Sat, 7 Mar 2026 22:48:43 +0900 Subject: [PATCH] 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 --- mrbgems/mruby-env/mrbgem.rake | 6 + mrbgems/mruby-env/mrblib/env.rb | 153 ++++++++++++++++++++ mrbgems/mruby-env/src/env.c | 223 +++++++++++++++++++++++++++++ mrbgems/mruby-env/test/env.rb | 243 ++++++++++++++++++++++++++++++++ mrbgems/stdlib-io.gembox | 3 + 5 files changed, 628 insertions(+) create mode 100644 mrbgems/mruby-env/mrbgem.rake create mode 100644 mrbgems/mruby-env/mrblib/env.rb create mode 100644 mrbgems/mruby-env/src/env.c create mode 100644 mrbgems/mruby-env/test/env.rb diff --git a/mrbgems/mruby-env/mrbgem.rake b/mrbgems/mruby-env/mrbgem.rake new file mode 100644 index 000000000..8ea123e3b --- /dev/null +++ b/mrbgems/mruby-env/mrbgem.rake @@ -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 diff --git a/mrbgems/mruby-env/mrblib/env.rb b/mrbgems/mruby-env/mrblib/env.rb new file mode 100644 index 000000000..e8183dd8a --- /dev/null +++ b/mrbgems/mruby-env/mrblib/env.rb @@ -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 diff --git a/mrbgems/mruby-env/src/env.c b/mrbgems/mruby-env/src/env.c new file mode 100644 index 000000000..7a621b466 --- /dev/null +++ b/mrbgems/mruby-env/src/env.c @@ -0,0 +1,223 @@ +/* +** env.c - ENV object for environment variable access +*/ + +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#define environ _environ +#else +#ifdef __APPLE__ +#include +#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) +{ +} diff --git a/mrbgems/mruby-env/test/env.rb b/mrbgems/mruby-env/test/env.rb new file mode 100644 index 000000000..3acdddad6 --- /dev/null +++ b/mrbgems/mruby-env/test/env.rb @@ -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 diff --git a/mrbgems/stdlib-io.gembox b/mrbgems/stdlib-io.gembox index 5993d5d48..3e13d183d 100644 --- a/mrbgems/stdlib-io.gembox +++ b/mrbgems/stdlib-io.gembox @@ -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