executioner 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ (c) 2009 Fingertips, Eloy Duran <eloy@fngtps.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
File without changes
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['test/**/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ begin
11
+ require 'jeweler'
12
+ Jeweler::Tasks.new do |s|
13
+ s.name = "executioner"
14
+ s.summary = s.description = "Execute CLI utilities"
15
+ s.email = "eloy@fngtps.com"
16
+ s.homepage = "http://fingertips.github.com"
17
+ s.authors = ["Eloy Duran"]
18
+ end
19
+ rescue LoadError
20
+ end
21
+
22
+ begin
23
+ require 'jewelry_portfolio/tasks'
24
+ JewelryPortfolio::Tasks.new do |p|
25
+ p.account = 'Fingertips'
26
+ end
27
+ rescue LoadError
28
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 2
3
+ :patch: 0
4
+ :major: 0
@@ -0,0 +1,98 @@
1
+ require 'open3'
2
+
3
+ module Executioner
4
+ class ExecutionerError < StandardError; end
5
+ class ProcessError < ExecutionerError; end
6
+ class ExecutableNotFoundError < ExecutionerError; end
7
+
8
+ SEARCH_PATHS = %w{ /bin /usr/bin /usr/local/bin /opt/local/bin }
9
+
10
+ class << self
11
+ attr_accessor :logger
12
+
13
+ def included(klass)
14
+ klass.extend ClassMethods
15
+ end
16
+ end
17
+
18
+ def execute(command, options={})
19
+ command = "#{options[:env].map { |k,v| "#{k}='#{v}'" }.join(' ')} #{command}" if options[:env]
20
+
21
+ Executioner.logger.debug("Executing: `#{command}'") if Executioner.logger
22
+
23
+ output = nil
24
+ Open3.popen3(command) do |stdin, stdout, stderr|
25
+ stdout, stderr = stderr, stdout if options[:switch_stdout_and_stderr]
26
+
27
+ output = stdout.gets(nil)
28
+ if output.nil? && (error_message = stderr.gets(nil))
29
+ if error_message =~ /:in\s`exec':\s(.+)\s\(.+\)$/
30
+ error_message = $1
31
+ end
32
+ raise ProcessError, "Command: \"#{command}\"\nOutput: \"#{error_message.chomp}\""
33
+ end
34
+ end
35
+ output
36
+ end
37
+ module_function :execute
38
+
39
+ def concat_args(args)
40
+ args.map { |a,v| "-#{a} #{v}" }.join(' ')
41
+ end
42
+
43
+ def queue(command)
44
+ @commands ||= []
45
+ @commands << command
46
+ end
47
+
48
+ def queued_commands
49
+ @commands ? @commands.join(' && ') : ''
50
+ end
51
+
52
+ def execute_queued(options={})
53
+ execute(queued_commands, options)
54
+ @commands = []
55
+ end
56
+
57
+ module ClassMethods
58
+ def executable(executable, options={})
59
+ options[:switch_stdout_and_stderr] ||= false
60
+ options[:use_queue] ||= false
61
+
62
+ executable = executable.to_s if executable.is_a? Symbol
63
+ use_queue = options.delete(:use_queue)
64
+
65
+ if selection_proc = options.delete(:select_if)
66
+ advance_from = nil
67
+ while executable_path = find_executable(executable, advance_from)
68
+ break if selection_proc.call(executable_path)
69
+ advance_from = File.dirname(executable_path)
70
+ end
71
+ else
72
+ executable_path = options[:path] || find_executable(executable)
73
+ end
74
+
75
+ if executable_path
76
+ if use_queue
77
+ body = "queue(\"#{executable_path} \#{args}\")"
78
+ else
79
+ body = "execute(\"#{executable_path} \#{args}\", #{options.inspect}.merge(options))"
80
+ end
81
+ else
82
+ body = "raise Executioner::ExecutableNotFoundError, \"Unable to find the executable '#{executable}' in: #{Executioner::SEARCH_PATHS.join(', ')}\""
83
+ end
84
+
85
+ class_eval "def #{executable.gsub(/-/, '_')}(args, options = {}); #{body}; end", __FILE__, __LINE__
86
+ end
87
+
88
+ def find_executable(executable, advance_from = nil)
89
+ search_paths = Executioner::SEARCH_PATHS
90
+ search_paths = search_paths[(search_paths.index(advance_from) + 1)..-1] if advance_from
91
+
92
+ if executable_in_path = search_paths.find { |path| File.exist? File.join(path, executable) }
93
+ File.join executable_in_path, executable
94
+ end
95
+ end
96
+ module_function :find_executable
97
+ end
98
+ end
@@ -0,0 +1,195 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class AClassThatUsesSubshells
4
+ include Executioner
5
+ executable :sh
6
+ executable :doesnotexistforsure
7
+ executable 'executable-with-dash'
8
+ executable :with_path, :path => '/path/to/executable'
9
+ executable :with_env, :path => '/path/to/executable', :env => { :foo => 'bar' }
10
+
11
+ public :execute
12
+ end
13
+
14
+ describe "Executioner, when executing" do
15
+ before do
16
+ @object = AClassThatUsesSubshells.new
17
+ end
18
+
19
+ it "should open a pipe with the given command" do
20
+ Open3.expects(:popen3).with('/the/command')
21
+ @object.execute('/the/command')
22
+ end
23
+
24
+ it "should return the output received from stdout" do
25
+ stub_popen3 'stdout output', ''
26
+ @object.execute('/the/command').should == 'stdout output'
27
+ end
28
+
29
+ it "should return the output received from stderr if they should be reversed" do
30
+ stub_popen3 '', 'stderr output'
31
+ @object.execute('/the/command', :switch_stdout_and_stderr => true).should == 'stderr output'
32
+ end
33
+
34
+ it "should raise a Executioner::ProcessError if stdout is empty and stderr is not" do
35
+ stub_popen3 '', 'stderr output'
36
+ lambda { @object.execute('foo') }.should.raise Executioner::ProcessError
37
+ end
38
+
39
+ it "should raise a Executioner::ProcessError if stderr is empty and stdout is not and the streams are reversed" do
40
+ stub_popen3 'stdout output', ''
41
+ lambda { @object.execute('foo', :switch_stdout_and_stderr => true) }.should.raise Executioner::ProcessError
42
+ end
43
+
44
+ it "should prepend the given env variables" do
45
+ Open3.expects(:popen3).with("foo='bar' /the/command")
46
+ @object.execute('/the/command', :env => { :foo => :bar })
47
+ end
48
+
49
+ it "should log the command that's going to be executed if a logger is available" do
50
+ begin
51
+ logger = mock('Logger')
52
+ Executioner.logger = logger
53
+
54
+ logger.expects(:debug).with("Executing: `foo'")
55
+ stub_popen3
56
+ @object.execute('foo')
57
+ ensure
58
+ Executioner.logger = nil
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def stub_popen3(stdout = '', stderr = '')
65
+ Open3.stubs(:popen3).yields(*['stdin', stdout, stderr].map { |s| StringIO.new(s) })
66
+ end
67
+ end
68
+
69
+ describe "Executioner, instance methods" do
70
+ before do
71
+ @object = AClassThatUsesSubshells.new
72
+ end
73
+
74
+ it "should raise a Executioner::ProcessError if a command could not be executed" do
75
+ proc = lambda { @object.send(:execute, "/bin/sh -M") }
76
+
77
+ proc.should.raise Executioner::ProcessError
78
+
79
+ begin
80
+ proc.call
81
+ rescue Executioner::ProcessError => error
82
+ error.message.should =~ %r%Command: "/bin/sh -M"%
83
+ error.message.should =~ %r%Output: "/bin/sh: -M: invalid option%
84
+ end
85
+ end
86
+
87
+ it "should be able to switch stdout and stderr, for instance for ffmpeg" do
88
+ lambda { @object.send(:execute, "/bin/sh -M", :switch_stdout_and_stderr => true) }.should.not.raise Executioner::ProcessError
89
+ end
90
+
91
+ it "should help concat arguments" do
92
+ @object.send(:concat_args, [[:foo, 'foo'], [:bar, 'bar']]).should == "-foo foo -bar bar"
93
+ end
94
+
95
+ it "should queue one command" do
96
+ @object.send(:queue, 'ls')
97
+ @object.send(:queued_commands).should == 'ls'
98
+ end
99
+
100
+ it "should queue multiple commands" do
101
+ @object.send(:queue, 'ls')
102
+ @object.send(:queue, 'cat')
103
+ @object.send(:queued_commands).should == 'ls && cat'
104
+ end
105
+
106
+ it "should execute queued commands" do
107
+ @object.send(:queue, 'ls')
108
+ @object.send(:queue, 'ls')
109
+ @object.expects(:execute).with(@object.send(:queued_commands), {})
110
+ @object.send(:execute_queued)
111
+ @object.send(:queued_commands).should == ''
112
+ end
113
+ end
114
+
115
+ describe "Executioner, class methods" do
116
+ before do
117
+ @object = AClassThatUsesSubshells.new
118
+ end
119
+
120
+ it "should define an instance method for the specified binary that's needed" do
121
+ AClassThatUsesSubshells.instance_methods.should.include 'sh'
122
+ end
123
+
124
+ it "should define an instance method which calls #execute with the correct path to the executable" do
125
+ @object.expects(:execute).with('/bin/sh with some args', { :switch_stdout_and_stderr => false })
126
+ @object.sh 'with some args'
127
+ end
128
+
129
+ it "should define an instance method for an executable with dashes replaced by underscores" do
130
+ @object.should.respond_to :executable_with_dash
131
+ end
132
+
133
+ it "should be possible to switch stdin and stderr" do
134
+ AClassThatUsesSubshells.class_eval { executable(:sh, { :switch_stdout_and_stderr => true }) }
135
+ @object.expects(:execute).with('/bin/sh with some args', { :switch_stdout_and_stderr => true })
136
+ @object.sh 'with some args'
137
+ end
138
+
139
+ it "should be possible to use the queue by default" do
140
+ AClassThatUsesSubshells.class_eval { executable(:sh, { :use_queue => true }) }
141
+ @object.expects(:execute).with('/bin/sh arg1 && /bin/sh arg2', {})
142
+ @object.sh 'arg1'
143
+ @object.sh 'arg2'
144
+ @object.execute_queued
145
+ end
146
+
147
+ it "should be possible to specify the path to the executable" do
148
+ @object.expects(:execute).with { |command, options| command == "/path/to/executable arg1" }
149
+ @object.with_path 'arg1'
150
+ end
151
+
152
+ it "should be possible to specify the env that's to be prepended to the command" do
153
+ @object.expects(:execute).with { |command, options| options[:env] == { :foo => 'bar' } }
154
+ @object.with_env 'arg1'
155
+ end
156
+
157
+ it "should merge options onto the default options" do
158
+ @object.expects(:execute).with { |command, options| options[:env] == { :foo => 'foo' } }
159
+ @object.with_env 'arg1', :env => { :foo => 'foo' }
160
+ end
161
+
162
+ it "should be possible to find an executable" do
163
+ File.stubs(:exist?).with('/bin/sh').returns(true)
164
+ Executioner::ClassMethods.find_executable('sh').should == '/bin/sh'
165
+ end
166
+
167
+ it "should be possible to find an executable advancing from a given path" do
168
+ File.stubs(:exist?).with('/usr/bin/sh').returns(true)
169
+ Executioner::ClassMethods.find_executable('sh', '/bin').should == '/usr/bin/sh'
170
+ end
171
+
172
+ it "should yield all found executables, but use the one for which the proc returns a truthful value" do
173
+ File.stubs(:exist?).with('/bin/with_selection_proc').returns(true)
174
+ File.stubs(:exist?).with('/usr/bin/with_selection_proc').returns(true)
175
+ File.stubs(:exist?).with('/usr/local/bin/with_selection_proc').returns(true)
176
+ File.stubs(:exist?).with('/opt/local/bin/with_selection_proc').returns(true)
177
+
178
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| nil })
179
+ lambda { @object.with_selection_proc('foo') }.should.raise Executioner::ExecutableNotFoundError
180
+
181
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| executable == '/usr/bin/with_selection_proc' })
182
+ @object.expects(:execute).with("/usr/bin/with_selection_proc foo", {:switch_stdout_and_stderr => false})
183
+ @object.with_selection_proc('foo')
184
+
185
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| executable == '/opt/local/bin/with_selection_proc' })
186
+ @object.expects(:execute).with("/opt/local/bin/with_selection_proc foo", {:switch_stdout_and_stderr => false})
187
+ @object.with_selection_proc('foo')
188
+ end
189
+
190
+ it "should define an instance method which raises a Executioner::ExecutableNotFoundError error if the executable could not be found" do
191
+ lambda {
192
+ @object.doesnotexistforsure 'right?'
193
+ }.should.raise Executioner::ExecutableNotFoundError
194
+ end
195
+ end
@@ -0,0 +1,6 @@
1
+ require "rubygems"
2
+ require "test/spec"
3
+ require "mocha"
4
+
5
+ $:.unshift File.expand_path('../../lib', __FILE__)
6
+ require "executioner"
metadata ADDED
@@ -0,0 +1,63 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: executioner
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Eloy Duran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-07-08 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Execute CLI utilities
17
+ email: eloy@fngtps.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - LICENSE
27
+ - README.rdoc
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/executioner.rb
31
+ - test/executioner_test.rb
32
+ - test/test_helper.rb
33
+ has_rdoc: true
34
+ homepage: http://fingertips.github.com
35
+ licenses: []
36
+
37
+ post_install_message:
38
+ rdoc_options:
39
+ - --charset=UTF-8
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.3.5
58
+ signing_key:
59
+ specification_version: 3
60
+ summary: Execute CLI utilities
61
+ test_files:
62
+ - test/executioner_test.rb
63
+ - test/test_helper.rb