cliver 0.2.2 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/cliver.rb +9 -5
- data/lib/cliver/dependency.rb +5 -4
- data/lib/cliver/detector.rb +3 -11
- data/lib/cliver/shell_capture.rb +35 -0
- data/lib/cliver/version.rb +1 -1
- data/lib/core_ext/file.rb +11 -3
- data/spec/cliver/detector_spec.rb +34 -0
- data/spec/cliver/shell_capture_spec.rb +42 -0
- data/spec/cliver_spec.rb +27 -16
- data/spec/support/executable_mock.rb +45 -0
- metadata +7 -2
data/lib/cliver.rb
CHANGED
@@ -4,6 +4,7 @@ require File.expand_path('../core_ext/file', __FILE__)
|
|
4
4
|
|
5
5
|
require 'cliver/version'
|
6
6
|
require 'cliver/dependency'
|
7
|
+
require 'cliver/shell_capture'
|
7
8
|
require 'cliver/detector'
|
8
9
|
require 'cliver/filter'
|
9
10
|
|
@@ -49,16 +50,19 @@ module Cliver
|
|
49
50
|
end
|
50
51
|
|
51
52
|
# Verify an absolute-path to an executable.
|
52
|
-
# @
|
53
|
-
#
|
54
|
-
#
|
55
|
-
#
|
56
|
-
#
|
53
|
+
# @overload verify!(executable, *requirements, options = {})
|
54
|
+
# @param executable [String] absolute path to an executable
|
55
|
+
# @param requirements (see Cliver::Dependency#initialize)
|
56
|
+
# @option options (see Cliver::Dependency::initialize)
|
57
|
+
# @raise (see Cliver::Dependency#detect!)
|
58
|
+
# @return (see Cliver::Dependency#detect!)
|
57
59
|
def self.verify!(executable, *args, &block)
|
58
60
|
unless File.absolute_path?(executable)
|
59
61
|
raise ArgumentError, "executable path must be absolute, " +
|
60
62
|
"got '#{executable.inspect}'."
|
61
63
|
end
|
64
|
+
options = args.last.kind_of?(Hash) ? args.pop : {}
|
65
|
+
args << options.merge(:path => '.') # ensure path non-empty.
|
62
66
|
Dependency::new(executable, *args, &block).detect!
|
63
67
|
end
|
64
68
|
|
data/lib/cliver/dependency.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'rubygems/requirement'
|
3
|
+
require 'set'
|
3
4
|
|
4
5
|
module Cliver
|
5
6
|
# This is how a dependency is specified.
|
@@ -197,18 +198,18 @@ module Cliver
|
|
197
198
|
def find_executables
|
198
199
|
return enum_for(:find_executables) unless block_given?
|
199
200
|
|
200
|
-
exts = ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : [''
|
201
|
+
exts = (ENV.has_key?('PATHEXT') ? ENV.fetch('PATHEXT').split(';') : []) << ''
|
201
202
|
paths = @path.sub('*', ENV['PATH']).split(File::PATH_SEPARATOR)
|
203
|
+
raise ArgumentError.new('No PATH to search!') if paths.empty?
|
202
204
|
cmds = strict? ? @executables.first(1) : @executables
|
203
205
|
|
204
|
-
|
206
|
+
lookup_cache = Set.new
|
205
207
|
cmds.product(paths, exts).map do |cmd, path, ext|
|
206
208
|
exe = File.absolute_path?(cmd) ? cmd : File.expand_path("#{cmd}#{ext}", path)
|
207
209
|
|
210
|
+
next unless lookup_cache.add?(exe) # don't yield the same exe path 2x
|
208
211
|
next unless File.executable?(exe)
|
209
|
-
next if detected_exes.include?(exe) # don't yield the same exe path 2x
|
210
212
|
|
211
|
-
detected_exes << exe
|
212
213
|
yield exe
|
213
214
|
end
|
214
215
|
end
|
data/lib/cliver/detector.rb
CHANGED
@@ -40,15 +40,15 @@ module Cliver
|
|
40
40
|
# @return [String] - should be contain {Gem::Version}-parsable
|
41
41
|
# version number.
|
42
42
|
def detect_version(executable_path)
|
43
|
-
|
44
|
-
|
43
|
+
capture = ShellCapture.new(version_command(executable_path))
|
44
|
+
unless capture.command_found
|
45
45
|
raise Cliver::Dependency::NotFound.new(
|
46
46
|
"Could not find an executable at given path '#{executable_path}'." +
|
47
47
|
"If this path was not specified explicitly, it is probably a " +
|
48
48
|
"bug in [Cliver](https://github.com/yaauie/cliver/issues)."
|
49
49
|
)
|
50
50
|
end
|
51
|
-
|
51
|
+
capture.stdout[version_pattern] || capture.stderr[version_pattern]
|
52
52
|
end
|
53
53
|
|
54
54
|
# This is the interface that any detector must have.
|
@@ -80,13 +80,5 @@ module Cliver
|
|
80
80
|
def version_command(executable_path)
|
81
81
|
[executable_path, *Array(command_arg)]
|
82
82
|
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
# @api private
|
87
|
-
# A boundary that is useful for testing.
|
88
|
-
def shell_out_and_capture(command)
|
89
|
-
`#{command} 2>&1`
|
90
|
-
end
|
91
83
|
end
|
92
84
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Cliver
|
4
|
+
class ShellCapture
|
5
|
+
attr_reader :stdout, :stderr, :command_found
|
6
|
+
|
7
|
+
# @overlaod initialize(command)
|
8
|
+
# @param command [String] the command to run
|
9
|
+
# @overload initialize(command)
|
10
|
+
# @param command [Array<String>] the command to run; elements in
|
11
|
+
# the supplied array will be shelljoined.
|
12
|
+
# @return [void]
|
13
|
+
def initialize(command)
|
14
|
+
command = command.shelljoin if command.kind_of?(Array)
|
15
|
+
@stdout = @stderr = ''
|
16
|
+
begin
|
17
|
+
Open3.popen3(command) do |i, o, e|
|
18
|
+
@stdout = o.read.chomp
|
19
|
+
@stderr = e.read.chomp
|
20
|
+
end
|
21
|
+
# Fix for ruby 1.8.7 (and probably earlier):
|
22
|
+
# Open3.popen3 does not raise anything there, but the error goes to STDERR.
|
23
|
+
if @stderr =~ /open3.rb:\d+:in `exec': No such file or directory -.*\(Errno::ENOENT\)/ or
|
24
|
+
@stderr =~ /An exception occurred in a forked block\W+No such file or directory.*\(Errno::ENOENT\)/
|
25
|
+
@stderr = ''
|
26
|
+
@command_found = false
|
27
|
+
else
|
28
|
+
@command_found = true
|
29
|
+
end
|
30
|
+
rescue Errno::ENOENT, IOError
|
31
|
+
@command_found = false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/cliver/version.rb
CHANGED
data/lib/core_ext/file.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
# Core-Extensions on File
|
3
4
|
class File
|
4
5
|
# determine whether a String path is absolute.
|
5
6
|
# @example
|
@@ -9,10 +10,17 @@ class File
|
|
9
10
|
# File.absolute_path?('/foo/bar') #=> true
|
10
11
|
# File.absolute_path?('C:foo/bar') #=> false
|
11
12
|
# File.absolute_path?('C:/foo/bar') #=> true
|
12
|
-
# @param [String] - a pathname
|
13
|
+
# @param path [String] - a pathname
|
13
14
|
# @return [Boolean]
|
14
|
-
def self.absolute_path?(path)
|
15
|
-
|
15
|
+
def self.absolute_path?(path, platform = :default)
|
16
|
+
pattern = case platform
|
17
|
+
when :default then ABSOLUTE_PATH_PATTERN
|
18
|
+
when :windows then WINDOWS_ABSOLUTE_PATH_PATTERN
|
19
|
+
when :posix then POSIX_ABSOLUTE_PATH_PATTERN
|
20
|
+
else raise ArgumentError, "Unsupported platform '#{platform.inspect}'"
|
21
|
+
end
|
22
|
+
|
23
|
+
false | path[pattern]
|
16
24
|
end
|
17
25
|
|
18
26
|
unless defined?(POSIX_ABSOLUTE_PATH_PATTERN)
|
@@ -41,4 +41,38 @@ describe Cliver::Detector do
|
|
41
41
|
its(:command_arg) { should eq [version_arg] }
|
42
42
|
its(:version_pattern) { should eq regexp_arg }
|
43
43
|
end
|
44
|
+
|
45
|
+
context 'detecting a command' do
|
46
|
+
before(:each) do
|
47
|
+
Cliver::ShellCapture.stub(:new => capture)
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'that reports version on stdout' do
|
51
|
+
let(:capture) { double('capture', :stdout => '1.1',
|
52
|
+
:stderr => 'Warning: There is a monkey 1.2 metres left of you.',
|
53
|
+
:command_found => true) }
|
54
|
+
|
55
|
+
it 'should prefer the stdout output' do
|
56
|
+
expect(detector.detect_version('foo')).to eq('1.1')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'that reports version on stderr' do
|
61
|
+
let(:capture) { double('capture', :stdout => '',
|
62
|
+
:stderr => 'Version: 1.666',
|
63
|
+
:command_found => true) }
|
64
|
+
|
65
|
+
it 'should prefer the stderr output' do
|
66
|
+
expect(detector.detect_version('foo')).to eq('1.666')
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'that does not exist' do
|
71
|
+
let(:capture) { Cliver::ShellCapture.new('acommandnosystemshouldhave123') }
|
72
|
+
|
73
|
+
it 'should raise an exception' do
|
74
|
+
expect { detector.detect_version('foo') }.to raise_error(Cliver::Dependency::NotFound)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
44
78
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'cliver'
|
3
|
+
|
4
|
+
describe Cliver::ShellCapture do
|
5
|
+
let(:test_command) { 'test command' }
|
6
|
+
subject { Cliver::ShellCapture.new(test_command) }
|
7
|
+
|
8
|
+
context 'a command that exists' do
|
9
|
+
let(:intended_stdout) { StringIO.new('1.1.1').tap(&:rewind) }
|
10
|
+
let(:intended_stderr) { StringIO.new('foo baar 1').tap(&:rewind) }
|
11
|
+
let(:intended_stdin) { StringIO.new('').tap(&:rewind) }
|
12
|
+
|
13
|
+
['test command', %w(test command)].each do |input|
|
14
|
+
context "with #{input.class.name} input" do
|
15
|
+
let(:test_command) { input }
|
16
|
+
|
17
|
+
before(:each) do
|
18
|
+
Open3.should_receive(:popen3) do |*args|
|
19
|
+
args.size.should eq 1
|
20
|
+
args.first.should == 'test command'
|
21
|
+
end.and_yield(intended_stdin, intended_stdout, intended_stderr)
|
22
|
+
end
|
23
|
+
|
24
|
+
its(:stdout) { should eq '1.1.1' }
|
25
|
+
its(:stderr) { should eq 'foo baar 1' }
|
26
|
+
its(:command_found) { should be_true }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'looking for a command that does not exist' do
|
32
|
+
before(:each) do
|
33
|
+
Open3.should_receive(:popen3) do |command|
|
34
|
+
command.should eq test_command
|
35
|
+
raise Errno::ENOENT.new("No such file or directory - #{test_command}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
its(:stdout) { should eq '' }
|
39
|
+
its(:stderr) { should eq '' }
|
40
|
+
its(:command_found) { should be_false }
|
41
|
+
end
|
42
|
+
end
|
data/spec/cliver_spec.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'cliver'
|
3
3
|
require 'spec_helper'
|
4
|
+
require File.expand_path('../support/executable_mock', __FILE__)
|
5
|
+
|
6
|
+
RSpec::Matchers.define :be_filesystem_equivalent_of do |expected|
|
7
|
+
match do |actual|
|
8
|
+
ExecutableMock.new(expected) == actual
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
4
12
|
|
5
13
|
describe Cliver do
|
6
14
|
# The setup. Your test will likeley interact with subject.
|
@@ -10,29 +18,32 @@ describe Cliver do
|
|
10
18
|
# These can get overridden in context blocks
|
11
19
|
let(:method) { raise ArgumentError, 'spec didn\'t specify :method' }
|
12
20
|
let(:args) { raise ArgumentError, 'spec didn\'t specify :args' }
|
13
|
-
let(:block) {
|
21
|
+
let(:block) { version_directory.method(:version) }
|
22
|
+
|
23
|
+
# BecauseWindows. This enables us to mock out File::executable?
|
24
|
+
# and the responses from our detectors given any representation
|
25
|
+
# of a file path.
|
26
|
+
let(:version_directory) do
|
27
|
+
ExecutableMock::Registry.new(version_map)
|
28
|
+
end
|
14
29
|
|
15
30
|
before(:each) do
|
16
|
-
File.stub(:executable
|
17
|
-
# Cliver::Dependency.any_instance.stub(:detect_version) do |arg|
|
18
|
-
# version_map[arg]
|
19
|
-
# end
|
31
|
+
File.stub(:executable?, &version_directory.method(:executable?))
|
20
32
|
end
|
21
33
|
|
22
34
|
let(:options) do
|
23
35
|
{
|
24
|
-
:path => path,
|
36
|
+
:path => path.join(File::PATH_SEPARATOR),
|
25
37
|
:executable => executable,
|
26
38
|
}
|
27
39
|
end
|
28
|
-
let(:executables) { version_map.keys }
|
29
40
|
let(:args) do
|
30
41
|
args = [Array(executable)]
|
31
42
|
args.concat Array(requirement)
|
32
43
|
args << options
|
33
44
|
end
|
34
45
|
|
35
|
-
let(:path) { '/foo/bar
|
46
|
+
let(:path) { ['/foo/bar','/baz/bingo'] }
|
36
47
|
let(:executable) { 'doodle' }
|
37
48
|
let(:requirement) { '~>1.1'}
|
38
49
|
|
@@ -55,14 +66,14 @@ describe Cliver do
|
|
55
66
|
end
|
56
67
|
context '::detect' do
|
57
68
|
let(:method) { :detect }
|
58
|
-
it { should
|
69
|
+
it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
|
59
70
|
end
|
60
71
|
context '::detect!' do
|
61
72
|
let(:method) { :detect! }
|
62
73
|
it 'should not raise' do
|
63
74
|
expect { action }.to_not raise_exception
|
64
75
|
end
|
65
|
-
it { should
|
76
|
+
it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
|
66
77
|
end
|
67
78
|
end
|
68
79
|
|
@@ -137,7 +148,7 @@ describe Cliver do
|
|
137
148
|
let(:version_map) do
|
138
149
|
{'/baz/bingo/doodle' => '1.2.1'}
|
139
150
|
end
|
140
|
-
let(:path) { '/fiddle/foo
|
151
|
+
let(:path) { ['/fiddle/foo','/deedle/dee'] }
|
141
152
|
|
142
153
|
context 'that is absolute' do
|
143
154
|
let(:executable) { '/baz/bingo/doodle' }
|
@@ -212,14 +223,14 @@ describe Cliver do
|
|
212
223
|
end
|
213
224
|
context '::detect' do
|
214
225
|
let(:method) { :detect }
|
215
|
-
it { should
|
226
|
+
it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
|
216
227
|
end
|
217
228
|
context '::detect!' do
|
218
229
|
let(:method) { :detect! }
|
219
230
|
it 'should not raise' do
|
220
231
|
expect { action }.to_not raise_exception
|
221
232
|
end
|
222
|
-
it { should
|
233
|
+
it { should be_filesystem_equivalent_of '/baz/bingo/doodle' }
|
223
234
|
end
|
224
235
|
end
|
225
236
|
end
|
@@ -262,7 +273,7 @@ describe Cliver do
|
|
262
273
|
end
|
263
274
|
context '::detect' do
|
264
275
|
let(:method) { :detect }
|
265
|
-
it { should
|
276
|
+
it { should be_filesystem_equivalent_of '/baz/bingo/primary' }
|
266
277
|
end
|
267
278
|
end
|
268
279
|
context 'and primary insufficient' do
|
@@ -275,7 +286,7 @@ describe Cliver do
|
|
275
286
|
context 'the secondary' do
|
276
287
|
context '::detect' do
|
277
288
|
let(:method) { :detect }
|
278
|
-
it { should
|
289
|
+
it { should be_filesystem_equivalent_of '/foo/bar/fallback' }
|
279
290
|
end
|
280
291
|
end
|
281
292
|
end
|
@@ -289,7 +300,7 @@ describe Cliver do
|
|
289
300
|
end
|
290
301
|
context '::detect' do
|
291
302
|
let(:method) { :detect }
|
292
|
-
it { should
|
303
|
+
it { should be_filesystem_equivalent_of '/foo/bar/fallback' }
|
293
304
|
end
|
294
305
|
end
|
295
306
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class ExecutableMock
|
4
|
+
def initialize(path)
|
5
|
+
@path = _to_platform_abs_path(path)
|
6
|
+
end
|
7
|
+
attr_reader :path
|
8
|
+
attr_reader :version
|
9
|
+
|
10
|
+
def == (other_path)
|
11
|
+
other_path = _to_platform_abs_path(other_path)
|
12
|
+
if other_path[/[A-Z]:/i] # windows
|
13
|
+
pattern = /\A#{self.path}(#{(ENV['PATHEXT']||'').split(';').map(&Regexp::method(:escape)).join('|')})?\Z/i
|
14
|
+
pattern =~ other_path
|
15
|
+
else # posix
|
16
|
+
self.path == other_path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def _to_platform_abs_path(source)
|
23
|
+
(File::absolute_path?(source, :windows) && !File::absolute_path?(source, :posix)) ?
|
24
|
+
source.tr('\\','/') :
|
25
|
+
File.expand_path(source)
|
26
|
+
end
|
27
|
+
|
28
|
+
class Registry
|
29
|
+
def initialize(version_map)
|
30
|
+
@registry = {}
|
31
|
+
version_map.each do |path,version|
|
32
|
+
@registry[ExecutableMock.new(path)] = version
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def executable?(path)
|
37
|
+
false | version(path)
|
38
|
+
end
|
39
|
+
|
40
|
+
def version(path)
|
41
|
+
key = @registry.keys.find {|exe| exe == path }
|
42
|
+
key && @registry[key]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cliver
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-10-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -127,12 +127,15 @@ files:
|
|
127
127
|
- lib/cliver/dependency.rb
|
128
128
|
- lib/cliver/detector.rb
|
129
129
|
- lib/cliver/filter.rb
|
130
|
+
- lib/cliver/shell_capture.rb
|
130
131
|
- lib/cliver/version.rb
|
131
132
|
- lib/core_ext/file.rb
|
132
133
|
- spec/cliver/detector_spec.rb
|
134
|
+
- spec/cliver/shell_capture_spec.rb
|
133
135
|
- spec/cliver_spec.rb
|
134
136
|
- spec/core_ext/file_spec.rb
|
135
137
|
- spec/spec_helper.rb
|
138
|
+
- spec/support/executable_mock.rb
|
136
139
|
homepage: https://www.github.com/yaauie/cliver
|
137
140
|
licenses:
|
138
141
|
- MIT
|
@@ -160,7 +163,9 @@ specification_version: 3
|
|
160
163
|
summary: Cross-platform version constraints for cli tools
|
161
164
|
test_files:
|
162
165
|
- spec/cliver/detector_spec.rb
|
166
|
+
- spec/cliver/shell_capture_spec.rb
|
163
167
|
- spec/cliver_spec.rb
|
164
168
|
- spec/core_ext/file_spec.rb
|
165
169
|
- spec/spec_helper.rb
|
170
|
+
- spec/support/executable_mock.rb
|
166
171
|
has_rdoc: yard
|