methadone 0.5.1 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ html
7
7
  tmp
8
8
  results.html
9
9
  coverage
10
+ *.rbc
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
+ gem 'open4', :platform => :ruby_18
3
4
  # Specify your gem's dependencies in methadone.gemspec
4
5
  gemspec
data/README.rdoc CHANGED
@@ -12,6 +12,7 @@ Currently, this library is under development and has the following to offer:
12
12
 
13
13
  * Bootstrapping a new CLI app
14
14
  * Lightweight DSL to structure your bin file
15
+ * Simple wrapper for running external commands with good logging
15
16
  * Utility Classes
16
17
  * Methadone::CLILogger - a logger subclass that sends messages to standard error standard out as appropriate
17
18
  * Methadone::CLILogging - a module that, when included in any class, provides easy access to a shared logger
@@ -22,7 +23,18 @@ Currently, this library is under development and has the following to offer:
22
23
  * {Source on Github}[http://github.com/davetron5000/methadone]
23
24
  * RDoc[http://rdoc.info/github/davetron5000/methadone/master/frames]
24
25
 
25
- == Bootstrapping
26
+ == Platforms
27
+
28
+ * Works completely on:
29
+ * MRI Ruby 1.9.2
30
+ * MRI Ruby 1.9.3
31
+ * MRI Ruby Head
32
+ * MRI Ruby 1.8.7
33
+ * RBX
34
+ * REE
35
+ * For JRuby, everything works but aruba; aruba just doesn't work on JRuby for some reason, though this library *is* being used in production under JRuby 1.6.6
36
+
37
+ == Bootstrapping a new CLI App
26
38
 
27
39
  The +methadone+ command-line app will bootstrap a new command-line app, setting up a proper gem structure, unit tests, and cucumber-based tests with aruba:
28
40
 
@@ -112,6 +124,52 @@ don't accept options, <tt>[options]</tt> won't appear in the help. The names of
112
124
  will appear in proper order and <tt>:optional</tt> ones will be in square brackets. You don't have to
113
125
  touch a thing.
114
126
 
127
+ == Wrapper for running external commands with good logging
128
+
129
+ While backtick and <tt>%x[]</tt> are nice for compact, bash-like scripting, they have some failings:
130
+
131
+ * You have to check the return value via <tt>$?</tt>
132
+ * You have no access to the standard error
133
+ * You really want to log: the command, the output, and the error so that for cron-like tasks, you can sort out what happened
134
+
135
+ Enter Methadone::SH
136
+
137
+ include Methadone::SH
138
+
139
+ sh 'cp foo.txt /tmp'
140
+ # => logs the command to DEBUG, executes the command, logs its output to DEBUG and its
141
+ # error output to WARN, returns 0
142
+
143
+ sh 'cp non_existent_file.txt /nowhere_good'
144
+ # => logs the command to DEBUG, executes thecommand, logs its output to INFO and
145
+ # its error output to WARN, returns the nonzero exit status of the underlying command
146
+
147
+ sh! 'cp non_existent_file.txt /nowhere_good'
148
+ # => same as above, EXCEPT, raises a Methadone::FailedCommandError
149
+
150
+ With this, you can easily script external commands in *almost* as expedient a fashion as with +bash+, however you get sensible logging along the way. By default, this uses the logger provided by Methadone::CLILogging (which is *not* mixed in when you mix in Methadone::SH). If you want to use a different logger, or don't want to mix in Methadone::CLILogging, simply call +set_sh_logger+ with your preferred logger.
151
+
152
+ But that's not all! You can run code when the command succeed by passing a block:
153
+
154
+ sh 'cp foo.txt /tmp' do
155
+ # Behaves exactly as before, but this block is called after
156
+ end
157
+
158
+ sh 'cp non_existent_file.txt /nowhere_good' do
159
+ # This block isn't called, since the command failed
160
+ end
161
+
162
+ The <tt>sh!</tt> form works this way as well. The block form is also how you can access the standard output or error of the command that ran. Simply have your block accept one or two aguments:
163
+
164
+ sh 'ls -l /tmp/' do |stdout|
165
+ # stdout contains the output of the command
166
+ end
167
+ sh 'ls -l /tmp/ /non_existent_dir' do |stdout,stderr|
168
+ # stdout contains the output of the command,
169
+ # stderr contains the standard error output.
170
+ end
171
+
172
+ This isn't a replacement for Open3 or ChildProcess, but a way to easily "do the right thing" for most cases.
115
173
 
116
174
  == Utility Classes
117
175
 
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ Rake::TestTask.new do |t|
15
15
  t.libs << "lib"
16
16
  t.libs << "test"
17
17
  t.ruby_opts << "-rrubygems"
18
- t.test_files = FileList['test/test_*.rb']
18
+ t.test_files = FileList['test/test_*.rb'] + FileList['test/execution_strategy/test_*.rb']
19
19
  end
20
20
 
21
21
  desc 'build rdoc'
@@ -43,5 +43,5 @@ Cucumber::Rake::Task.new('features:wip') do |t|
43
43
  end
44
44
 
45
45
  CLEAN << "coverage"
46
-
46
+ CLOBBER << FileList['**/*.rbc']
47
47
  task :default => [:test, :features]
data/bin/methadone CHANGED
@@ -8,6 +8,7 @@ require 'methadone/cli'
8
8
  include FileUtils
9
9
  include Methadone::Main
10
10
  include Methadone::CLI
11
+ include Methadone::SH
11
12
 
12
13
  main do |app_name|
13
14
  check_and_prepare_basedir!(app_name,options[:force])
@@ -20,7 +21,7 @@ main do |app_name|
20
21
 
21
22
  chdir File.dirname(app_name)
22
23
 
23
- %x[bundle gem #{gemname}]
24
+ sh! "bundle gem #{gemname}"
24
25
 
25
26
  chdir gemname
26
27
 
@@ -50,11 +51,12 @@ main do |app_name|
50
51
  copy_file "features/step_definitions/executable_steps.rb", :as => "#{gemname}_steps.rb"
51
52
  copy_file "bin/executable", :as => gemname, :executable => true, :binding => binding
52
53
 
54
+ gem_variable = File.open("#{gemname}.gemspec") { |x| x.read }.match(/(\w+)\.executables/)[1]
53
55
  add_to_file "#{gemname}.gemspec", [
54
- " s.add_development_dependency('rdoc')",
55
- " s.add_development_dependency('aruba')",
56
- " s.add_development_dependency('rake','~> 0.9.2')",
57
- " s.add_dependency('methadone')",
56
+ " #{gem_variable}.add_development_dependency('rdoc')",
57
+ " #{gem_variable}.add_development_dependency('aruba')",
58
+ " #{gem_variable}.add_development_dependency('rake','~> 0.9.2')",
59
+ " #{gem_variable}.add_dependency('methadone')",
58
60
  ], :before => /^end\s*$/
59
61
  end
60
62
 
@@ -35,6 +35,7 @@ Feature: Bootstrap a new command-line app
35
35
  And the file "tmp/newgem/newgem.gemspec" should match /add_development_dependency\('rdoc'/
36
36
  And the file "tmp/newgem/newgem.gemspec" should match /add_development_dependency\('rake','~> 0.9.2'/
37
37
  And the file "tmp/newgem/newgem.gemspec" should match /add_dependency\('methadone'/
38
+ And the file "tmp/newgem/newgem.gemspec" should use the same block variable throughout
38
39
  Given I cd to "tmp/newgem"
39
40
  And my app's name is "newgem"
40
41
  When I successfully run `bin/newgem --help` with "lib" in the library path
@@ -10,6 +10,15 @@ Given /^my app's name is "([^"]*)"$/ do |app_name|
10
10
  @app_name = app_name
11
11
  end
12
12
 
13
+ Then /^the file "([^"]*)" should use the same block variable throughout$/ do |file|
14
+ prep_for_fs_check do
15
+ content = IO.read(file)
16
+ from_bundler = content.match(/(\w+)\.authors/)[1]
17
+ added_by_methadone = content.match(/(\w+).add_development_dependency\('rdoc'/)[1]
18
+ from_bundler.should == added_by_methadone
19
+ end
20
+ end
21
+
13
22
  Then /^the stderr should match \/([^\/]*)\/$/ do |expected|
14
23
  assert_matching_output(expected, all_stderr)
15
24
  end
@@ -1,4 +1,4 @@
1
1
  When /^I successfully run `([^`]*)` with "([^"]*)" in the library path$/ do |command,dir|
2
- ENV["RUBYOPT"] = "-I" + File.join(Dir.pwd,ARUBA_DIR,'tmp','newgem',dir)
2
+ ENV["RUBYOPT"] = (ENV["RUBYOPT"] || '') + " -I" + File.join(Dir.pwd,ARUBA_DIR,'tmp','newgem',dir)
3
3
  step %(I successfully run `#{command}`)
4
4
  end
@@ -9,13 +9,18 @@ Before do
9
9
  @puts = true
10
10
  @aruba_timeout_seconds = 60
11
11
  @original_rubylib = ENV['RUBYLIB']
12
+ @original_rubyopt = ENV['RUBYOPT']
12
13
 
13
14
  # We want to use, hopefully, the methadone from this codebase and not
14
15
  # the gem, so we put it in the RUBYLIB
15
16
  ENV['RUBYLIB'] = File.join(PROJECT_ROOT,'lib') + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
17
+
18
+ # We need -rubygems here so that 1.8-style rubies work AND travis-ci doesn't barf with it in the shebang line
19
+ ENV['RUBYOPT'] = (ENV['RUBYOPT'] || '') + ' -rubygems'
16
20
  end
17
21
 
18
22
  After do
19
23
  # Put back how it was
20
24
  ENV['RUBYLIB'] = @original_rubylib
25
+ ENV['RUBYOPT'] = @original_rubyopt
21
26
  end
@@ -1,4 +1,3 @@
1
- @wip
2
1
  Feature: The version should show up in the banner by default
3
2
  As a developer
4
3
  I should be able to have the current gem version in the banner
data/lib/methadone.rb CHANGED
@@ -3,4 +3,11 @@ require 'methadone/cli_logger'
3
3
  require 'methadone/cli_logging'
4
4
  require 'methadone/main'
5
5
  require 'methadone/error'
6
+ require 'methadone/execution_strategy/base'
7
+ require 'methadone/execution_strategy/mri'
8
+ require 'methadone/execution_strategy/open_3'
9
+ require 'methadone/execution_strategy/open_4'
10
+ require 'methadone/execution_strategy/rbx_open_4'
11
+ require 'methadone/execution_strategy/jvm'
12
+ require 'methadone/sh'
6
13
  # Note: DO NOT require cli.rb OR cucumber.rb here
@@ -9,4 +9,16 @@ module Methadone
9
9
  @exit_code = exit_code
10
10
  end
11
11
  end
12
+
13
+ # Thrown by certain methods when an externally-called command exits nonzero
14
+ class FailedCommandError < Error
15
+
16
+ # The command that caused the failure
17
+ attr_reader :command
18
+
19
+ def initialize(exit_code,command)
20
+ super(exit_code,"Command '#{command}' exited #{exit_code}")
21
+ @command = command
22
+ end
23
+ end
12
24
  end
@@ -0,0 +1,37 @@
1
+ module Methadone
2
+ # Module to contain ExecutionStrategy implementations.
3
+ # To build your own simply implement two methods:
4
+ #
5
+ # <tt>exception_meaning_command_not_found</tt>:: return the class that, if caught, means that the underlying command
6
+ # couldn't be found. This is needed because currently impelmentations
7
+ # throw an exception, but they don't all throw the same one.
8
+ module ExecutionStrategy
9
+ # Base for any ExecutionStrategy implementation. Currently, this is nothing more than an interface
10
+ # specification.
11
+ class Base
12
+ # Executes the command and returns the results back.
13
+ # This should do no logging or other logic other than to execute the command
14
+ # and return the required results.
15
+ #
16
+ # command:: the command-line to run, as a String
17
+ #
18
+ # Returns an array of size 3:
19
+ # <tt>[0]</tt>:: The standard output of the command as a String, never nil
20
+ # <tt>[1]</tt>:: The standard error output of the command as a String, never nil
21
+ # <tt>[2]</tt>:: A Process::Status-like objects that responds to <tt>exitstatus</tt> which returns
22
+ # the exit code of the command (e.g. 0 for success).
23
+ def run_command(command)
24
+ subclass_must_impelment!
25
+ end
26
+
27
+ # Returns the class that, if caught by calling #run_command, represents the underlying command
28
+ # not existing. For example, in MRI Ruby, if you try to execute a non-existent command,
29
+ # you get a Errno::ENOENT.
30
+ def exception_meaning_command_not_found
31
+ subclass_must_impelment!
32
+ end
33
+ protected
34
+ def subclass_must_impelment!; raise "subclass must implement"; end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,30 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # Methadone::ExecutionStrategy for the JVM that uses JVM classes to run the command and get its results.
4
+ class JVM < Base
5
+ def run_command(command)
6
+ process = java.lang.Runtime.get_runtime.exec(command)
7
+ process.get_output_stream.close
8
+ stdout = input_stream_to_string(process.get_input_stream)
9
+ stderr = input_stream_to_string(process.get_error_stream)
10
+ exitstatus = process.wait_for
11
+ [stdout.chomp,stderr.chomp,OpenStruct.new(:exitstatus => exitstatus)]
12
+ end
13
+
14
+ def exception_meaning_command_not_found
15
+ NativeException
16
+ end
17
+
18
+ private
19
+ def input_stream_to_string(is)
20
+ ''.tap do |string|
21
+ ch = is.read
22
+ while ch != -1
23
+ string << ch
24
+ ch = is.read
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,14 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # Base strategy for MRI rubies.
4
+ class MRI < Base
5
+ def run_command(command)
6
+ raise "subclass must implement"
7
+ end
8
+
9
+ def exception_meaning_command_not_found
10
+ Errno::ENOENT
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # Implementation for modern Rubies that uses the built-in Open3 library
4
+ class Open_3 < MRI
5
+ def run_command(command)
6
+ stdout,stderr,status = Open3.capture3(command)
7
+ [stdout.chomp,stderr.chomp,status]
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # ExecutionStrategy for non-modern Rubies that must rely on
4
+ # Open4 to get access to the standard output AND error.
5
+ class Open_4 < MRI
6
+ def run_command(command)
7
+ pid, stdin_io, stdout_io, stderr_io = Open4::popen4(command)
8
+ stdin_io.close
9
+ stdout = stdout_io.read
10
+ stderr = stderr_io.read
11
+ _ , status = Process::waitpid2(pid)
12
+ [stdout.chomp,stderr.chomp,status]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Methadone
2
+ module ExecutionStrategy
3
+ # For RBX; it throws a different exception when a command isn't found, so we override that here.
4
+ class RBXOpen_4 < Open_4
5
+ def exception_meaning_command_not_found
6
+ Errno::EINVAL
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,143 @@
1
+ if RUBY_PLATFORM == 'java'
2
+ require 'java'
3
+ require 'ostruct'
4
+ elsif RUBY_VERSION =~ /^1.8/
5
+ require 'open4'
6
+ else
7
+ require 'open3'
8
+ end
9
+
10
+ module Methadone
11
+ # Module with various helper methods for executing external commands.
12
+ # In most cases, you can use #sh to run commands and have decent logging
13
+ # done. You will likely use this in a class that also mixes-in
14
+ # Methadone::CLILogging. If you *don't*, you must provide a logger
15
+ # via #set_sh_logger.
16
+ #
17
+ # In order to work on as many Rubies as possible, this class defers the actual execution
18
+ # to an execution strategy. See #set_execution_strategy if you think you'd like to override
19
+ # that, or just want to know how it works.
20
+ #
21
+ # This is not intended to be a complete replacement for Open3, but instead of make common cases
22
+ # and good practice easy to accomplish.
23
+ module SH
24
+ # Run a shell command, capturing and logging its output.
25
+ # If the command completed successfully, it's output is logged at DEBUG.
26
+ # If not, its output as logged at INFO. In either case, its
27
+ # error output is logged at WARN.
28
+ #
29
+ # command:: the command to run
30
+ # block:: if provided, will be called if the command exited nonzero. The block may take 0, 1, or 2 arguments.
31
+ # The arguments provided are the standard output as a string and the standard error as a string,
32
+ # You should be safe to pass in a lambda instead of a block, as long as your
33
+ # lambda doesn't take more than two arguments
34
+ #
35
+ # Example
36
+ #
37
+ # sh "cp foo /tmp"
38
+ # sh "ls /tmp" do |stdout|
39
+ # # stdout contains the output of ls /tmp
40
+ # end
41
+ # sh "ls -l /tmp foobar" do |stdout,stderr|
42
+ # # ...
43
+ # end
44
+ #
45
+ # Returns the exit status of the command. Note that if the command doesn't exist, this returns 127.
46
+ def sh(command,&block)
47
+ sh_logger.debug("Executing '#{command}'")
48
+
49
+ stdout,stderr,status = execution_strategy.run_command(command)
50
+
51
+ sh_logger.warn("Error output of '#{command}': #{stderr}") unless stderr.strip.length == 0
52
+
53
+ if status.exitstatus != 0
54
+ sh_logger.info("Output of '#{command}': #{stdout}")
55
+ sh_logger.warn("Error running '#{command}'")
56
+ else
57
+ sh_logger.debug("Output of '#{command}': #{stdout}")
58
+ call_block(block,stdout,stderr) unless block.nil?
59
+ end
60
+
61
+ status.exitstatus
62
+ rescue exception_meaning_command_not_found => ex
63
+ sh_logger.error("Error running '#{command}': #{ex.message}")
64
+ 127
65
+ end
66
+
67
+ # Run a command, throwing an exception if the command exited nonzero.
68
+ # Otherwise, behaves exactly like #sh.
69
+ #
70
+ # Raises Methadone::FailedCommandError if the command exited nonzero.
71
+ def sh!(command,&block)
72
+ sh(command,&block).tap do |exitstatus|
73
+ raise Methadone::FailedCommandError.new(exitstatus,command) if exitstatus != 0
74
+ end
75
+ end
76
+
77
+ # Override the default logger (which is the one provided by CLILogging).
78
+ # You would do this if you want a custom logger or you aren't mixing-in
79
+ # CLILogging.
80
+ #
81
+ # Note that this method is *not* called <tt>sh_logger=</tt> to avoid annoying situations
82
+ # where Ruby thinks you are setting a local variable
83
+ def set_sh_logger(logger)
84
+ @sh_logger = logger
85
+ end
86
+
87
+ # Set the strategy to use for executing commands. In general, you don't need to set this
88
+ # since this module chooses an appropriate implementation based on your Ruby platform:
89
+ #
90
+ # 1.8 Rubies, including 1.8, and REE:: Open4 is used via Methadone::ExecutionStrategy::Open_4
91
+ # Rubinius:: Open4 is used, but we handle things a bit differently; see Methadone::ExecutionStrategy::RBXOpen_4
92
+ # JRuby:: Use JVM calls to +Runtime+ via Methadone::ExecutionStrategy::JVM
93
+ # Windows:: Currently no support for Windows
94
+ # All others:: we use Open3 from the standard library, via Methadone::ExecutionStrategy::Open_3
95
+ #
96
+ # See Methadone::ExecutionStrategy::Base for how to implement your own.
97
+ def set_execution_strategy(strategy)
98
+ @execution_strategy = strategy
99
+ end
100
+
101
+ private
102
+
103
+ def exception_meaning_command_not_found
104
+ execution_strategy.exception_meaning_command_not_found
105
+ end
106
+
107
+ def self.default_execution_strategy_class
108
+ if RUBY_PLATFORM == 'java'
109
+ Methadone::ExecutionStrategy::JVM
110
+ elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
111
+ Methadone::ExecutionStrategy::RBXOpen_4
112
+ elsif RUBY_VERSION =~ /^1.8/
113
+ Methadone::ExecutionStrategy::Open_4
114
+ else
115
+ Methadone::ExecutionStrategy::Open_3
116
+ end
117
+ end
118
+
119
+ def execution_strategy
120
+ @execution_strategy ||= SH.default_execution_strategy_class.new
121
+ end
122
+
123
+ def sh_logger
124
+ @sh_logger ||= self.logger
125
+ end
126
+
127
+ # Safely call our block, even if the user passed in a lambda
128
+ def call_block(block,stdout,stderr)
129
+ # blocks that take no arguments have arity -1. Or 0. Ugh.
130
+ if block.arity > 0
131
+ case block.arity
132
+ when 1
133
+ block.call(stdout)
134
+ else
135
+ # Let it fail for lambdas
136
+ block.call(stdout,stderr)
137
+ end
138
+ else
139
+ block.call
140
+ end
141
+ end
142
+ end
143
+ end