Skip to content
Merged
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
2 changes: 1 addition & 1 deletion lib/completely/commands/generate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}`"
Expand Down
47 changes: 17 additions & 30 deletions lib/completely/installer.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'fileutils'

module Completely
class Installer
class << self
Expand Down Expand Up @@ -38,27 +40,16 @@ 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
install_command.join ' '
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
Expand All @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion spec/approvals/cli/install/dry
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion spec/approvals/cli/install/stdin-dry
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sudo cp <tmpfile-path> /usr/share/bash-completion/completions/completely-test
cp <tmpfile-path> /home/USER/.local/share/bash-completion/completions/completely-test
2 changes: 1 addition & 1 deletion spec/approvals/cli/uninstall/dry
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions spec/completely/commands/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions spec/completely/commands/install_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <tmpfile-path>')
.except(%r[/home/([^/]+)], '/home/USER')
end
end

Expand Down
96 changes: 51 additions & 45 deletions spec/completely/installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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: }
Expand All @@ -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(' ')
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down