optparse-plus 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +7 -0
  6. data/CHANGES.md +66 -0
  7. data/Gemfile +5 -0
  8. data/LICENSE.txt +201 -0
  9. data/README.rdoc +173 -0
  10. data/Rakefile +94 -0
  11. data/bin/optparse_plus +130 -0
  12. data/fix.rb +29 -0
  13. data/lib/optparse-plus.rb +1 -0
  14. data/lib/optparse_plus.rb +15 -0
  15. data/lib/optparse_plus/argv_parser.rb +50 -0
  16. data/lib/optparse_plus/cli.rb +116 -0
  17. data/lib/optparse_plus/cli_logger.rb +133 -0
  18. data/lib/optparse_plus/cli_logging.rb +138 -0
  19. data/lib/optparse_plus/cucumber.rb +119 -0
  20. data/lib/optparse_plus/error.rb +32 -0
  21. data/lib/optparse_plus/execution_strategy/base.rb +34 -0
  22. data/lib/optparse_plus/execution_strategy/jvm.rb +37 -0
  23. data/lib/optparse_plus/execution_strategy/mri.rb +16 -0
  24. data/lib/optparse_plus/execution_strategy/open_3.rb +16 -0
  25. data/lib/optparse_plus/execution_strategy/open_4.rb +22 -0
  26. data/lib/optparse_plus/execution_strategy/rbx_open_4.rb +12 -0
  27. data/lib/optparse_plus/exit_now.rb +40 -0
  28. data/lib/optparse_plus/main.rb +603 -0
  29. data/lib/optparse_plus/process_status.rb +45 -0
  30. data/lib/optparse_plus/sh.rb +223 -0
  31. data/lib/optparse_plus/test/base_integration_test.rb +31 -0
  32. data/lib/optparse_plus/test/integration_test_assertions.rb +65 -0
  33. data/lib/optparse_plus/version.rb +3 -0
  34. data/optparse_plus.gemspec +28 -0
  35. data/templates/full/.gitignore.erb +4 -0
  36. data/templates/full/README.rdoc.erb +24 -0
  37. data/templates/full/Rakefile.erb +71 -0
  38. data/templates/full/_license_head.txt.erb +2 -0
  39. data/templates/full/apache_LICENSE.txt.erb +203 -0
  40. data/templates/full/bin/executable.erb +45 -0
  41. data/templates/full/custom_LICENSE.txt.erb +0 -0
  42. data/templates/full/gplv2_LICENSE.txt.erb +14 -0
  43. data/templates/full/gplv3_LICENSE.txt.erb +14 -0
  44. data/templates/full/mit_LICENSE.txt.erb +7 -0
  45. data/templates/rspec/spec/something_spec.rb.erb +5 -0
  46. data/templates/test_unit/test/integration/test_cli.rb.erb +11 -0
  47. data/templates/test_unit/test/unit/test_something.rb.erb +7 -0
  48. data/test/integration/base_integration_test.rb +60 -0
  49. data/test/integration/test_bootstrap.rb +150 -0
  50. data/test/integration/test_cli.rb +21 -0
  51. data/test/integration/test_license.rb +56 -0
  52. data/test/integration/test_readme.rb +53 -0
  53. data/test/integration/test_rspec.rb +28 -0
  54. data/test/integration/test_version.rb +21 -0
  55. data/test/unit/base_test.rb +19 -0
  56. data/test/unit/command_for_tests.sh +7 -0
  57. data/test/unit/execution_strategy/test_base.rb +24 -0
  58. data/test/unit/execution_strategy/test_jvm.rb +77 -0
  59. data/test/unit/execution_strategy/test_mri.rb +32 -0
  60. data/test/unit/execution_strategy/test_open_3.rb +70 -0
  61. data/test/unit/execution_strategy/test_open_4.rb +86 -0
  62. data/test/unit/execution_strategy/test_rbx_open_4.rb +25 -0
  63. data/test/unit/test/test_integration_test_assertions.rb +211 -0
  64. data/test/unit/test_cli_logger.rb +219 -0
  65. data/test/unit/test_cli_logging.rb +243 -0
  66. data/test/unit/test_exit_now.rb +37 -0
  67. data/test/unit/test_main.rb +840 -0
  68. data/test/unit/test_sh.rb +404 -0
  69. metadata +260 -0
