diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index ea85f9af22ab..721cc0cc6960 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -432,13 +432,18 @@ 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 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") @@ -447,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 9428e9db3bf8..9cb0be9eaac1 100644 --- a/bundler/lib/bundler/cli/exec.rb +++ b/bundler/lib/bundler/cli/exec.rb @@ -10,6 +10,24 @@ class CLI::Exec 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) + @env[key] = value + end + @cmd = args.shift @args = args @args << { close_others: !options.keep_file_descriptors? } unless Bundler.current_ruby.jruby? @@ -39,7 +57,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/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 cad1ac4ba3ce..81258f49561e 100644 --- a/bundler/spec/commands/exec_spec.rb +++ b/bundler/spec/commands/exec_spec.rb @@ -192,6 +192,71 @@ 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 + + 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