diff --git a/lib/completely/commands/generate.rb b/lib/completely/commands/generate.rb index 091bd06..fb013ff 100644 --- a/lib/completely/commands/generate.rb +++ b/lib/completely/commands/generate.rb @@ -60,7 +60,7 @@ def install(content) end def show(content) = puts content - + def save(content) File.write output_path, content say "Saved m`#{output_path}`" diff --git a/lib/completely/installer.rb b/lib/completely/installer.rb index f4eebdf..f1d2978 100644 --- a/lib/completely/installer.rb +++ b/lib/completely/installer.rb @@ -1,3 +1,5 @@ +require 'fileutils' + module Completely class Installer class << self @@ -38,18 +40,8 @@ def initialize(program:, script_path: nil) @script_path = script_path end - def target_directories - @target_directories ||= %W[ - /usr/share/bash-completion/completions - /usr/local/etc/bash_completion.d - #{Dir.home}/.local/share/bash-completion/completions - #{Dir.home}/.bash_completion.d - ] - end - def install_command - result = root_user? ? [] : %w[sudo] - result + %W[cp #{script_path} #{target_path}] + %W[cp #{script_path} #{target_path}] end def install_command_string @@ -57,8 +49,7 @@ def install_command_string end def uninstall_command - result = root_user? ? [] : %w[sudo] - result + %w[rm -f] + target_directories.map { |dir| "#{dir}/#{program}" } + %W[rm -f #{target_path}] end def uninstall_command_string @@ -70,14 +61,12 @@ def target_path end def install(force: false) - unless completions_path - raise InstallError, 'Cannot determine system completions directory' - end - unless script_exist? raise InstallError, "Cannot find script: m`#{script_path}`" end + FileUtils.mkdir_p completions_path + if target_exist? && !force raise InstallError, "File exists: m`#{target_path}`" end @@ -99,22 +88,20 @@ def script_exist? File.exist? script_path end - def root_user? - Process.uid.zero? + def completions_path + @completions_path ||= "#{user_completions_base_dir}/completions" end - def completions_path - @completions_path ||= begin - result = nil - target_directories.each do |target| - if Dir.exist? target - result = target - break - end - end + def user_completions_base_dir + @user_completions_base_dir ||= bash_completion_user_dir || "#{data_home}/bash-completion" + end - result - end + def bash_completion_user_dir + ENV['BASH_COMPLETION_USER_DIR']&.split(':')&.find { |entry| !entry.empty? } + end + + def data_home + ENV['XDG_DATA_HOME'] || "#{Dir.home}/.local/share" end end end diff --git a/spec/approvals/cli/install/dry b/spec/approvals/cli/install/dry index 02bae48..3a4ba7e 100644 --- a/spec/approvals/cli/install/dry +++ b/spec/approvals/cli/install/dry @@ -1 +1 @@ -sudo cp completely.bash /usr/share/bash-completion/completions/completely-test +cp completely.bash /home/USER/.local/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/install/stdin-dry b/spec/approvals/cli/install/stdin-dry index 47144da..cfbfd0a 100644 --- a/spec/approvals/cli/install/stdin-dry +++ b/spec/approvals/cli/install/stdin-dry @@ -1 +1 @@ -sudo cp /usr/share/bash-completion/completions/completely-test +cp /home/USER/.local/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/uninstall/dry b/spec/approvals/cli/uninstall/dry index d582874..c9791f4 100644 --- a/spec/approvals/cli/uninstall/dry +++ b/spec/approvals/cli/uninstall/dry @@ -1 +1 @@ -sudo rm -f /usr/share/bash-completion/completions/completely-test /usr/local/etc/bash_completion.d/completely-test /home/vagrant/.local/share/bash-completion/completion/completely-test /home/vagrant/.bash_completion.d/completely-test +rm -f /home/USER/.local/share/bash-completion/completions/completely-test diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index fb6a198..9d152ea 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -119,16 +119,16 @@ context 'with --install PROGRAM' do let(:mock_installer) do instance_double Installer, - install: true, + install: true, install_command_string: 'stubbed install_command_string', - target_path: 'stubbed target_path' + target_path: 'stubbed target_path' end it 'passes the generated script to the installer' do allow(Installer).to receive(:from_string) .with( program: 'mycli', - string: a_string_matching(/bash completions script/) + string: a_string_matching(/bash completions script/) ).and_return(mock_installer) expect(mock_installer).to receive(:install) diff --git a/spec/completely/commands/install_spec.rb b/spec/completely/commands/install_spec.rb index 014b476..6e2a93f 100644 --- a/spec/completely/commands/install_spec.rb +++ b/spec/completely/commands/install_spec.rb @@ -52,6 +52,7 @@ expect { subject.execute %w[install completely-test --dry] } .to output_approval('cli/install/dry') + .except(%r[/home/([^/]+)], '/home/USER') end end @@ -64,6 +65,7 @@ expect { subject.execute %w[install completely-test - --dry] } .to output_approval('cli/install/stdin-dry') .except(/cp [^\s]*completely-[^\s]*/, 'cp ') + .except(%r[/home/([^/]+)], '/home/USER') end end diff --git a/spec/completely/installer_spec.rb b/spec/completely/installer_spec.rb index f74f6fc..bf0a67b 100644 --- a/spec/completely/installer_spec.rb +++ b/spec/completely/installer_spec.rb @@ -4,14 +4,9 @@ let(:leeway) { RUBY_VERSION < '3.2.0' ? 0 : 3 } let(:program) { 'completely-test' } let(:script_path) { 'completions.bash' } - let(:targets) { subject.target_directories.map { |dir| "#{dir}/#{program}" } } - let(:install_command) do - %W[sudo cp #{subject.script_path} #{subject.target_path}] - end - - let(:uninstall_command) do - %w[sudo rm -f] + targets - end + let(:target_path) { "#{Dir.home}/.local/share/bash-completion/completions/#{program}" } + let(:install_command) { %W[cp #{subject.script_path} #{subject.target_path}] } + let(:uninstall_command) { %W[rm -f #{subject.target_path}] } describe '::from_io' do subject { described_class.from_io program:, io: } @@ -33,36 +28,61 @@ end end - describe '#target_directories' do - it 'returns an array of potential completion directories' do - expect(subject.target_directories).to be_an Array - expect(subject.target_directories.size).to eq 4 + describe '#target_path' do + it 'returns a user-level target path' do + expect(subject.target_path).to eq target_path end - end - describe '#target_path' do - it 'returns the first matching path' do - expect(subject.target_path) - .to eq '/usr/share/bash-completion/completions/completely-test' + context 'when BASH_COMPLETION_USER_DIR is set' do + around do |example| + original = ENV['BASH_COMPLETION_USER_DIR'] + ENV['BASH_COMPLETION_USER_DIR'] = '/tmp/completely-user-dir' + example.run + ensure + ENV['BASH_COMPLETION_USER_DIR'] = original + end + + it 'uses BASH_COMPLETION_USER_DIR/completions' do + expect(subject.target_path).to eq '/tmp/completely-user-dir/completions/completely-test' + end end - end - describe '#install_command' do - it 'returns a copy command as an array' do - expect(subject.install_command) - .to eq %w[sudo cp completions.bash /usr/share/bash-completion/completions/completely-test] + context 'when XDG_DATA_HOME is set' do + around do |example| + original = ENV['XDG_DATA_HOME'] + ENV['XDG_DATA_HOME'] = '/tmp/completely-xdg' + example.run + ensure + ENV['XDG_DATA_HOME'] = original + end + + it 'uses XDG_DATA_HOME/bash-completion/completions' do + expect(subject.target_path).to eq '/tmp/completely-xdg/bash-completion/completions/completely-test' + end end - context 'when the user is root' do - it 'returns the command without sudo' do - allow(subject).to receive(:root_user?).and_return true + context 'when BASH_COMPLETION_USER_DIR has multiple entries' do + around do |example| + original = ENV['BASH_COMPLETION_USER_DIR'] + ENV['BASH_COMPLETION_USER_DIR'] = ':/tmp/completely-first:/tmp/completely-second' + example.run + ensure + ENV['BASH_COMPLETION_USER_DIR'] = original + end - expect(subject.install_command) - .to eq %w[cp completions.bash /usr/share/bash-completion/completions/completely-test] + it 'uses the first non-empty entry' do + expect(subject.target_path).to eq '/tmp/completely-first/completions/completely-test' end end end + describe '#install_command' do + it 'returns a copy command as an array' do + expect(subject.install_command) + .to eq %W[cp completions.bash #{target_path}] + end + end + describe '#install_command_string' do it 'returns the install command as a string' do expect(subject.install_command_string).to eq subject.install_command.join(' ') @@ -71,15 +91,7 @@ describe '#uninstall_command' do it 'returns an rm command as an array' do - expect(subject.uninstall_command).to eq %w[sudo rm -f] + targets - end - - context 'when the user is root' do - it 'returns the command without sudo' do - allow(subject).to receive(:root_user?).and_return true - - expect(subject.uninstall_command).to eq %w[rm -f] + targets - end + expect(subject.uninstall_command).to eq %W[rm -f #{target_path}] end end @@ -95,15 +107,7 @@ before do allow(subject).to receive_messages(script_path: existing_file, target_path: missing_file) - end - - context 'when the completions_path cannot be found' do - it 'raises an error' do - allow(subject).to receive(:completions_path).and_return nil - - expect { subject.install }.to raise_approval('installer/install-no-dir') - .diff(leeway) - end + allow(FileUtils).to receive(:mkdir_p) end context 'when the script cannot be found' do @@ -128,6 +132,7 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return existing_file + expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install force: true @@ -138,6 +143,7 @@ it 'proceeds to install' do allow(subject).to receive(:target_path).and_return missing_file + expect(FileUtils).to receive(:mkdir_p) expect(subject).to receive(:system).with(*install_command) subject.install