methadone 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.rdoc +136 -0
- data/Rakefile +35 -0
- data/bin/methadone +136 -0
- data/features/bootstrap.feature +89 -0
- data/features/step_definitions/bootstrap_steps.rb +7 -0
- data/features/support/env.rb +12 -0
- data/lib/methadone.rb +3 -0
- data/lib/methadone/cli_logger.rb +65 -0
- data/lib/methadone/cli_logging.rb +52 -0
- data/lib/methadone/cucumber.rb +30 -0
- data/lib/methadone/version.rb +3 -0
- data/methadone.gemspec +25 -0
- data/templates/full/Rakefile.erb +28 -0
- data/templates/full/bin/executable.erb +11 -0
- data/templates/full/features/executable.feature.erb +12 -0
- data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -0
- data/templates/full/features/support/env.rb.erb +8 -0
- data/templates/full/test/tc_something.rb.erb +7 -0
- data/test/base_test.rb +18 -0
- data/test/test_cli_logger.rb +117 -0
- data/test/test_cli_logging.rb +97 -0
- metadata +123 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.2@methadone-dev --create
|
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
= Methadone - kick the bash habit and start your command line apps off right
|
2
|
+
|
3
|
+
Author:: Dave Copeland (mailto:davetron5000 at g mail dot com)
|
4
|
+
Copyright:: Copyright (c) 2011 by Dave Copeland
|
5
|
+
License:: Distributes under the Apache License, see LICENSE.txt in the source distro
|
6
|
+
|
7
|
+
A smattering of tools to make your command-line apps easily awesome; kick the bash habit without sacrificing any of the power.
|
8
|
+
|
9
|
+
Currently, this is under development and has the following to offer:
|
10
|
+
|
11
|
+
* Bootstrapping a new CLI app
|
12
|
+
* Utility Classes
|
13
|
+
* Methadone::CLILogger - a logger subclass that sends error message to standard error and all messages to standard out
|
14
|
+
* Methadone::CLILogging - a module that, when included in any class, provides easy access to a shared logger
|
15
|
+
* Cucumber Steps
|
16
|
+
|
17
|
+
== Links
|
18
|
+
|
19
|
+
* {Source on Github}[http://github.com/davetron5000/methadone]
|
20
|
+
* RDoc[http://rdoc.info/github/davetron5000/methadone/master/frames]
|
21
|
+
|
22
|
+
== Bootstrapping
|
23
|
+
|
24
|
+
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:
|
25
|
+
|
26
|
+
$ methadone --help
|
27
|
+
Usage: methadone [options] app_name
|
28
|
+
--force Overwrite files if they exist
|
29
|
+
$ methadone newgem
|
30
|
+
$ cd newgem
|
31
|
+
$ rake
|
32
|
+
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
33
|
+
1 scenario (1 passed)
|
34
|
+
3 steps (3 passed)
|
35
|
+
$ cat features/newgem.feature
|
36
|
+
Feature: My bootstrapped app kinda works
|
37
|
+
In order to get going on coding my awesome app
|
38
|
+
I want to have aruba and cucumber setup
|
39
|
+
So I don't have to do it myself
|
40
|
+
|
41
|
+
Scenario: App just runs
|
42
|
+
When I run `newgem --help`
|
43
|
+
Then the exit status should be 0
|
44
|
+
And the output should contain:
|
45
|
+
"""
|
46
|
+
Usage: newgem [options]
|
47
|
+
"""
|
48
|
+
|
49
|
+
Basically, this sets you up with all the boilerplate that you *should* be using to write a command-line app.
|
50
|
+
|
51
|
+
== Utility Classes
|
52
|
+
|
53
|
+
Currently, there are classes the assist in directing output logger-style to the right place; basically ensuring that errors go to +STDERR+ and everything else goes to +STDOUT+. All of this is, of course, configurable
|
54
|
+
|
55
|
+
=== Examples
|
56
|
+
|
57
|
+
==== Using STDOUT as a log, respecting STDERR
|
58
|
+
|
59
|
+
require 'methadone'
|
60
|
+
|
61
|
+
include Methadone::CLILogging
|
62
|
+
|
63
|
+
command = "rm -rf /tmp/*"
|
64
|
+
debug("About to run #{command}") # => goes only to STDOUT
|
65
|
+
if system(command)
|
66
|
+
info("Succesfully ran #{command}") # => goes only to STDOUT
|
67
|
+
else
|
68
|
+
error("There was a problem running #{command}") # => goes to STDOUT AND STDERR
|
69
|
+
end
|
70
|
+
|
71
|
+
==== Using a log file, but respecting STDERR
|
72
|
+
|
73
|
+
require 'methadone'
|
74
|
+
|
75
|
+
include Methadone::CLILogging
|
76
|
+
|
77
|
+
self.logger = CLILogger.new("logfile.txt")
|
78
|
+
command = "rm -rf /tmp/*"
|
79
|
+
debug("About to run #{command}") # => goes only to logfile.txt
|
80
|
+
if system(command)
|
81
|
+
info("Succesfully ran #{command}") # => goes only to logfile.txt
|
82
|
+
else
|
83
|
+
error("There was a problem running #{command}") # => goes to logfile.txt AND STDERR
|
84
|
+
end
|
85
|
+
|
86
|
+
== Cucumber Steps
|
87
|
+
|
88
|
+
Methadone uses aruba[http://www.github.com/cucumber/aruba] for BDD-style testing with cucumber. This library has some awesome steps, and methadone provides additional, more opinionated, steps.
|
89
|
+
|
90
|
+
=== Example
|
91
|
+
|
92
|
+
Here's an example from methadone's own tests:
|
93
|
+
|
94
|
+
Scenario: Help is properly documented
|
95
|
+
When I get help for "methadone"
|
96
|
+
Then the exit status should be 0
|
97
|
+
And the following options should be documented:
|
98
|
+
|--force|
|
99
|
+
And the banner should be present
|
100
|
+
And the banner should document that this app takes options
|
101
|
+
And the banner should document that this app's arguments are:
|
102
|
+
|app_name|required|
|
103
|
+
|
104
|
+
=== Steps Provided
|
105
|
+
* Run <tt>command_to_run --help</tt> using aruba
|
106
|
+
|
107
|
+
When I get help for "command_to_run"
|
108
|
+
|
109
|
+
* Make sure that each option shows up in the help and has *some* sort of documentation
|
110
|
+
|
111
|
+
Then the following options should be documented:
|
112
|
+
|--force|
|
113
|
+
|-x |
|
114
|
+
|
115
|
+
* Check an individual option for documentation:
|
116
|
+
|
117
|
+
Then the option "--force" should be documented
|
118
|
+
|
119
|
+
* Checks that the help has a proper usage banner
|
120
|
+
|
121
|
+
Then the banner should be present
|
122
|
+
|
123
|
+
* Checks that the usage banner indicates it takes options via <tt>[options]</tt>
|
124
|
+
|
125
|
+
Then the banner should document that this app takes options
|
126
|
+
|
127
|
+
* Checks that the app's usage banner documents that its arguments are <tt>args</tt>
|
128
|
+
|
129
|
+
Then the banner should document that this app's arguments are "args"
|
130
|
+
|
131
|
+
|
132
|
+
== What might be
|
133
|
+
|
134
|
+
* Support for running external commands easily, with full error checking
|
135
|
+
* Support for main method-style implementation
|
136
|
+
* Easy support for filtering the output of a command, e.g. <tt>File.open("ls|")</tt> Perl-style
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
gem 'rdoc'
|
5
|
+
require 'rdoc/task'
|
6
|
+
require 'cucumber'
|
7
|
+
require 'cucumber/rake/task'
|
8
|
+
|
9
|
+
include Rake::DSL
|
10
|
+
|
11
|
+
Bundler::GemHelper.install_tasks
|
12
|
+
|
13
|
+
desc 'run tests'
|
14
|
+
Rake::TestTask.new do |t|
|
15
|
+
t.libs << "lib"
|
16
|
+
t.libs << "test"
|
17
|
+
t.test_files = FileList['test/test_*.rb']
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'build rdoc'
|
21
|
+
Rake::RDocTask.new do |rd|
|
22
|
+
rd.main = "README.rdoc"
|
23
|
+
rd.generator = 'hanna'
|
24
|
+
rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
25
|
+
rd.title = 'Methadone - Power Up your Command Line Apps'
|
26
|
+
end
|
27
|
+
|
28
|
+
CUKE_RESULTS = 'results.html'
|
29
|
+
CLEAN << CUKE_RESULTS
|
30
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
31
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x"
|
32
|
+
t.fork = false
|
33
|
+
end
|
34
|
+
|
35
|
+
task :default => [:test, :features]
|
data/bin/methadone
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'erb'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
include FileUtils
|
8
|
+
|
9
|
+
def main(basedir,force)
|
10
|
+
if Dir.exists? basedir
|
11
|
+
if force
|
12
|
+
rm_rf basedir, :verbose => true, :secure => true
|
13
|
+
else
|
14
|
+
STDERR.puts "error: #{basedir} exists, use --force to override"
|
15
|
+
return 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
mkdir_p basedir
|
20
|
+
|
21
|
+
gemname = File.basename(basedir)
|
22
|
+
chdir File.dirname(basedir)
|
23
|
+
|
24
|
+
%x[bundle gem #{gemname}]
|
25
|
+
|
26
|
+
chdir gemname
|
27
|
+
|
28
|
+
template_dirs_in(:full).each { |dir| mkdir_p dir }
|
29
|
+
|
30
|
+
copy_file "Rakefile"
|
31
|
+
copy_file "test/tc_something.rb"
|
32
|
+
copy_file "features/executable.feature", :as => "#{gemname}.feature", :binding => binding
|
33
|
+
copy_file "features/support/env.rb"
|
34
|
+
copy_file "features/step_definitions/executable_steps.rb", :as => "#{gemname}_steps.rb"
|
35
|
+
copy_file "bin/executable", :as => gemname, :executable => true
|
36
|
+
add_to_file "#{gemname}.gemspec", [
|
37
|
+
" s.add_development_dependency('rdoc')",
|
38
|
+
" s.add_development_dependency('grancher')",
|
39
|
+
" s.add_development_dependency('aruba')",
|
40
|
+
], :before => /^end\s*$/
|
41
|
+
|
42
|
+
return 0
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add content to a file
|
46
|
+
#
|
47
|
+
# +file+:: path to the file
|
48
|
+
# +lines+:: Array of String representing the lines to add
|
49
|
+
# +options+:: Hash of options:
|
50
|
+
# <tt>:before</tt>:: A regexp that will appear right after the new content. i.e.
|
51
|
+
# this is where to insert said content.
|
52
|
+
def add_to_file(file,lines,options = {})
|
53
|
+
new_lines = []
|
54
|
+
found_line = false
|
55
|
+
File.open(file).readlines.each do |line|
|
56
|
+
line.chomp!
|
57
|
+
if options[:before] && options[:before] === line
|
58
|
+
found_line = true
|
59
|
+
new_lines += lines
|
60
|
+
end
|
61
|
+
new_lines << line
|
62
|
+
end
|
63
|
+
|
64
|
+
raise "No line matched #{options[:before]}" if options[:before] && !found_line
|
65
|
+
|
66
|
+
new_lines += lines unless options[:before]
|
67
|
+
File.open(file,'w') do |fp|
|
68
|
+
new_lines.each { |line| fp.puts line }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Copies a file, running it through ERB
|
73
|
+
#
|
74
|
+
# +relative_path+:: path to the file, relative to the project root, minus the .erb extension
|
75
|
+
# You should use forward slashes to separate paths; this method
|
76
|
+
# will handle making the ultimate path OS independent.
|
77
|
+
# +options+:: Options to affect how the copy is done:
|
78
|
+
# <tt>:from</tt>:: The name of the profile from which to find the file, "full" by default
|
79
|
+
# <tt>:as</tt>:: The name the file should get if not the one in relative_path
|
80
|
+
# <tt>:executable</tt>:: true if this file should be set executable
|
81
|
+
# <tt>:binding</tt>:: the binding to use for the template
|
82
|
+
def copy_file(relative_path,options = {})
|
83
|
+
options[:from] ||= :full
|
84
|
+
|
85
|
+
relative_path = File.join(relative_path.split(/\//))
|
86
|
+
|
87
|
+
template_path = File.join(template_dir(options[:from]),relative_path + ".erb")
|
88
|
+
template = ERB.new(File.open(template_path).readlines.join(''))
|
89
|
+
|
90
|
+
relative_path_parts = File.split(relative_path)
|
91
|
+
relative_path_parts[-1] = options[:as] if options[:as]
|
92
|
+
|
93
|
+
erb_binding = options[:binding] or binding
|
94
|
+
|
95
|
+
File.open(File.join(relative_path_parts),'w') do |file|
|
96
|
+
file.puts template.result(erb_binding)
|
97
|
+
file.chmod(0755) if options[:executable]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Get the location of the templates for profile "from"
|
102
|
+
def template_dir(from)
|
103
|
+
File.join(File.dirname(EXE),'..','templates',from.to_s)
|
104
|
+
end
|
105
|
+
|
106
|
+
def template_dirs_in(profile)
|
107
|
+
template_dir = template_dir(:full)
|
108
|
+
|
109
|
+
Dir["#{template_dir}/**/*"].select { |x|
|
110
|
+
File.directory? x
|
111
|
+
}.map { |dir|
|
112
|
+
dir.gsub(/^#{template_dir}\//,'')
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
options = {}
|
117
|
+
option_parser = OptionParser.new do |opts|
|
118
|
+
executable = File.basename(__FILE__)
|
119
|
+
opts.banner = "Usage: #{executable} [options] app_name"
|
120
|
+
|
121
|
+
opts.on("--force","Overwrite files if they exist") do
|
122
|
+
options[:force] = true
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
option_parser.parse!
|
127
|
+
|
128
|
+
EXE = File.expand_path(__FILE__)
|
129
|
+
|
130
|
+
if ARGV.empty?
|
131
|
+
STDERR.puts("error: app_dir required")
|
132
|
+
exit 2
|
133
|
+
else
|
134
|
+
exit main(ARGV[0],options[:force])
|
135
|
+
end
|
136
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
Feature: Bootstrap a new command-line app
|
2
|
+
As an awesome developer who wants to make a command-line app
|
3
|
+
I should be able to use methadone to bootstrap it
|
4
|
+
And get all kinds of cool things
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given the directory "tmp/newgem" does not exist
|
8
|
+
|
9
|
+
Scenario: Bootstrap a new app from scratch
|
10
|
+
When I successfully run `methadone tmp/newgem`
|
11
|
+
Then the following directories should exist:
|
12
|
+
|tmp/newgem |
|
13
|
+
|tmp/newgem/bin |
|
14
|
+
|tmp/newgem/lib |
|
15
|
+
|tmp/newgem/lib/newgem |
|
16
|
+
|tmp/newgem/test |
|
17
|
+
|tmp/newgem/features |
|
18
|
+
|tmp/newgem/features/support |
|
19
|
+
|tmp/newgem/features/step_definitions |
|
20
|
+
And the following files should exist:
|
21
|
+
|tmp/newgem/newgem.gemspec |
|
22
|
+
|tmp/newgem/Rakefile |
|
23
|
+
|tmp/newgem/Gemfile |
|
24
|
+
|tmp/newgem/bin/newgem |
|
25
|
+
|tmp/newgem/features/newgem.feature |
|
26
|
+
|tmp/newgem/features/support/env.rb |
|
27
|
+
|tmp/newgem/features/step_definitions/newgem_steps.rb |
|
28
|
+
|tmp/newgem/test/tc_something.rb |
|
29
|
+
And the file "tmp/newgem/newgem.gemspec" should match /add_development_dependency\('grancher'/
|
30
|
+
And the file "tmp/newgem/newgem.gemspec" should match /add_development_dependency\('aruba'/
|
31
|
+
And the file "tmp/newgem/newgem.gemspec" should match /add_development_dependency\('rdoc'/
|
32
|
+
Given I cd to "tmp/newgem"
|
33
|
+
When I successfully run `rake -T`
|
34
|
+
Then the output should contain:
|
35
|
+
"""
|
36
|
+
rake build # Build newgem-0.0.1.gem into the pkg directory
|
37
|
+
rake clean # Remove any temporary products.
|
38
|
+
rake clobber # Remove any generated file.
|
39
|
+
rake clobber_rdoc # Remove RDoc HTML files
|
40
|
+
rake features # Run Cucumber features
|
41
|
+
rake install # Build and install newgem-0.0.1.gem into system gems
|
42
|
+
rake rdoc # Build RDoc HTML files
|
43
|
+
rake release # Create tag v0.0.1 and build and push newgem-0.0.1.gem to Rubygems
|
44
|
+
rake rerdoc # Rebuild RDoc HTML files
|
45
|
+
rake test # Run tests
|
46
|
+
"""
|
47
|
+
When I run `rake`
|
48
|
+
Then the exit status should be 0
|
49
|
+
And the output should contain:
|
50
|
+
"""
|
51
|
+
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
|
52
|
+
"""
|
53
|
+
And the output should contain:
|
54
|
+
"""
|
55
|
+
1 scenario (1 passed)
|
56
|
+
3 steps (3 passed)
|
57
|
+
"""
|
58
|
+
|
59
|
+
Scenario: Won't squash an existing dir
|
60
|
+
When I successfully run `methadone tmp/newgem`
|
61
|
+
And I run `methadone tmp/newgem`
|
62
|
+
Then the exit status should not be 0
|
63
|
+
And the stderr should contain:
|
64
|
+
"""
|
65
|
+
error: tmp/newgem exists, use --force to override
|
66
|
+
"""
|
67
|
+
|
68
|
+
Scenario: WILL squash an existing dir if we use --force
|
69
|
+
When I successfully run `methadone tmp/newgem`
|
70
|
+
And I run `methadone --force tmp/newgem`
|
71
|
+
Then the exit status should be 0
|
72
|
+
|
73
|
+
Scenario: We must supply a dirname
|
74
|
+
When I run `methadone`
|
75
|
+
Then the exit status should not be 0
|
76
|
+
And the stderr should contain:
|
77
|
+
"""
|
78
|
+
error: app_dir required
|
79
|
+
"""
|
80
|
+
|
81
|
+
Scenario: Help is properly documented
|
82
|
+
When I get help for "methadone"
|
83
|
+
Then the exit status should be 0
|
84
|
+
And the following options should be documented:
|
85
|
+
|--force|
|
86
|
+
And the banner should be present
|
87
|
+
And the banner should document that this app takes options
|
88
|
+
And the banner should document that this app's arguments are:
|
89
|
+
|app_name|required|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'methadone/cucumber'
|
3
|
+
|
4
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
5
|
+
ARUBA_DIR = File.join(%w(tmp aruba))
|
6
|
+
Before do
|
7
|
+
@dirs = [ARUBA_DIR]
|
8
|
+
@puts = true
|
9
|
+
@aruba_timeout_seconds = 60
|
10
|
+
end
|
11
|
+
|
12
|
+
|
data/lib/methadone.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Methadone
|
4
|
+
# A Logger appropriate for a command-line program in that it logs
|
5
|
+
# all messages (based on level) to the standard output and logs "error" type
|
6
|
+
# messages additionally to the standard error. By default, this will pretty
|
7
|
+
# much do what you want, however it can be customized:
|
8
|
+
#
|
9
|
+
# * You can override the devices used by passing different devices to the constructor
|
10
|
+
# * You can adjust the level of message that goes to the error logger via error_level=
|
11
|
+
# * You can adjust the format for messages to the error logger separately via error_formatter=
|
12
|
+
#
|
13
|
+
# === Example
|
14
|
+
#
|
15
|
+
# logger = CLILogger.new
|
16
|
+
# logger.debug("Starting up") # => only the standard output gets this
|
17
|
+
# logger.error("Something went wrong!") # => both standard error AND standard output get this
|
18
|
+
class CLILogger < Logger
|
19
|
+
|
20
|
+
# Helper to proxy methods to the super class AND to the internal error logger
|
21
|
+
#
|
22
|
+
# +symbol+:: Symbol for name of the method to proxy
|
23
|
+
def self.proxy_method(symbol) #:nodoc:
|
24
|
+
old_name = "old_#{symbol}".to_sym
|
25
|
+
alias_method old_name,symbol
|
26
|
+
define_method symbol do |*args,&block|
|
27
|
+
send(old_name,*args,&block)
|
28
|
+
@stderr_logger.send(symbol,*args,&block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
proxy_method :'formatter='
|
33
|
+
proxy_method :'datetime_format='
|
34
|
+
proxy_method :add
|
35
|
+
|
36
|
+
# A logger that logs error-type messages to a second device; useful
|
37
|
+
# for ensuring that error messages go to standard error
|
38
|
+
#
|
39
|
+
# +log_device+:: device where all log messages should go, based on level
|
40
|
+
# +error_device+:: device where all error messages should go. By default, this is Logger::Severity::WARN
|
41
|
+
def initialize(log_device=$stdout,error_device=$stderr)
|
42
|
+
super(log_device)
|
43
|
+
@stderr_logger = Logger.new(error_device)
|
44
|
+
@stderr_logger.level = Logger::Severity::WARN
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the threshold for what messages go to the error device. Note that calling
|
48
|
+
# #level= will *not* affect the error logger
|
49
|
+
#
|
50
|
+
# +level+:: a constant from Logger::Severity for the level of messages that should go
|
51
|
+
# to the error logger
|
52
|
+
def error_level=(level)
|
53
|
+
@stderr_logger.level = level
|
54
|
+
end
|
55
|
+
|
56
|
+
# Overrides the formatter for the error logger. A future call to #formatter= will
|
57
|
+
# affect both, so the order of the calls matters.
|
58
|
+
#
|
59
|
+
# +formatter+:: Proc that handles the formatting, the same as for #formatter=
|
60
|
+
def error_formatter=(formatter)
|
61
|
+
@stderr_logger.formatter=formatter
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Methadone
|
2
|
+
# Provides easier access to a shared Methadone::CLILogger instance.
|
3
|
+
#
|
4
|
+
# Include this module into your class, and #logger provides access to a shared logger.
|
5
|
+
# This is handy if you want all of your clases to have access to the same logger, but
|
6
|
+
# don't want to (or aren't able to) pass it around to each class.
|
7
|
+
#
|
8
|
+
# This also provides methods for direct logging without going through the #logger
|
9
|
+
#
|
10
|
+
# === Example
|
11
|
+
#
|
12
|
+
# class MyClass
|
13
|
+
# include Methadone::CLILogger
|
14
|
+
#
|
15
|
+
# def doit
|
16
|
+
# debug("About to doit!")
|
17
|
+
# if results
|
18
|
+
# info("We did it!"
|
19
|
+
# else
|
20
|
+
# error("Something went wrong")
|
21
|
+
# end
|
22
|
+
# debug("Done doing it")
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
module CLILogging
|
26
|
+
# Access the shared logger. All classes that include this module
|
27
|
+
# will get the same logger via this method.
|
28
|
+
def logger
|
29
|
+
@@logger ||= CLILogger.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Change the global logger that includers will use. Useful if you
|
33
|
+
# don't want the default configured logger.
|
34
|
+
#
|
35
|
+
# +new_logger+:: the new logger. May not be nil and should be a a logger of some kind
|
36
|
+
def logger=(new_logger)
|
37
|
+
raise ArgumentError,"Logger may not be nil" if new_logger.nil?
|
38
|
+
@@logger = new_logger
|
39
|
+
end
|
40
|
+
|
41
|
+
# pass-through to <tt>logger.debug(progname,&block)</tt>
|
42
|
+
def debug(progname = nil, &block); logger.debug(progname,&block); end
|
43
|
+
# pass-through to <tt>logger.info(progname,&block)</tt>
|
44
|
+
def info(progname = nil, &block); logger.info(progname,&block); end
|
45
|
+
# pass-through to <tt>logger.warn(progname,&block)</tt>
|
46
|
+
def warn(progname = nil, &block); logger.warn(progname,&block); end
|
47
|
+
# pass-through to <tt>logger.error(progname,&block)</tt>
|
48
|
+
def error(progname = nil, &block); logger.error(progname,&block); end
|
49
|
+
# pass-through to <tt>logger.fatal(progname,&block)</tt>
|
50
|
+
def fatal(progname = nil, &block); logger.fatal(progname,&block); end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
When /^I get help for "([^"]*)"$/ do |app_name|
|
2
|
+
@app_name = app_name
|
3
|
+
When %(I run `#{app_name} --help`)
|
4
|
+
end
|
5
|
+
|
6
|
+
Then /^the following options should be documented:$/ do |options|
|
7
|
+
options.raw.each do |option|
|
8
|
+
Then %(the option "#{option[0]}" should be documented)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Then /^the option "([^"]*)" should be documented$/ do |option|
|
13
|
+
Then %(the output should match /^\\s*#{option}\\s+\\w\\w\\w+/)
|
14
|
+
end
|
15
|
+
|
16
|
+
Then /^the banner should be present$/ do
|
17
|
+
Then %(the output should match /Usage: #{@app_name}/)
|
18
|
+
end
|
19
|
+
|
20
|
+
Then /^the banner should document that this app takes options$/ do
|
21
|
+
Then %(the output should match /\[options\]/)
|
22
|
+
end
|
23
|
+
|
24
|
+
Then /^the banner should document that this app's arguments are:$/ do |table|
|
25
|
+
expected_arguments = table.raw.map { |row|
|
26
|
+
option = row[0]
|
27
|
+
option = "[#{option}]" if row[1] == 'optional'
|
28
|
+
}.join(' ')
|
29
|
+
Then %(the output should contain "#{expected_arguments}")
|
30
|
+
end
|
data/methadone.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "methadone/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "methadone"
|
7
|
+
s.version = Methadone::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["davetron5000"]
|
10
|
+
s.email = ["davetron5000 at gmail.com"]
|
11
|
+
s.homepage = "http://github.com/davetron5000/methadone"
|
12
|
+
s.summary = %q{Kick the bash habit and start your command-line apps off right}
|
13
|
+
s.description = %q{Methadone provides a lot of small but useful features for developing a command-line app, including an opinionated bootstrapping process, some helpful cucumber steps, and some classes to bridge logging and output into a simple, unified, interface}
|
14
|
+
|
15
|
+
s.rubyforge_project = "methadone"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
s.add_development_dependency("rspec-expectations")
|
22
|
+
s.add_development_dependency("rake")
|
23
|
+
s.add_development_dependency("rdoc","~> 3.6.1")
|
24
|
+
s.add_development_dependency("aruba")
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'cucumber'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
require 'rdoc/task'
|
7
|
+
|
8
|
+
include Rake::DSL
|
9
|
+
|
10
|
+
Bundler::GemHelper.install_tasks
|
11
|
+
|
12
|
+
Rake::TestTask.new do |t|
|
13
|
+
t.pattern = 'test/tc_*.rb'
|
14
|
+
end
|
15
|
+
|
16
|
+
CUKE_RESULTS = 'results.html'
|
17
|
+
CLEAN << CUKE_RESULTS
|
18
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
19
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
|
20
|
+
t.fork = false
|
21
|
+
end
|
22
|
+
|
23
|
+
Rake::RDocTask.new do |rd|
|
24
|
+
rd.main = "README.rdoc"
|
25
|
+
rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
26
|
+
end
|
27
|
+
|
28
|
+
task :default => [:test,:features]
|
@@ -0,0 +1,12 @@
|
|
1
|
+
Feature: My bootstrapped app kinda works
|
2
|
+
In order to get going on coding my awesome app
|
3
|
+
I want to have aruba and cucumber setup
|
4
|
+
So I don't have to do it myself
|
5
|
+
|
6
|
+
Scenario: App just runs
|
7
|
+
When I run `<%= gemname %> --help`
|
8
|
+
Then the exit status should be 0
|
9
|
+
And the output should contain:
|
10
|
+
"""
|
11
|
+
Usage: <%= gemname %> [options]
|
12
|
+
"""
|
@@ -0,0 +1 @@
|
|
1
|
+
# Put your step defintions here
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'rspec/expectations'
|
3
|
+
|
4
|
+
class BaseTest < Test::Unit::TestCase
|
5
|
+
# Copied from Rails; makes a test method using a string
|
6
|
+
def self.test(name, &block)
|
7
|
+
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
|
8
|
+
defined = instance_method(test_name) rescue false
|
9
|
+
raise "#{test_name} is already defined in #{self}" if defined
|
10
|
+
if block_given?
|
11
|
+
define_method(test_name, &block)
|
12
|
+
else
|
13
|
+
define_method(test_name) do
|
14
|
+
raise "No implementation provided for #{name}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'base_test'
|
2
|
+
require 'methadone'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class TestCLILogger < BaseTest
|
6
|
+
include Methadone
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@blank_format = proc { |severity,datetime,progname,msg|
|
10
|
+
msg + "\n"
|
11
|
+
}
|
12
|
+
@real_stderr = $stderr
|
13
|
+
@real_stdout = $stdout
|
14
|
+
$stderr = StringIO.new
|
15
|
+
$stdout = StringIO.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
$stderr = @real_stderr
|
20
|
+
$stdout = @real_stdout
|
21
|
+
end
|
22
|
+
|
23
|
+
test "logger sends everything to stdout, and warns, errors, and fatals to stderr" do
|
24
|
+
logger = logger_with_blank_format
|
25
|
+
|
26
|
+
logger.debug("debug")
|
27
|
+
logger.info("info")
|
28
|
+
logger.warn("warn")
|
29
|
+
logger.error("error")
|
30
|
+
logger.fatal("fatal")
|
31
|
+
|
32
|
+
$stdout.string.should == "debug\ninfo\nwarn\nerror\nfatal\n"
|
33
|
+
$stderr.string.should == "warn\nerror\nfatal\n"
|
34
|
+
end
|
35
|
+
|
36
|
+
test "we can control what goes to stderr" do
|
37
|
+
logger = logger_with_blank_format
|
38
|
+
logger.error_level = Logger::Severity::FATAL
|
39
|
+
|
40
|
+
logger.debug("debug")
|
41
|
+
logger.info("info")
|
42
|
+
logger.warn("warn")
|
43
|
+
logger.error("error")
|
44
|
+
logger.fatal("fatal")
|
45
|
+
|
46
|
+
$stdout.string.should == "debug\ninfo\nwarn\nerror\nfatal\n"
|
47
|
+
$stderr.string.should == "fatal\n"
|
48
|
+
end
|
49
|
+
|
50
|
+
test "we can log to alternate devices easily" do
|
51
|
+
out = StringIO.new
|
52
|
+
err = StringIO.new
|
53
|
+
|
54
|
+
logger = CLILogger.new(out,err)
|
55
|
+
logger.formatter = @blank_format
|
56
|
+
|
57
|
+
logger.debug("debug")
|
58
|
+
logger.info("info")
|
59
|
+
logger.warn("warn")
|
60
|
+
logger.error("error")
|
61
|
+
logger.fatal("fatal")
|
62
|
+
|
63
|
+
out.string.should == "debug\ninfo\nwarn\nerror\nfatal\n"
|
64
|
+
err.string.should == "warn\nerror\nfatal\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
test "error logger ignores the log level" do
|
69
|
+
logger = logger_with_blank_format
|
70
|
+
logger.level = Logger::Severity::FATAL
|
71
|
+
logger.debug("debug")
|
72
|
+
logger.info("info")
|
73
|
+
logger.warn("warn")
|
74
|
+
logger.error("error")
|
75
|
+
logger.fatal("fatal")
|
76
|
+
|
77
|
+
$stdout.string.should == "fatal\n"
|
78
|
+
$stderr.string.should == "warn\nerror\nfatal\n"
|
79
|
+
end
|
80
|
+
|
81
|
+
test "both loggers use the same date format" do
|
82
|
+
logger = CLILogger.new
|
83
|
+
logger.datetime_format = "the time"
|
84
|
+
logger.debug("debug")
|
85
|
+
logger.error("error")
|
86
|
+
$stdout.string.should match /the time.*DEBUG.*debug/
|
87
|
+
$stderr.string.should match /the time.*ERROR.*error/
|
88
|
+
end
|
89
|
+
|
90
|
+
test "error logger does not get <<" do
|
91
|
+
logger = logger_with_blank_format
|
92
|
+
logger << "foo"
|
93
|
+
$stdout.string.should == "foo"
|
94
|
+
$stderr.string.should == ""
|
95
|
+
end
|
96
|
+
|
97
|
+
test "error logger can have a different format" do
|
98
|
+
logger = logger_with_blank_format
|
99
|
+
logger.error_formatter = proc { |severity,datetime,progname,msg|
|
100
|
+
"ERROR_LOGGER: #{msg}\n"
|
101
|
+
}
|
102
|
+
logger.debug("debug")
|
103
|
+
logger.error("error")
|
104
|
+
$stdout.string.should == "debug\nerror\n"
|
105
|
+
$stderr.string.should == "ERROR_LOGGER: error\n"
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def logger_with_blank_format
|
111
|
+
logger = CLILogger.new
|
112
|
+
logger.formatter = @blank_format
|
113
|
+
logger.level = Logger::Severity::DEBUG
|
114
|
+
logger
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'base_test'
|
2
|
+
require 'methadone'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class TestCLILogging < BaseTest
|
6
|
+
include Methadone
|
7
|
+
|
8
|
+
def setup
|
9
|
+
@blank_format = proc do |severity,datetime,progname,msg|
|
10
|
+
msg + "\n"
|
11
|
+
end
|
12
|
+
@real_stderr = $stderr
|
13
|
+
@real_stdout = $stdout
|
14
|
+
$stderr = StringIO.new
|
15
|
+
$stdout = StringIO.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
$stderr = @real_stderr
|
20
|
+
$stdout = @real_stdout
|
21
|
+
end
|
22
|
+
|
23
|
+
class MyClassThatLogsToStdout
|
24
|
+
include Methadone::CLILogging
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
logger.formatter = proc do |severity,datetime,progname,msg|
|
28
|
+
msg + "\n"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def doit
|
33
|
+
debug("debug")
|
34
|
+
info("info")
|
35
|
+
warn("warn")
|
36
|
+
error("error")
|
37
|
+
fatal("fatal")
|
38
|
+
end
|
39
|
+
|
40
|
+
def logger_id; logger.object_id; end
|
41
|
+
end
|
42
|
+
|
43
|
+
test "a class can include CLILogging and get terser logging" do
|
44
|
+
MyClassThatLogsToStdout.new.doit
|
45
|
+
$stdout.string.should == "debug\ninfo\nwarn\nerror\nfatal\n"
|
46
|
+
$stderr.string.should == "warn\nerror\nfatal\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
test "another class using CLILogging gets the same logger instance" do
|
50
|
+
first = MyClassThatLogsToStdout.new
|
51
|
+
second = MyOtherClassThatLogsToStdout.new
|
52
|
+
first.logger_id.should == second.logger_id
|
53
|
+
end
|
54
|
+
|
55
|
+
test "we can change the global logger" do
|
56
|
+
first = MyClassThatLogsToStdout.new
|
57
|
+
second = MyOtherClassThatLogsToStdout.new
|
58
|
+
logger_id = second.logger_id
|
59
|
+
|
60
|
+
second.change_logger
|
61
|
+
|
62
|
+
logger_id.should_not == second.logger_id
|
63
|
+
first.logger_id.should == second.logger_id
|
64
|
+
end
|
65
|
+
|
66
|
+
test "we cannot use a nil logger" do
|
67
|
+
lambda { MyOtherClassThatLogsToStdout.new.change_to_nil_logger }.should raise_error(ArgumentError)
|
68
|
+
end
|
69
|
+
|
70
|
+
class MyOtherClassThatLogsToStdout
|
71
|
+
include Methadone::CLILogging
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
logger.formatter = proc do |severity,datetime,progname,msg|
|
75
|
+
msg + "\n"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def doit
|
80
|
+
debug("debug")
|
81
|
+
info("info")
|
82
|
+
warn("warn")
|
83
|
+
error("error")
|
84
|
+
fatal("fatal")
|
85
|
+
end
|
86
|
+
|
87
|
+
def change_logger
|
88
|
+
self.logger=Methadone::CLILogger.new
|
89
|
+
end
|
90
|
+
|
91
|
+
def change_to_nil_logger
|
92
|
+
self.logger = nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def logger_id; logger.object_id; end
|
96
|
+
end
|
97
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: methadone
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- davetron5000
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec-expectations
|
16
|
+
requirement: &70311581180940 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70311581180940
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70311581180520 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70311581180520
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rdoc
|
38
|
+
requirement: &70311581180020 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.6.1
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70311581180020
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: aruba
|
49
|
+
requirement: &70311581179600 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70311581179600
|
58
|
+
description: Methadone provides a lot of small but useful features for developing
|
59
|
+
a command-line app, including an opinionated bootstrapping process, some helpful
|
60
|
+
cucumber steps, and some classes to bridge logging and output into a simple, unified,
|
61
|
+
interface
|
62
|
+
email:
|
63
|
+
- davetron5000 at gmail.com
|
64
|
+
executables:
|
65
|
+
- methadone
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rvmrc
|
71
|
+
- Gemfile
|
72
|
+
- README.rdoc
|
73
|
+
- Rakefile
|
74
|
+
- bin/methadone
|
75
|
+
- features/bootstrap.feature
|
76
|
+
- features/step_definitions/bootstrap_steps.rb
|
77
|
+
- features/support/env.rb
|
78
|
+
- lib/methadone.rb
|
79
|
+
- lib/methadone/cli_logger.rb
|
80
|
+
- lib/methadone/cli_logging.rb
|
81
|
+
- lib/methadone/cucumber.rb
|
82
|
+
- lib/methadone/version.rb
|
83
|
+
- methadone.gemspec
|
84
|
+
- templates/full/Rakefile.erb
|
85
|
+
- templates/full/bin/executable.erb
|
86
|
+
- templates/full/features/executable.feature.erb
|
87
|
+
- templates/full/features/step_definitions/executable_steps.rb.erb
|
88
|
+
- templates/full/features/support/env.rb.erb
|
89
|
+
- templates/full/test/tc_something.rb.erb
|
90
|
+
- test/base_test.rb
|
91
|
+
- test/test_cli_logger.rb
|
92
|
+
- test/test_cli_logging.rb
|
93
|
+
homepage: http://github.com/davetron5000/methadone
|
94
|
+
licenses: []
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ! '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubyforge_project: methadone
|
113
|
+
rubygems_version: 1.8.10
|
114
|
+
signing_key:
|
115
|
+
specification_version: 3
|
116
|
+
summary: Kick the bash habit and start your command-line apps off right
|
117
|
+
test_files:
|
118
|
+
- features/bootstrap.feature
|
119
|
+
- features/step_definitions/bootstrap_steps.rb
|
120
|
+
- features/support/env.rb
|
121
|
+
- test/base_test.rb
|
122
|
+
- test/test_cli_logger.rb
|
123
|
+
- test/test_cli_logging.rb
|