diff --git a/lib/completely/commands/base.rb b/lib/completely/commands/base.rb index d3bf9ca..3107280 100644 --- a/lib/completely/commands/base.rb +++ b/lib/completely/commands/base.rb @@ -35,7 +35,13 @@ def script end def completions - @completions ||= Completions.load(config_path, function_name: args['--function']) + @completions ||= if config_path == '-' + raise Error, 'Nothing is piped on stdin' if $stdin.tty? + + Completions.read $stdin, function_name: args['--function'] + else + Completions.load config_path, function_name: args['--function'] + end end def config_path @@ -43,7 +49,11 @@ def config_path end def output_path - @output_path ||= args['OUTPUT_PATH'] || ENV['COMPLETELY_OUTPUT_PATH'] || "#{config_basename}.bash" + @output_path ||= args['OUTPUT_PATH'] || ENV['COMPLETELY_OUTPUT_PATH'] || stdout || "#{config_basename}.bash" + end + + def stdout + @stdout ||= config_path == '-' ? '-' : nil end def config_basename diff --git a/lib/completely/commands/generate.rb b/lib/completely/commands/generate.rb index b42d54a..6fea310 100644 --- a/lib/completely/commands/generate.rb +++ b/lib/completely/commands/generate.rb @@ -3,7 +3,7 @@ module Completely module Commands class Generate < Base - help 'Generate the bash completion script to a file' + help 'Generate the bash completion script to file or stdout' usage 'completely generate [CONFIG_PATH OUTPUT_PATH --function NAME --wrap NAME]' usage 'completely generate (-h|--help)' @@ -12,10 +12,19 @@ class Generate < Base option '-w --wrap NAME', 'Wrap the completion script inside a function that echos the ' \ 'script. This is useful if you wish to embed it directly in your script.' - param_config_path + param 'CONFIG_PATH', <<~USAGE + Path to the YAML configuration file [default: completely.yaml]. + Use '-' to read from stdin. + + Can also be set by an environment variable. + USAGE + param 'OUTPUT_PATH', <<~USAGE Path to the output bash script. - When not provided, the name of the input file will be used with a .bash extension. + Use '-' for stdout. + + When not provided, the name of the input file will be used with a .bash extension, unless the input is stdin - in this case the default will be to output to stdout. + Can also be set by an environment variable. USAGE @@ -26,8 +35,12 @@ class Generate < Base def run wrap = args['--wrap'] output = wrap ? wrapper_function(wrap) : script - File.write output_path, output - say "Saved m`#{output_path}`" + if output_path == '-' + puts output + else + File.write output_path, output + say "Saved m`#{output_path}`" + end syntax_warning unless completions.valid? end diff --git a/lib/completely/commands/install.rb b/lib/completely/commands/install.rb index fa300eb..4834a5a 100644 --- a/lib/completely/commands/install.rb +++ b/lib/completely/commands/install.rb @@ -1,3 +1,4 @@ +require 'tempfile' require 'completely/commands/base' module Completely @@ -17,9 +18,19 @@ class Install < Base option '-d --dry', 'Show the installation command but do not run it' param 'PROGRAM', 'Name of the program the completions are for.' - param 'SCRIPT_PATH', 'Path to the source bash script [default: completely.bash].' + param 'SCRIPT_PATH', <<~USAGE + Path to the source bash script [default: completely.bash]. + Use '-' to provide the script via stdin. + USAGE def run + if script_path == '-' + raise InstallError, "Nothing is piped on stdin" if $stdin.tty? + + @script_path = tempfile.path + File.write script_path, $stdin.read + end + if args['--dry'] puts installer.install_command_string return @@ -32,14 +43,18 @@ def run say 'You may need to restart your session to test it' end + def tempfile + @tempfile ||= Tempfile.new('stdin-completely-') + end + private def installer - Installer.new program: args['PROGRAM'], script_path: script_path + @installer ||= Installer.new(program: args['PROGRAM'], script_path: script_path) end def script_path - args['SCRIPT_PATH'] || 'completely.bash' + @script_path ||= args['SCRIPT_PATH'] || 'completely.bash' end end end diff --git a/lib/completely/commands/preview.rb b/lib/completely/commands/preview.rb index 82f7121..e411ed0 100644 --- a/lib/completely/commands/preview.rb +++ b/lib/completely/commands/preview.rb @@ -3,7 +3,7 @@ module Completely module Commands class Preview < Base - help 'Generate the bash completion script to STDOUT' + help 'Generate the bash completion script to stdout' usage 'completely preview [CONFIG_PATH --function NAME]' usage 'completely preview (-h|--help)' diff --git a/lib/completely/completions.rb b/lib/completely/completions.rb index 9f0a013..955a4f1 100644 --- a/lib/completely/completions.rb +++ b/lib/completely/completions.rb @@ -6,9 +6,12 @@ class Completions attr_reader :config class << self - def load(config_path, function_name: nil) - config = Config.load config_path - new config, function_name: function_name + def load(path, function_name: nil) + new Config.load(path), function_name: function_name + end + + def read(io, function_name: nil) + new Config.read(io), function_name: function_name end end @@ -26,7 +29,7 @@ def patterns end def valid? - pattern_prefixes.uniq.count == 1 + pattern_prefixes.uniq.one? end def script diff --git a/lib/completely/config.rb b/lib/completely/config.rb index fdf4d6e..643904c 100644 --- a/lib/completely/config.rb +++ b/lib/completely/config.rb @@ -3,17 +3,14 @@ class Config attr_reader :config, :options class << self - def load(config_path) - begin - config = YAML.load_file config_path, aliases: true - rescue ArgumentError - # :nocov: - config = YAML.load_file config_path - # :nocov: - end - - new config + def parse(str) + new YAML.load(str, aliases: true) + rescue Psych::Exception => e + raise ParseError, "Invalid YAML: #{e.message}" end + + def load(path) = parse(File.read(path)) + def read(io) = parse(io.read) end def initialize(config) diff --git a/lib/completely/exceptions.rb b/lib/completely/exceptions.rb index 1ebd7a9..cd02a1f 100644 --- a/lib/completely/exceptions.rb +++ b/lib/completely/exceptions.rb @@ -1,4 +1,5 @@ module Completely class Error < StandardError; end class InstallError < Error; end + class ParseError < Error; end end diff --git a/lib/completely/installer.rb b/lib/completely/installer.rb index e877d58..eb5b2c4 100644 --- a/lib/completely/installer.rb +++ b/lib/completely/installer.rb @@ -73,15 +73,13 @@ def root_user? end def completions_path - @completions_path ||= completions_path! - end + @completions_path ||= begin + target_directories.each do |target| + return target if Dir.exist? target + end - def completions_path! - target_directories.each do |target| - return target if Dir.exist? target + nil end - - nil end end end diff --git a/spec/approvals/cli/commands b/spec/approvals/cli/commands index 84a8522..639d394 100644 --- a/spec/approvals/cli/commands +++ b/spec/approvals/cli/commands @@ -2,8 +2,8 @@ Completely - Bash Completions Generator Commands: init Create a new sample YAML configuration file - preview Generate the bash completion script to STDOUT - generate Generate the bash completion script to a file + preview Generate the bash completion script to stdout + generate Generate the bash completion script to file or stdout test Test completions install Install a bash completion script uninstall Uninstall a bash completion script diff --git a/spec/approvals/cli/generate/custom-path-stdin b/spec/approvals/cli/generate/custom-path-stdin new file mode 100644 index 0000000..1c973a8 --- /dev/null +++ b/spec/approvals/cli/generate/custom-path-stdin @@ -0,0 +1 @@ +Saved spec/tmp/stdin-to-file.bash diff --git a/spec/approvals/cli/generate/help b/spec/approvals/cli/generate/help index e262f90..5bbb67f 100644 --- a/spec/approvals/cli/generate/help +++ b/spec/approvals/cli/generate/help @@ -1,4 +1,4 @@ -Generate the bash completion script to a file +Generate the bash completion script to file or stdout Usage: completely generate [CONFIG_PATH OUTPUT_PATH --function NAME --wrap NAME] @@ -18,12 +18,18 @@ Options: Parameters: CONFIG_PATH Path to the YAML configuration file [default: completely.yaml]. + Use '-' to read from stdin. + Can also be set by an environment variable. OUTPUT_PATH Path to the output bash script. + Use '-' for stdout. + When not provided, the name of the input file will be used with a .bash - extension. + extension, unless the input is stdin - in this case the default will be to + output to stdout. + Can also be set by an environment variable. Environment Variables: diff --git a/spec/approvals/cli/install/help b/spec/approvals/cli/install/help index 2435df1..181822f 100644 --- a/spec/approvals/cli/install/help +++ b/spec/approvals/cli/install/help @@ -25,3 +25,4 @@ Parameters: SCRIPT_PATH Path to the source bash script [default: completely.bash]. + Use '-' to provide the script via stdin. diff --git a/spec/approvals/cli/install/stdin-dry b/spec/approvals/cli/install/stdin-dry new file mode 100644 index 0000000..47144da --- /dev/null +++ b/spec/approvals/cli/install/stdin-dry @@ -0,0 +1 @@ +sudo cp /usr/share/bash-completion/completions/completely-test diff --git a/spec/approvals/cli/install/stdin-install b/spec/approvals/cli/install/stdin-install new file mode 100644 index 0000000..7c7f085 --- /dev/null +++ b/spec/approvals/cli/install/stdin-install @@ -0,0 +1,2 @@ +Saved some-target-path +You may need to restart your session to test it diff --git a/spec/approvals/cli/preview/help b/spec/approvals/cli/preview/help index 6546285..fb122af 100644 --- a/spec/approvals/cli/preview/help +++ b/spec/approvals/cli/preview/help @@ -1,4 +1,4 @@ -Generate the bash completion script to STDOUT +Generate the bash completion script to stdout Usage: completely preview [CONFIG_PATH --function NAME] diff --git a/spec/completely/commands/generate_spec.rb b/spec/completely/commands/generate_spec.rb index a962902..748c037 100644 --- a/spec/completely/commands/generate_spec.rb +++ b/spec/completely/commands/generate_spec.rb @@ -1,8 +1,14 @@ describe Commands::Generate do subject { described_class.new } - before { system 'cp lib/completely/templates/sample.yaml completely.yaml' } - after { system 'rm -f completely.yaml' } + before do + reset_tmp_dir + system 'cp lib/completely/templates/sample.yaml completely.yaml' + end + + after do + system 'rm -f completely.yaml' + end context 'with --help' do it 'shows long usage' do @@ -81,6 +87,26 @@ end end + context 'with stdin and stdout' do + it 'reads config from stdin and writes to stdout' do + allow($stdin).to receive_messages(tty?: false, read: File.read('completely.yaml')) + + expect { subject.execute %w[generate -] } + .to output_approval('cli/generated-script') + end + end + + context 'with stdin and output path' do + let(:outfile) { 'spec/tmp/stdin-to-file.bash' } + + it 'reads config from stdin and writes to file' do + allow($stdin).to receive_messages(tty?: false, read: File.read('completely.yaml')) + + expect { subject.execute %W[generate - #{outfile}] }.to output_approval('cli/generate/custom-path-stdin') + expect(File.read outfile).to match_approval('cli/generated-script') + end + end + context 'with --function NAME' do after { system 'rm -f completely.bash' } diff --git a/spec/completely/commands/install_spec.rb b/spec/completely/commands/install_spec.rb index 3208920..4e09242 100644 --- a/spec/completely/commands/install_spec.rb +++ b/spec/completely/commands/install_spec.rb @@ -34,6 +34,20 @@ end end + context 'with PROGRAM - (stdin)' do + it 'invokes the Installer using a temp file' do + allow(subject).to receive(:installer).and_return(mock_installer) + allow($stdin).to receive_messages(tty?: false, read: 'dummy data') + + expect(mock_installer).to receive(:install) + + expect { subject.execute %w[install completely-test -] } + .to output_approval('cli/install/stdin-install') + + expect(File.read subject.tempfile.path).to eq 'dummy data' + end + end + context 'with PROGRAM --dry' do it 'shows the command and does not install anything' do expect(mock_installer).not_to receive(:install) @@ -43,6 +57,29 @@ end end + context 'with PROGRAM - --dry (stdin)' do + it 'shows the command and does not install anything' do + allow($stdin).to receive_messages(tty?: false, read: 'dummy data') + + expect(mock_installer).not_to receive(:install) + + expect { subject.execute %w[install completely-test - --dry] } + .to output_approval('cli/install/stdin-dry') + .except(/[^\s]*stdin-completely-[^\s]*/, '') + end + + context 'when stdin is empty' do + it 'raises InstallError' do + allow($stdin).to receive_messages(tty?: true, read: nil) + expect(mock_installer).not_to receive(:install) + + expect { subject.execute %w[install completely-test - --dry] } + .to raise_error(InstallError, 'Nothing is piped on stdin') + end + end + end + + context 'when the installer fails' do it 'raises an error' do allow(subject).to receive(:installer).and_return(mock_installer) diff --git a/spec/completely/completions_spec.rb b/spec/completely/completions_spec.rb index 6f95d75..0f29946 100644 --- a/spec/completely/completions_spec.rb +++ b/spec/completely/completions_spec.rb @@ -4,6 +4,13 @@ let(:path) { "spec/fixtures/#{file}.yaml" } let(:file) { 'basic' } + describe '::read' do + it 'reads from io' do + io = double :io, read: "cli: [--help, --version]" + expect(described_class.read(io).config.config).to eq({ 'cli' => %w[--help --version] }) + end + end + describe '#valid?' do context 'when all patterns start with the same word' do it 'returns true' do diff --git a/spec/completely/config_spec.rb b/spec/completely/config_spec.rb index f3b9f50..abca739 100644 --- a/spec/completely/config_spec.rb +++ b/spec/completely/config_spec.rb @@ -3,6 +3,27 @@ let(:path) { "spec/fixtures/#{file}.yaml" } let(:file) { 'nested' } + let(:config_string) { "cli: [--help, --version]" } + let(:config_hash) { { 'cli' => %w[--help --version] } } + + describe '::parse' do + it 'loads config from string' do + expect(described_class.parse(config_string).config).to eq config_hash + end + + context 'when the string is not a valid YAML' do + it 'raises ParseError' do + expect { described_class.parse("not: a: yaml") }.to raise_error(Completely::ParseError) + end + end + end + + describe '::read' do + it 'loads config from io' do + io = double :io, read: config_string + expect(described_class.read(io).config).to eq config_hash + end + end describe '#flat_config' do it 'returns a flat pattern => completions hash' do diff --git a/spec/completely/installer_spec.rb b/spec/completely/installer_spec.rb index 2972015..ed8e9f7 100644 --- a/spec/completely/installer_spec.rb +++ b/spec/completely/installer_spec.rb @@ -25,6 +25,16 @@ expect(subject.target_path) .to eq '/usr/share/bash-completion/completions/completely-test' end + + # This method will not be called if there is no completions path + # The test is here to cover the nil fallback + context 'when no paths found' do + it 'returns nil as the base path' do + allow(subject).to receive(:target_directories).and_return([]) + + expect(subject.target_path).to eq '/completely-test' + end + end end describe '#install_command' do