Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions bundler/lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand Down
24 changes: 23 additions & 1 deletion bundler/lib/bundler/cli/exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion bundler/lib/bundler/man/bundle-exec.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ bundle-exec(1) -- Execute a command in the context of the bundle

## SYNOPSIS

`bundle exec` [--keep-file-descriptors] [--gemfile=GEMFILE] <command>
`bundle exec` [--keep-file-descriptors] [--gemfile=GEMFILE] [--env KEY=VALUE] <command>

## DESCRIPTION

Expand All @@ -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
Expand Down
65 changes: 65 additions & 0 deletions bundler/spec/commands/exec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down