diff --git a/lib/bundler/cli/open.rb b/lib/bundler/cli/open.rb index f24693b84367..64d44e6a0b7c 100644 --- a/lib/bundler/cli/open.rb +++ b/lib/bundler/cli/open.rb @@ -18,12 +18,22 @@ def run Bundler.ui.info "Unable to open #{name} because it's a default gem, so the directory it would normally be installed to does not exist." else root_path = spec.full_gem_path - require "shellwords" - command = Shellwords.split(editor) << File.join([root_path, path].compact) + command = editor_command(editor) << File.join([root_path, path].compact) Bundler.with_original_env do system(*command, { chdir: root_path }) end || Bundler.ui.info("Could not run '#{command.join(" ")}'") end end + + def editor_command(editor) + # On Windows an editor is often configured with a full path such as + # C:\Program Files\Microsoft VS Code\Code.exe, which shell splitting + # would corrupt. Take a value that names an existing file as a + # single word. + return [editor] if Gem.win_platform? && File.file?(editor) + + require "shellwords" + Shellwords.split(editor) + end end end diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index 0fe90dc8b835..d59237d3b99b 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -70,7 +70,18 @@ def open_gem(name) end def open_editor(path) - system(*@editor.split(/\s+/) + [path], { chdir: path }) + system(*editor_command(@editor), path, { chdir: path }) + end + + def editor_command(editor) # :nodoc: + # On Windows an editor is often configured with a full path such as + # C:\Program Files\Microsoft VS Code\Code.exe, which shell splitting + # would corrupt. Take a value that names an existing file as a + # single word. + return [editor] if Gem.win_platform? && File.file?(editor) + + require "shellwords" + Shellwords.split(editor) end def spec_for(name) diff --git a/spec/bundler/cli/open_spec.rb b/spec/bundler/cli/open_spec.rb new file mode 100644 index 000000000000..bb8c2b59de10 --- /dev/null +++ b/spec/bundler/cli/open_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "bundler/cli" +require "bundler/cli/open" + +RSpec.describe Bundler::CLI::Open do + subject { described_class.new({}, "rack") } + + describe "#editor_command" do + it "takes an editor path that names an existing file as a single word on Windows" do + editor = File.join(Dir.mktmpdir, "editor.exe") + FileUtils.touch editor + allow(Gem).to receive(:win_platform?).and_return(true) + + expect(subject.editor_command(editor)).to eq([editor]) + end + + it "keeps backslashes in a quoted path" do + expect(subject.editor_command('"C:\Program Files\Microsoft VS Code\Code.exe" -w')). + to eq(['C:\Program Files\Microsoft VS Code\Code.exe', "-w"]) + end + + it "splits an editor with arguments" do + expect(subject.editor_command("code -w")).to eq(["code", "-w"]) + end + + it "splits an existing path on POSIX" do + dir = Dir.mktmpdir + FileUtils.mkdir_p File.join(dir, "editor dir") + editor = File.join(dir, "editor dir", "editor") + FileUtils.touch editor + allow(Gem).to receive(:win_platform?).and_return(false) + + expect(subject.editor_command(editor)).to eq(editor.split(" ")) + end + end +end diff --git a/spec/support/shards.rb b/spec/support/shards.rb index 396ee8049d6b..28a0a5142842 100644 --- a/spec/support/shards.rb +++ b/spec/support/shards.rb @@ -202,6 +202,7 @@ module Shards "spec/install/path_spec.rb", "spec/bundler/fetcher/gem_remote_fetcher_local_ssl_server_spec.rb", "spec/bundler/plugin/unloaded_source_spec.rb", + "spec/bundler/cli/open_spec.rb", ], }.freeze end diff --git a/test/rubygems/test_gem_commands_open_command.rb b/test/rubygems/test_gem_commands_open_command.rb index addc7427e2dc..3a774a9343c0 100644 --- a/test/rubygems/test_gem_commands_open_command.rb +++ b/test/rubygems/test_gem_commands_open_command.rb @@ -43,6 +43,34 @@ def test_execute assert_equal "", @ui.output end + def test_editor_command_windows_path + win_platform = Gem.win_platform? + + editor = if win_platform + FileUtils.mkdir_p File.join(@tempdir, "editor dir") + File.join(@tempdir, "editor dir", "editor.exe").tr("/", "\\") + else + # backslashes and spaces are plain filename characters on POSIX + File.join(@tempdir, 'C:\editor dir\editor.exe') + end + FileUtils.touch editor + + Gem.win_platform = true + + assert_equal [editor], @cmd.editor_command(editor) + ensure + Gem.win_platform = win_platform + end + + def test_editor_command_quoted_path + assert_equal ['C:\editor dir\editor.exe', "-w"], + @cmd.editor_command('"C:\editor dir\editor.exe" -w') + end + + def test_editor_command_with_arguments + assert_equal %w[code -w], @cmd.editor_command("code -w") + end + def test_wrong_version @cmd.options[:version] = "4.0" @cmd.options[:args] = %w[foo]