From 7d6c1202f320592cb1d81f2d2e943af008fd3ec6 Mon Sep 17 00:00:00 2001 From: Jonathan Barquero Date: Fri, 25 Jul 2025 10:58:38 -0600 Subject: [PATCH 1/2] feat: Supports key value pairs prefixing the commands with ENV vars --- bundler/lib/bundler/cli.rb | 5 ++++- bundler/lib/bundler/cli/exec.rb | 14 +++++++++++++- bundler/spec/commands/exec_spec.rb | 10 ++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index ea85f9af22ab..78e9c307eac7 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -432,13 +432,16 @@ def cache map aliases_for("cache") - desc "exec [OPTIONS]", "Run the command in context of the bundle" + desc "exec [OPTIONS] [KEY=VALUE...] COMMAND", "Run the command in context of the bundle" method_option :keep_file_descriptors, type: :boolean, default: true, banner: "Passes all file descriptors to the new processes. Default is true, and setting it to false is deprecated" method_option :gemfile, type: :string, required: false, banner: "Use the specified gemfile instead of Gemfile" long_desc <<-D Exec runs a command, providing it access to the gems in the bundle. While using bundle exec you can require and call the bundled gems as if they were installed into the system wide RubyGems repository. + + You can also set environment variables for the commands by prefixing with KEY=VALUE pairs. + e.g.: bundle exec RUBYOPT=-rlogger ruby script.rb D def exec(*args) if ARGV.include?("--no-keep-file-descriptors") diff --git a/bundler/lib/bundler/cli/exec.rb b/bundler/lib/bundler/cli/exec.rb index 9428e9db3bf8..a04436b13e03 100644 --- a/bundler/lib/bundler/cli/exec.rb +++ b/bundler/lib/bundler/cli/exec.rb @@ -10,6 +10,14 @@ class CLI::Exec def initialize(options, args) @options = options + @env = {} + + # Parse leading KEY=VALUE pairs as env vars + while args.first && args.first.include?("=") && args.first =~ /^[A-Za-z_][A-Za-z0-9_]*=/ + key, value = args.shift.split("=", 2) + @env[key] = value + end + @cmd = args.shift @args = args @args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby? @@ -39,7 +47,11 @@ def validate_cmd! end def kernel_exec(*args) - Kernel.exec(*args) + if @env.any? + Kernel.exec(@env, *args) + else + Kernel.exec(*args) + end rescue Errno::EACCES, Errno::ENOEXEC Bundler.ui.error "bundler: not executable: #{cmd}" exit 126 diff --git a/bundler/spec/commands/exec_spec.rb b/bundler/spec/commands/exec_spec.rb index cad1ac4ba3ce..cbef73363333 100644 --- a/bundler/spec/commands/exec_spec.rb +++ b/bundler/spec/commands/exec_spec.rb @@ -192,6 +192,16 @@ expect(out).to eq("1.0.0") end + it "sets environment variables only for the child process" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + ruby_script = "puts ENV['RUBYOPT'] || 'none'" + bundle "exec RUBYOPT=foo ruby -e \"#{ruby_script}\"" + expect(out).to include("foo") + end + context "with default gems" do # TODO: Switch to ERB::VERSION once Ruby 3.4 support is dropped, so all # supported rubies include an `erb` gem version where `ERB::VERSION` is From 2174711dcf283053b1706dd7d7ca0fc8cac83f9b Mon Sep 17 00:00:00 2001 From: Jonathan Barquero Date: Tue, 5 Aug 2025 09:24:31 -0600 Subject: [PATCH 2/2] feat: env option following Docker/Kubernetes patterns (--env) --- bundler/lib/bundler/cli.rb | 38 +++++++++++++-- bundler/lib/bundler/cli/exec.rb | 10 ++++ bundler/lib/bundler/man/bundle-exec.1.ronn | 6 ++- bundler/spec/commands/exec_spec.rb | 55 ++++++++++++++++++++++ 4 files changed, 105 insertions(+), 4 deletions(-) diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index 78e9c307eac7..721cc0cc6960 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -439,9 +439,11 @@ def cache Exec runs a command, providing it access to the gems in the bundle. While using bundle exec you can require and call the bundled gems as if they were installed into the system wide RubyGems repository. - - You can also set environment variables for the commands by prefixing with KEY=VALUE pairs. + + You can also set environment variables for the commands by prefixing with KEY=VALUE pairs or using the --env option. + e.g.: bundle exec RUBYOPT=-rlogger ruby script.rb + e.g.: bundle exec --env RUBYOPT=-rlogger ruby script.rb D def exec(*args) if ARGV.include?("--no-keep-file-descriptors") @@ -450,8 +452,38 @@ def exec(*args) SharedHelpers.major_deprecation(2, message, removed_message: removed_message) end + # Handle --env options separately + env_vars = [] + args = args.reject do |arg| + if arg == "--env" + # Next argument should be KEY=VALUE + next_arg = args[args.index(arg) + 1] + if next_arg && next_arg.include?("=") + env_vars << next_arg + true # Remove both --env and the next argument + else + false # Keep --env if no valid next argument + end + elsif arg.start_with?("--env=") + # Handle --env=KEY=VALUE format + env_var = arg[6..-1] # Remove "--env=" + if env_var.include?("=") + env_vars << env_var + true # Remove this argument + else + false # Keep if invalid format + end + else + false # Keep other arguments + end + end + + # Create new options hash with env_vars + new_options = options.dup + new_options[:env] = env_vars + require_relative "cli/exec" - Exec.new(options, args).run + Exec.new(new_options, args).run end map aliases_for("exec") diff --git a/bundler/lib/bundler/cli/exec.rb b/bundler/lib/bundler/cli/exec.rb index a04436b13e03..9cb0be9eaac1 100644 --- a/bundler/lib/bundler/cli/exec.rb +++ b/bundler/lib/bundler/cli/exec.rb @@ -12,6 +12,16 @@ def initialize(options, args) @options = options @env = {} + # Parse leading --env option separately + if options[:env] + options[:env].each do |env_var| + if env_var.include?("=") + key, value = env_var.split("=", 2) + @env[key] = value + end + end + end + # Parse leading KEY=VALUE pairs as env vars while args.first && args.first.include?("=") && args.first =~ /^[A-Za-z_][A-Za-z0-9_]*=/ key, value = args.shift.split("=", 2) diff --git a/bundler/lib/bundler/man/bundle-exec.1.ronn b/bundler/lib/bundler/man/bundle-exec.1.ronn index 3d3f0eed7b89..d1092b84a675 100644 --- a/bundler/lib/bundler/man/bundle-exec.1.ronn +++ b/bundler/lib/bundler/man/bundle-exec.1.ronn @@ -3,7 +3,7 @@ bundle-exec(1) -- Execute a command in the context of the bundle ## SYNOPSIS -`bundle exec` [--keep-file-descriptors] [--gemfile=GEMFILE] +`bundle exec` [--keep-file-descriptors] [--gemfile=GEMFILE] [--env KEY=VALUE] ## DESCRIPTION @@ -27,6 +27,10 @@ available on your shell's `$PATH`. * `--gemfile=GEMFILE`: Use the specified gemfile instead of [`Gemfile(5)`][Gemfile(5)]. +* `--env KEY=VALUE`: + Set environment variables for the child process. Can be specified multiple times. + Example: `bundle exec --env RUBYOPT=-rlogger --env DEBUG=1 ruby script.rb` + ## BUNDLE INSTALL --BINSTUBS If you use the `--binstubs` flag in [bundle install(1)](bundle-install.1.html), Bundler will diff --git a/bundler/spec/commands/exec_spec.rb b/bundler/spec/commands/exec_spec.rb index cbef73363333..81258f49561e 100644 --- a/bundler/spec/commands/exec_spec.rb +++ b/bundler/spec/commands/exec_spec.rb @@ -202,6 +202,61 @@ expect(out).to include("foo") end + it "sets environment variables with --env option" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + ruby_script = "puts ENV['RUBYOPT'] || 'none'" + bundle "exec --env RUBYOPT=foo ruby -e \"#{ruby_script}\"" + expect(out).to include("foo") + end + + it "supports multiple --env flags" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + ruby_script = "puts ENV['RUBYOPT'] || 'none'; puts ENV['DEBUG'] || 'none'" + bundle "exec --env RUBYOPT=foo --env DEBUG=1 ruby -e \"#{ruby_script}\"" + expect(out).to include("foo") + expect(out).to include("1") + end + + it "does not affect Bundler's environment with --env" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + # Test that Bundler itself doesn't see the env vars + bundle "exec --env RUBYOPT=foo ruby -e 'puts ENV[\"RUBYOPT\"]'" + expect(out).to include("foo") + # Bundler should still work normally + bundle "exec myrackup" + expect(out).to eq("1.0.0") + end + + it "works with mixed KEY=VALUE and --env syntax" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + ruby_script = "puts ENV['RUBYOPT'] || 'none'; puts ENV['DEBUG'] || 'none'" + bundle "exec RUBYOPT=foo --env DEBUG=1 ruby -e \"#{ruby_script}\"" + expect(out).to include("foo") + expect(out).to include("1") + end + + it "overrides existing environment variables with --env" do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + ruby_script = "puts ENV['RUBYOPT'] || 'none'" + bundle "exec --env RUBYOPT=override ruby -e \"#{ruby_script}\"", env: { "RUBYOPT" => "original" } + expect(out).to include("override") + end + context "with default gems" do # TODO: Switch to ERB::VERSION once Ruby 3.4 support is dropped, so all # supported rubies include an `erb` gem version where `ERB::VERSION` is