optparse-plus 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,138 @@
1
+ module OptparsePlus
2
+ # Provides easier access to a shared OptparsePlus::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 OptparsePlus::CLILogging
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
+ #
26
+ # Note that every class that mixes this in shares the *same logger instance*, so if you call #change_logger, this
27
+ # will change the logger for all classes that mix this in. This is likely what you want.
28
+ module CLILogging
29
+
30
+ def self.included(k)
31
+ k.extend(self)
32
+ end
33
+
34
+ # Access the shared logger. All classes that include this module
35
+ # will get the same logger via this method.
36
+ def logger
37
+ @@logger ||= CLILogger.new
38
+ end
39
+
40
+ # Change the global logger that includers will use. Useful if you
41
+ # don't want the default configured logger. Note that the +change_logger+
42
+ # version is preferred because Ruby will often parse <tt>logger = Logger.new</tt> as
43
+ # the declaration of, and assignment to, of a local variable. You'd need to
44
+ # do <tt>self.logger=Logger.new</tt> to be sure. This method
45
+ # is a bit easier.
46
+ #
47
+ # +new_logger+:: the new logger. May not be nil and should be a logger of some kind
48
+ def change_logger(new_logger)
49
+ raise ArgumentError,"Logger may not be nil" if new_logger.nil?
50
+ @@logger = new_logger
51
+ @@logger.level = @log_level if defined?(@log_level) && @log_level
52
+ end
53
+
54
+ alias logger= change_logger
55
+
56
+
57
+ # pass-through to <tt>logger.debug(progname,&block)</tt>
58
+ def debug(progname = nil, &block); logger.debug(progname,&block); end
59
+ # pass-through to <tt>logger.info(progname,&block)</tt>
60
+ def info(progname = nil, &block); logger.info(progname,&block); end
61
+ # pass-through to <tt>logger.warn(progname,&block)</tt>
62
+ def warn(progname = nil, &block); logger.warn(progname,&block); end
63
+ # pass-through to <tt>logger.error(progname,&block)</tt>
64
+ def error(progname = nil, &block); logger.error(progname,&block); end
65
+ # pass-through to <tt>logger.fatal(progname,&block)</tt>
66
+ def fatal(progname = nil, &block); logger.fatal(progname,&block); end
67
+
68
+ LOG_LEVELS = {
69
+ 'debug' => Logger::DEBUG,
70
+ 'info' => Logger::INFO,
71
+ 'warn' => Logger::WARN,
72
+ 'error' => Logger::ERROR,
73
+ 'fatal' => Logger::FATAL,
74
+ }
75
+
76
+ # Call this *if* you've included OptparsePlus::Main to set up a <tt>--log-level</tt> option for your app
77
+ # that will allow the user to configure the logging level. You can pass an optional hash with
78
+ # <tt>:toggle_debug_on_signal => <SIGNAME></tt> to enable runtime toggling of the log level by sending the
79
+ # signal <tt><SIGNAME></tt> to your app
80
+ #
81
+ # +args+:: optional hash
82
+ #
83
+ # Example:
84
+ #
85
+ # main do
86
+ # # your app
87
+ # end
88
+ #
89
+ # use_log_level_option
90
+ #
91
+ # go!
92
+ #
93
+ # Example with runtime toggling:
94
+ #
95
+ #
96
+ # main do
97
+ # # your app
98
+ # end
99
+ #
100
+ # use_log_level_option :toggle_debug_on_signal => 'USR1'
101
+ #
102
+ # go!
103
+ def use_log_level_option(args = {})
104
+ on("--log-level LEVEL",LOG_LEVELS,'Set the logging level',
105
+ '(' + LOG_LEVELS.keys.join('|') + ')',
106
+ '(Default: info)') do |level|
107
+ @log_level = level
108
+ @log_level_original = level
109
+ @log_level_toggled = false
110
+ logger.level = level
111
+
112
+ setup_toggle_trap(args[:toggle_debug_on_signal])
113
+ end
114
+ end
115
+
116
+ private
117
+
118
+ # Call this to toggle the log level between <tt>debug</tt> and its initial value
119
+ def toggle_log_level
120
+ @log_level_original = logger.level unless @log_level_toggled
121
+ logger.level = if @log_level_toggled
122
+ @log_level_original
123
+ else
124
+ LOG_LEVELS['debug']
125
+ end
126
+ @log_level_toggled = !@log_level_toggled
127
+ @log_level = logger.level
128
+ end
129
+
130
+ def setup_toggle_trap(signal)
131
+ if signal
132
+ Signal.trap(signal) do
133
+ toggle_log_level
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,119 @@
1
+ module OptparsePlus
2
+ #
3
+ # **NOTE!** Cucumber is not recommened or supported by optparse_plus, as Aruba has diverged too much. This
4
+ # file is left here to allow you to update optparse_plus but still use Cucumber & Aruba on older versions.
5
+ #
6
+ # By <tt>require</tt>'ing <tt>optparse_plus/cucumber</tt> in your Cucumber setup (e.g. in <tt>env.rb</tt>), you
7
+ # gain access to the steps defined in this file. They provide you with the following:
8
+ #
9
+ # * Run <tt>command_to_run --help</tt> using aruba
10
+ #
11
+ # When I get help for "command_to_run"
12
+ #
13
+ # * Make sure that each option shows up in the help and has *some* sort of documentation. By default,
14
+ # the options won't be required to be negatable.
15
+ #
16
+ # Then the following options should be documented:
17
+ # |--force|
18
+ # |-x |
19
+ #
20
+ # Then the following options should be documented:
21
+ # |--force| which is negatable |
22
+ # |-x | which is not negatable |
23
+ #
24
+ # * Check an individual option for documentation:
25
+ #
26
+ # Then the option "--force" should be documented
27
+ # Then the option "--force" should be documented which is negatable
28
+ #
29
+ # * Checks that the help has a proper usage banner
30
+ #
31
+ # Then the banner should be present
32
+ #
33
+ # * Checks that the banner includes the version
34
+ #
35
+ # Then the banner should include the version
36
+ #
37
+ # * Checks that the usage banner indicates it takes options via <tt>[options]</tt>
38
+ #
39
+ # Then the banner should document that this app takes options
40
+ #
41
+ # * Do the opposite; check that you don't indicate options are accepted
42
+ #
43
+ # Then the banner should document that this app takes no options
44
+ #
45
+ # * Checks that the app's usage banner documents that its arguments are <tt>args</tt>
46
+ #
47
+ # Then the banner should document that this app's arguments are
48
+ # |foo|which is optional|
49
+ # |bar|which is required|
50
+ #
51
+ # * Do the opposite; check that your app doesn't take any arguments
52
+ #
53
+ # Then the banner should document that this app takes no arguments
54
+ #
55
+ # * Check for a usage description which occurs after the banner and a blank line
56
+ #
57
+ # Then there should be a one line summary of what the app does
58
+ #
59
+ module Cucumber
60
+ end
61
+ end
62
+ When /^I get help for "([^"]*)"$/ do |app_name|
63
+ @app_name = app_name
64
+ step %(I run `#{app_name} --help`)
65
+ end
66
+
67
+ Then /^the following options should be documented:$/ do |options|
68
+ options.raw.each do |option|
69
+ step %(the option "#{option[0]}" should be documented #{option[1]})
70
+ end
71
+ end
72
+
73
+ Then /^the option "([^"]*)" should be documented(.*)$/ do |options,qualifiers|
74
+ options.split(',').map(&:strip).each do |option|
75
+ if qualifiers.strip == "which is negatable"
76
+ option = option.gsub(/^--/,"--[no-]")
77
+ end
78
+ step %(the output should match /\\s*#{Regexp.escape(option)}[\\s\\W]+\\w[\\s\\w][\\s\\w]+/)
79
+ end
80
+ end
81
+
82
+ Then /^the banner should be present$/ do
83
+ step %(the output should match /Usage: #{@app_name}/)
84
+ end
85
+
86
+ Then /^the banner should document that this app takes options$/ do
87
+ step %(the output should match /\[options\]/)
88
+ step %(the output should contain "Options")
89
+ end
90
+
91
+ Then /^the banner should document that this app's arguments are:$/ do |table|
92
+ expected_arguments = table.raw.map { |row|
93
+ option = row[0]
94
+ option = "[#{option}]" if row[1] == 'optional' || row[1] == 'which is optional'
95
+ option
96
+ }.join(' ')
97
+ step %(the output should contain "#{expected_arguments}")
98
+ end
99
+
100
+ Then /^the banner should document that this app takes no options$/ do
101
+ step %(the output should not contain "[options]")
102
+ step %(the output should not contain "Options")
103
+ end
104
+
105
+ Then /^the banner should document that this app takes no arguments$/ do
106
+ step %(the output should match /Usage: #{@app_name}\\s*\(\\[options\\]\)?$/)
107
+ end
108
+
109
+ Then /^the banner should include the version$/ do
110
+ step %(the output should match /v\\d+\\.\\d+\\.\\d+/)
111
+ end
112
+
113
+ Then /^there should be a one line summary of what the app does$/ do
114
+ output_lines = all_output.split(/\n/)
115
+ output_lines.size.should >= 3
116
+ # [0] is our banner, which we've checked for
117
+ output_lines[1].should match(/^\s*$/)
118
+ output_lines[2].should match(/^\w+\s+\w+/)
119
+ end
@@ -0,0 +1,32 @@
1
+ module OptparsePlus
2
+ # Standard exception you can throw to exit with a given
3
+ # status code. Generally, you should prefer OptparsePlus::Main#exit_now! over using
4
+ # this directly, however you may wish to create a rich hierarchy of exceptions that extend from
5
+ # this in your app, so this is provided if you wish to do so.
6
+ class Error < StandardError
7
+ attr_reader :exit_code
8
+ # Create an Error with the given status code and message
9
+ def initialize(exit_code,message=nil)
10
+ super(message)
11
+ @exit_code = exit_code
12
+ end
13
+ end
14
+
15
+ # Thrown by certain methods when an externally-called command exits nonzero
16
+ class FailedCommandError < Error
17
+
18
+ # The command that caused the failure
19
+ attr_reader :command
20
+
21
+ # exit_code:: exit code of the command that caused this
22
+ # command:: the entire command-line that caused this
23
+ # custom_error_message:: an error message to show the user instead of the boilerplate one. Useful
24
+ # for allowing this exception to bubble up and exit the program, but to give
25
+ # the user something actionable.
26
+ def initialize(exit_code,command,custom_error_message = nil)
27
+ error_message = String(custom_error_message).empty? ? "Command '#{command}' exited #{exit_code}" : custom_error_message
28
+ super(exit_code,error_message)
29
+ @command = command
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # Base for any ExecutionStrategy implementation. Currently, this is nothing more than an interface
4
+ # specification.
5
+ class Base
6
+ # Executes the command and returns the results back. This
7
+ # should do no logging or other logic other than to execute the
8
+ # command and return the required results. If command is an
9
+ # array, use exec directly bypassing any tokenization, shell or
10
+ # otherwise; otherwise use the normal shell interpretation of
11
+ # the command string.
12
+ #
13
+ # command:: the command-line to run, as an Array or a String
14
+ #
15
+ # Returns an array of size 3:
16
+ # <tt>[0]</tt>:: The standard output of the command as a String, never nil
17
+ # <tt>[1]</tt>:: The standard error output of the command as a String, never nil
18
+ # <tt>[2]</tt>:: A Process::Status-like objects that responds to <tt>exitstatus</tt> which returns
19
+ # the exit code of the command (e.g. 0 for success).
20
+ def run_command(command)
21
+ subclass_must_implement!
22
+ end
23
+
24
+ # Returns the class that, if caught by calling #run_command, represents the underlying command
25
+ # not existing. For example, in MRI Ruby, if you try to execute a non-existent command,
26
+ # you get a Errno::ENOENT.
27
+ def exception_meaning_command_not_found
28
+ subclass_must_implement!
29
+ end
30
+ protected
31
+ def subclass_must_implement!; raise "subclass must implement"; end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # <b>OptparsePlus Internal - treat as private</b>
4
+ #
5
+ # OptparsePlus::ExecutionStrategy for the JVM that uses JVM classes to run the command and get its results.
6
+ class JVM < Base
7
+ def run_command(command)
8
+ process = case command
9
+ when String then
10
+ java.lang.Runtime.get_runtime.exec(command)
11
+ else
12
+ java.lang.Runtime.get_runtime.exec(*command)
13
+ end
14
+ process.get_output_stream.close
15
+ stdout = input_stream_to_string(process.get_input_stream)
16
+ stderr = input_stream_to_string(process.get_error_stream)
17
+ exitstatus = process.wait_for
18
+ [stdout.chomp,stderr.chomp,OpenStruct.new(:exitstatus => exitstatus)]
19
+ end
20
+
21
+ def exception_meaning_command_not_found
22
+ NativeException
23
+ end
24
+
25
+ private
26
+ def input_stream_to_string(is)
27
+ ''.tap do |string|
28
+ ch = is.read
29
+ while ch != -1
30
+ string << ch
31
+ ch = is.read
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,16 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # <b>OptparsePlus Internal - treat as private</b>
4
+ #
5
+ # Base strategy for MRI rubies.
6
+ class MRI < Base
7
+ def run_command(command)
8
+ raise "subclass must implement"
9
+ end
10
+
11
+ def exception_meaning_command_not_found
12
+ Errno::ENOENT
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # <b>OptparsePlus Internal - treat as private</b>
4
+ #
5
+ # Implementation for modern Rubies that uses the built-in Open3 library
6
+ class Open_3 < MRI
7
+ def run_command(command)
8
+ stdout,stderr,status = case command
9
+ when String then Open3.capture3(command)
10
+ else Open3.capture3(*command)
11
+ end
12
+ [stdout.chomp,stderr.chomp,status]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # <b>OptparsePlus Internal - treat as private</b>
4
+ #
5
+ # ExecutionStrategy for non-modern Rubies that must rely on
6
+ # Open4 to get access to the standard output AND error.
7
+ class Open_4 < MRI
8
+ def run_command(command)
9
+ pid, stdin_io, stdout_io, stderr_io =
10
+ case command
11
+ when String then Open4::popen4(command)
12
+ else Open4::popen4(*command)
13
+ end
14
+ stdin_io.close
15
+ stdout = stdout_io.read
16
+ stderr = stderr_io.read
17
+ _ , status = Process::waitpid2(pid)
18
+ [stdout.chomp,stderr.chomp,status]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ module OptparsePlus
2
+ module ExecutionStrategy
3
+ # <b>OptparsePlus Internal - treat as private</b>
4
+ #
5
+ # For RBX; it throws a different exception when a command isn't found, so we override that here.
6
+ class RBXOpen_4 < Open_4
7
+ def exception_meaning_command_not_found
8
+ [Errno::EINVAL] + Array(super)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,40 @@
1
+ module OptparsePlus
2
+ # Provides #exit_now! and #help_now!. You might mix this into your business logic classes if they will
3
+ # need to exit the program with a human-readable error message.
4
+ module ExitNow
5
+ def self.included(k)
6
+ k.extend(self)
7
+ end
8
+ # Call this to exit the program immediately
9
+ # with the given error code and message.
10
+ #
11
+ # +exit_code+:: exit status you'd like to exit with
12
+ # +message+:: message to display to the user explaining the problem
13
+ #
14
+ # If +exit_code+ is a String and +message+ is omitted, +exit_code+ will be used as the message
15
+ # and the actual exit code will be 1.
16
+ #
17
+ # === Examples
18
+ #
19
+ # exit_now!(4,"Oh noes!")
20
+ # # => exit app with status 4 and show the user "Oh noes!" on stderr
21
+ # exit_now!("Oh noes!")
22
+ # # => exit app with status 1 and show the user "Oh noes!" on stderr
23
+ # exit_now!(4)
24
+ # # => exit app with status 4 and dont' give the user a message (how rude of you)
25
+ def exit_now!(exit_code,message=nil)
26
+ if exit_code.kind_of?(String) && message.nil?
27
+ raise OptparsePlus::Error.new(1,exit_code)
28
+ else
29
+ raise OptparsePlus::Error.new(exit_code,message)
30
+ end
31
+ end
32
+
33
+ # Exit the program as if the user made an error invoking your app, providing
34
+ # them the message as well as printing the help. This is useful if
35
+ # you have complex UI validation that can't be done by OptionParser.
36
+ def help_now!(message)
37
+ raise OptionParser::ParseError.new(message)
38
+ end
39
+ end
40
+ end