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,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