@@ -0,0 +1,94 @@
1
+ require 'sdoc'
2
+ require 'bundler'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+
6
+ include Rake::DSL
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ desc 'run unit tests'
11
+ Rake::TestTask.new do |t|
12
+ t.libs << "lib"
13
+ t.libs << "test/unit"
14
+ test_file = ENV["TEST"]
15
+ ENV.delete("TEST")
16
+ t.test_files = if test_file
17
+ [test_file]
18
+ else
19
+ FileList['test/unit/test_*.rb'] +
20
+ FileList['test/unit/execution_strategy/test_*.rb'] +
21
+ FileList['test/unit/test/test_*.rb']
22
+ end
23
+ end
24
+
25
+ desc 'run integration tests'
26
+ Rake::TestTask.new("test:integration") do |t|
27
+ t.libs << "lib"
28
+ t.libs << "test/integration"
29
+ test_file = ENV["TEST"]
30
+ ENV.delete("TEST")
31
+ t.test_files = if test_file
32
+ [test_file]
33
+ else
34
+ FileList['test/integration/test_*.rb']
35
+ end
36
+ end
37
+
38
+ desc 'build rdoc'
39
+ task :rdoc => [:build_rdoc, :hack_css]
40
+ RDoc::Task.new(:build_rdoc) do |rd|
41
+ rd.main = "README.rdoc"
42
+ rd.options << '-f' << 'sdoc'
43
+ rd.template = 'direct'
44
+ rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
45
+ rd.title = 'optparse-plus - Power Up your Command Line Apps'
46
+ end
47
+ CLOBBER << 'html'
48
+
49
+ FONT_FIX = {
50
+ "0.82em" => "16px",
51
+ "0.833em" => "16px",
52
+ "0.85em" => "16px",
53
+ "1.15em" => "20px",
54
+ "1.1em" => "20px",
55
+ "1.2em" => "20px",
56
+ "1.4em" => "24px",
57
+ "1.5em" => "24px",
58
+ "1.6em" => "32px",
59
+ "1em" => "16px",
60
+ "2.1em" => "38px",
61
+ }
62
+
63
+
64
+ task :hack_css do
65
+ maincss = File.open('html/css/main.css').readlines
66
+ File.open('html/css/main.css','w') do |file|
67
+ file.puts '@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700);'
68
+ maincss.each do |line|
69
+ if line.strip == 'font-family: "Helvetica Neue", Arial, sans-serif;'
70
+ file.puts 'font-family: Lato, "Helvetica Neue", Arial, sans-serif;'
71
+ elsif line.strip == 'font-family: monospace;'
72
+ file.puts 'font-family: Monaco, monospace;'
73
+ elsif line =~ /^pre\s*$/
74
+ file.puts "pre {
75
+ font-family: Monaco, monospace;
76
+ margin-bottom: 1em;
77
+ }
78
+ pre.original"
79
+ elsif line =~ /^\s*font-size:\s*(.*)\s*;/
80
+ if FONT_FIX[$1]
81
+ file.puts "font-size: #{FONT_FIX[$1]};"
82
+ else
83
+ file.puts line.chomp
84
+ end
85
+ else
86
+ file.puts line.chomp
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ CLEAN << "coverage"
93
+ CLOBBER << FileList['**/*.rbc']
94
+ task :default => [:test, "test:integration"]
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'fileutils'
4
+ require 'optparse'
5
+ require 'optparse_plus'
6
+ require 'optparse_plus/cli'
7
+
8
+ include FileUtils
9
+ include OptparsePlus::Main
10
+ include OptparsePlus::CLILogging
11
+ include OptparsePlus::CLI
12
+ include OptparsePlus::SH
13
+
14
+ main do |app_name|
15
+ check_and_prepare_basedir!(app_name,options[:force])
16
+ using_readme = options[:readme]
17
+
18
+ gemname = File.basename(app_name)
19
+ module_name = gemname.split(/-/).map(&:capitalize).collect{ |segment| segment.split(/_/).map(&:capitalize).join('') }.join('::')
20
+
21
+ debug "Creating project for gem #{gemname}"
22
+
23
+ chdir File.dirname(app_name)
24
+
25
+ require_file = nil
26
+ sh! "bundle gem #{gemname} --no-color" do |stdout|
27
+ require_file = stdout.split(/\n/).select { |file|
28
+ file =~ /\.rb$/ && file !~ /version.rb$/
29
+ }.first
30
+ end
31
+ rm_rf "#{gemname}/spec" # Don't want the default RSpec droppings
32
+ rm_rf "#{gemname}/.rspec"
33
+
34
+ require_file = gemname if require_file.nil?
35
+ require_file.gsub!(/^.*lib\//,'')
36
+
37
+ chdir gemname
38
+
39
+ template_dirs_in(:full).each { |dir| mkdir_p dir }
40
+
41
+ rspec = options[:rspec]
42
+
43
+ ["Rakefile", ".gitignore", ].each do |file|
44
+ copy_file file, :binding => binding
45
+ end
46
+
47
+ if rspec
48
+ template_dirs_in(:rspec).each { |dir| mkdir_p dir }
49
+ copy_file "spec/something_spec.rb", :from => :rspec, :binding => binding
50
+ else
51
+ template_dirs_in(:test_unit).each { |dir| mkdir_p dir }
52
+ copy_file "test/unit/test_something.rb", :from => :test_unit, :binding => binding
53
+ copy_file "test/integration/test_cli.rb", :from => :test_unit, :binding => binding
54
+ end
55
+
56
+
57
+ gemspec = "#{gemname}.gemspec"
58
+ gem_variable = File.open(gemspec) { |x| x.read }.match(/(\w+)\.executables/)[1]
59
+
60
+ license = options[:license]
61
+ warn "warning: your app has no license" unless license
62
+ license = nil if license == 'NONE'
63
+ if license
64
+ copy_file "#{options[:license]}_LICENSE.txt", :as => "LICENSE.txt"
65
+ else
66
+ #Remove the MIT license generated by `bundle gem`
67
+ debug "Making sure no LICENSE.txt file exists at the root of the repo"
68
+ FileUtils.rm_f "LICENSE.txt"
69
+ end
70
+
71
+ #Ensure the gemspec file mentions the correct license
72
+ gemspec_content = File.read(gemspec)
73
+ if gemspec_content =~ /(^\s*#{gem_variable}\.license\s*=\s*).*/
74
+ gemspec_content.gsub!(/(^\s*#{gem_variable}\.license\s*=\s*).*/,"\\1#{license.to_s.inspect}")
75
+ else
76
+ gemspec_content.gsub!(/(^\s*#{gem_variable}\.name\s*=\s*.*)/,"\\1\n#{" #{gem_variable}.license".ljust(20)} = #{license.to_s.inspect.upcase}")
77
+ end
78
+ # RubyGems won't deal with a gemspec in this state and so Bundler won't even
79
+ # work at all. This is not helpful, so replace the magic keys its looking for
80
+ # with something else
81
+ gemspec_content.gsub!(/TODO/,"to-do")
82
+ gemspec_content.gsub!(/FIXME/,"fix me")
83
+ gemspec_content.gsub!(/^.*\.homepage.*$/,"")
84
+ File.open(gemspec,'w') {|f| f.write gemspec_content }
85
+
86
+
87
+ copy_file "README.rdoc", :binding => binding if using_readme
88
+
89
+ copy_file "bin/executable", :as => gemname, :executable => true, :binding => binding
90
+
91
+ add_to_file gemspec, [
92
+ " #{gem_variable}.add_development_dependency('rdoc')",
93
+ " #{gem_variable}.add_dependency('optparse_plus', '~> #{OptparsePlus::VERSION}')",
94
+ ], :before => /^end\s*$/
95
+ ruby_major,ruby_minor,ruby_patch = RUBY_VERSION.split(/\./).map(&:to_i)
96
+
97
+ if ruby_major >= 2 && ruby_minor >= 2
98
+ add_to_file gemspec, [
99
+ " #{gem_variable}.add_development_dependency('test-unit')",
100
+ ], :before => /^end\s*$/
101
+ end
102
+
103
+ if rspec
104
+ add_to_file gemspec, [
105
+ " #{gem_variable}.add_development_dependency('rspec', '~> 3')",
106
+ ], :before => /^end\s*$/
107
+ end
108
+ FileUtils.rm_f "README.md", verbose: true
109
+
110
+ sh! %q(git add --all .)
111
+ end
112
+
113
+ options[:readme] = true
114
+
115
+ description "Kick the bash habit by bootstrapping your Ruby command-line apps"
116
+
117
+ on("--force","Overwrite files if they exist")
118
+ on("--[no-]readme","[Do not ]produce a README file")
119
+ on("--rspec", "Generate RSpec unit tests instead of Test::Unit")
120
+
121
+ licenses = %w(mit apache gplv2 gplv3 custom NONE)
122
+ on("-l LICENSE","--license",licenses,"Specify the license for your project",'(' + licenses.join('|') + ')')
123
+
124
+ use_log_level_option
125
+
126
+ arg :app_name, :required, "Name of your app, which is used for the gem name and executable name"
127
+
128
+ version OptparsePlus::VERSION, :compact => true
129
+
130
+ go!
data/fix.rb ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "pathname"
4
+ require "fileutils"
5
+
6
+
7
+ ARGV.each do |filename|
8
+ filename = Pathname(filename)
9
+
10
+ contents = File.read(filename).split(/\n/)
11
+ File.open(filename,"w") do |file|
12
+ contents.each do |line|
13
+ file.puts line.gsub(/methadone/,"optparse_plus").gsub(/Methadone/,"OptparsePlus")
14
+ end
15
+ end
16
+
17
+ if filename.split.any? { |_| _ == "methadone" }
18
+ new_filename = filename.split.map { |_|
19
+ if _ == "methadone"
20
+ "optparse_plus"
21
+ else
22
+ _
23
+ end
24
+ }.join
25
+ FileUtils.mkdir_p new_filename.dirname
26
+ FileUtils.mv filename new_filename
27
+ end
28
+
29
+ end
@@ -0,0 +1 @@
1
+ require_relative "optparse_plus"
@@ -0,0 +1,15 @@
1
+ require 'optparse_plus/version'
2
+ require 'optparse_plus/cli_logger'
3
+ require 'optparse_plus/cli_logging'
4
+ require 'optparse_plus/exit_now'
5
+ require 'optparse_plus/argv_parser'
6
+ require 'optparse_plus/main'
7
+ require 'optparse_plus/error'
8
+ require 'optparse_plus/execution_strategy/base'
9
+ require 'optparse_plus/execution_strategy/mri'
10
+ require 'optparse_plus/execution_strategy/open_3'
11
+ require 'optparse_plus/execution_strategy/open_4'
12
+ require 'optparse_plus/execution_strategy/rbx_open_4'
13
+ require 'optparse_plus/execution_strategy/jvm'
14
+ require 'optparse_plus/sh'
15
+ # Note: DO NOT require cli.rb, cucumber.rb, or anything in test/ here
@@ -0,0 +1,50 @@
1
+ module OptparsePlus #:nodoc:
2
+ # Assists with parsing strings in the same way that ARGV might.
3
+ # This is *not* used to parse the command-line, but used to
4
+ # parse config files/environment variables so they can be placed into ARGV and properly interpretted by
5
+ # OptionParser
6
+ module ARGVParser #:nodoc:
7
+
8
+ private
9
+
10
+ # Parses +string+, returning an array that can be placed into ARGV or given to OptionParser
11
+ def parse_string_for_argv(string) #:nodoc:
12
+ return [] if string.nil?
13
+
14
+ args = [] # return value we are building up
15
+ current = 0 # pointer to where we are in +string+
16
+ next_arg = '' # the next arg we are building up to ultimatley put into args
17
+ inside_quote = nil # quote character we are "inside" of
18
+ last_char = nil # the last character we saw
19
+
20
+ while current < string.length
21
+ char = string.chars.to_a[current]
22
+ case char
23
+ when /["']/
24
+ if inside_quote.nil? # eat the quote, but remember we are now "inside" one
25
+ inside_quote = char
26
+ elsif inside_quote == char # we closed the quote we were "inside"
27
+ inside_quote = nil
28
+ else # we got a different quote, so it goes in literally
29
+ next_arg << char
30
+ end
31
+ when /\s/
32
+ if last_char == "\\" # we have an escaped space, replace the escape char
33
+ next_arg[-1] = char
34
+ elsif inside_quote # we are inside a quote so keep the space
35
+ next_arg << char
36
+ else # new argument
37
+ args << next_arg
38
+ next_arg = ''
39
+ end
40
+ else
41
+ next_arg << char
42
+ end
43
+ current += 1
44
+ last_char = char
45
+ end
46
+ args << next_arg unless next_arg == ''
47
+ args
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,116 @@
1
+ require 'erb'
2
+
3
+ module OptparsePlus
4
+ # <b>OptparsePlus Internal - treat as private</b>
5
+ #
6
+ # Stuff to implement optparse_plus's CLI app. These
7
+ # stuff isn't generally for your use and it's not
8
+ # included when you require 'optparse_plus'
9
+ module CLI
10
+
11
+ # Checks that the basedir can be used, either by
12
+ # not existing, or by existing and force is true.
13
+ # In that case, we clean it out entirely
14
+ #
15
+ # +basedir+:: base directory where the user wants to create a new project
16
+ # +force+:: if true, and +basedir+ exists, delete it before proceeding
17
+ #
18
+ # This will exit the app if the dir exists and force is false
19
+ def check_and_prepare_basedir!(basedir,force)
20
+ if File.exists? basedir
21
+ if force
22
+ rm_rf basedir, :verbose => true, :secure => true
23
+ else
24
+ exit_now! 1,"error: #{basedir} exists, use --force to override"
25
+ end
26
+ end
27
+ mkdir_p basedir
28
+ end
29
+
30
+ # Add content to a file
31
+ #
32
+ # +file+:: path to the file
33
+ # +lines+:: Array of String representing the lines to add
34
+ # +options+:: Hash of options:
35
+ # <tt>:before</tt>:: A regexp that will appear right after the new content. i.e.
36
+ # this is where to insert said content.
37
+ def add_to_file(file,lines,options = {})
38
+ new_lines = []
39
+ found_line = false
40
+ File.open(file).readlines.each do |line|
41
+ line.chomp!
42
+ if options[:before] && options[:before] === line
43
+ found_line = true
44
+ new_lines += lines
45
+ end
46
+ new_lines << line
47
+ end
48
+
49
+ raise "No line matched #{options[:before]}" if options[:before] && !found_line
50
+
51
+ new_lines += lines unless options[:before]
52
+ File.open(file,'w') do |fp|
53
+ new_lines.each { |line| fp.puts line }
54
+ end
55
+ end
56
+
57
+ # Copies a file, running it through ERB
58
+ #
59
+ # +relative_path+:: path to the file, relative to the project root, minus the .erb extension
60
+ # You should use forward slashes to separate paths; this method
61
+ # will handle making the ultimate path OS independent.
62
+ # +options+:: Options to affect how the copy is done:
63
+ # <tt>:from</tt>:: The name of the profile from which to find the file, "full" by default
64
+ # <tt>:as</tt>:: The name the file should get if not the one in relative_path
65
+ # <tt>:executable</tt>:: true if this file should be set executable
66
+ # <tt>:binding</tt>:: the binding to use for the template
67
+ def copy_file(relative_path,options = {})
68
+ options[:from] ||= :full
69
+
70
+ relative_path = File.join(relative_path.split(/\//))
71
+
72
+ template_path = File.join(template_dir(options[:from]),relative_path + ".erb")
73
+ template = ERB.new(File.open(template_path).readlines.join(''))
74
+
75
+ relative_path_parts = File.split(relative_path)
76
+ relative_path_parts[-1] = options[:as] if options[:as]
77
+
78
+ erb_binding = options[:binding] or binding
79
+
80
+ File.open(File.join(relative_path_parts),'w') do |file|
81
+ file.puts template.result(erb_binding)
82
+ file.chmod(0755) if options[:executable]
83
+ end
84
+ end
85
+
86
+ # Get the location of the templates for profile "from"
87
+ def template_dir(from)
88
+ File.join(File.dirname(__FILE__),'..','..','templates',from.to_s)
89
+ end
90
+
91
+ def template_dirs_in(profile)
92
+ template_dir = template_dir(profile)
93
+
94
+ Dir["#{template_dir}/**/*"].select { |x|
95
+ File.directory? x
96
+ }.map { |dir|
97
+ dir.gsub(/^#{template_dir}\//,'')
98
+ }
99
+ end
100
+
101
+ def render_license_partial(partial)
102
+ ERB.new(File.read(template_dir('full/'+partial))).result(binding).strip
103
+ end
104
+
105
+ def gemspec
106
+ @gemspec || @gemspec=_get_gemspec
107
+ end
108
+ private
109
+ def _get_gemspec
110
+ files=Dir.glob("*.gemspec")
111
+ raise "Multiple gemspec files" if files.size>1
112
+ raise "No gemspec file" if files.size < 1
113
+ Gem::Specification::load(files.first)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,133 @@
1
+ require 'logger'
2
+
3
+ module OptparsePlus
4
+ # A Logger instance that gives better control of messaging the user
5
+ # and logging app activity. At it's most basic, you would use <tt>info</tt>
6
+ # as a replacement for +puts+ and <tt>error</tt> as a replacement
7
+ # for <tt>STDERR.puts</tt>. Since this is a logger, however, you
8
+ # can also use #debug, #warn, and #fatal, and you can control
9
+ # the format and "logging level" as such.
10
+ #
11
+ # So, by default:
12
+ # * debug messages do not appear anywhere
13
+ # * info messages appear on the standard output
14
+ # * warn, error, and fatal messagse appear on the standard error
15
+ # * The default format of messages is simply the message, no logging cruft, however if your output
16
+ # is redirected to a file, a better timestamped logging format is used
17
+ #
18
+ # You can customize this in several ways:
19
+ #
20
+ # * You can override the devices used by passing different devices to the constructor
21
+ # * You can adjust the level of message that goes to the error logger via error_level=
22
+ # * You can adjust the format for messages to the error logger separately via error_formatter=
23
+ #
24
+ # === Example
25
+ #
26
+ # logger = CLILogger.new
27
+ # logger.debug("Starting up") # => only the standard output gets this
28
+ # logger.warn("careful!") # => only the standard error gets this
29
+ # logger.error("Something went wrong!") # => only the standard error gets this
30
+ #
31
+ # logger = CLILogger.new
32
+ # logger.error_level = Logger::ERROR
33
+ # logger.debug("Starting up") # => only the standard output gets this
34
+ # logger.warn("careful!") # => only the standard OUTPUT gets this
35
+ # logger.error("Something went wrong!") # => only the standard error gets this
36
+ #
37
+ # logger = CLILogger.new('logfile.txt')
38
+ # logger.debug("Starting up") # => logfile.txt gets this
39
+ # logger.error("Something went wrong!") # => BOTH logfile.txt AND the standard error get this
40
+ class CLILogger < Logger
41
+ BLANK_FORMAT = proc { |severity,datetime,progname,msg|
42
+ msg + "\n"
43
+ }
44
+
45
+ # Helper to proxy methods to the super class AND to the internal error logger
46
+ #
47
+ # +symbol+:: Symbol for name of the method to proxy
48
+ def self.proxy_method(symbol) #:nodoc:
49
+ old_name = "old_#{symbol}".to_sym
50
+ alias_method old_name,symbol
51
+ define_method symbol do |*args,&block|
52
+ send(old_name,*args,&block)
53
+ @stderr_logger.send(symbol,*args,&block)
54
+ end
55
+ end
56
+
57
+ proxy_method :'formatter='
58
+ proxy_method :'datetime_format='
59
+
60
+ def add(severity, message = nil, progname = nil, &block) #:nodoc:
61
+ if @split_logs
62
+ unless severity >= @stderr_logger.level
63
+ super(severity,message,progname,&block)
64
+ end
65
+ else
66
+ super(severity,message,progname,&block)
67
+ end
68
+ @stderr_logger.add(severity,message,progname,&block)
69
+ end
70
+
71
+ DEFAULT_ERROR_LEVEL = Logger::Severity::WARN
72
+
73
+ # A logger that logs error-type messages to a second device; useful
74
+ # for ensuring that error messages go to standard error. This should be
75
+ # pretty smart about doing the right thing. If both log devices are
76
+ # ttys, e.g. one is going to standard error and the other to the standard output,
77
+ # messages only appear once in the overall output stream. In other words,
78
+ # an ERROR logged will show up *only* in the standard error. If either
79
+ # log device is NOT a tty, then all messages go to +log_device+ and only
80
+ # errors go to +error_device+
81
+ #
82
+ # +log_device+:: device where all log messages should go, based on level
83
+ # +error_device+:: device where all error messages should go. By default, this is Logger::Severity::WARN
84
+ def initialize(log_device=$stdout,error_device=$stderr)
85
+ @stderr_logger = Logger.new(error_device)
86
+
87
+ super(log_device)
88
+
89
+ log_device_tty = tty?(log_device)
90
+ error_device_tty = tty?(error_device)
91
+
92
+ @split_logs = log_device_tty && error_device_tty
93
+
94
+ self.level = Logger::Severity::INFO
95
+ @stderr_logger.level = DEFAULT_ERROR_LEVEL
96
+
97
+ self.formatter = BLANK_FORMAT if log_device_tty
98
+ @stderr_logger.formatter = BLANK_FORMAT if error_device_tty
99
+ end
100
+
101
+ def level=(level)
102
+ super(level)
103
+ if (level > DEFAULT_ERROR_LEVEL) && @split_logs
104
+ @stderr_logger.level = level
105
+ end
106
+ end
107
+
108
+ # Set the threshold for what messages go to the error device. Note that calling
109
+ # #level= will *not* affect the error logger *unless* both devices are TTYs.
110
+ #
111
+ # +level+:: a constant from Logger::Severity for the level of messages that should go
112
+ # to the error logger
113
+ def error_level=(level)
114
+ @stderr_logger.level = level
115
+ end
116
+
117
+ # Overrides the formatter for the error logger. A future call to #formatter= will
118
+ # affect both, so the order of the calls matters.
119
+ #
120
+ # +formatter+:: Proc that handles the formatting, the same as for #formatter=
121
+ def error_formatter=(formatter)
122
+ @stderr_logger.formatter=formatter
123
+ end
124
+
125
+ private
126
+
127
+ def tty?(device_or_string)
128
+ return device_or_string.tty? if device_or_string.respond_to? :tty?
129
+ false
130
+ end
131
+
132
+ end
133
+ end