methadone-rehab 1.9.2
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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +66 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +201 -0
- data/README.rdoc +179 -0
- data/Rakefile +98 -0
- data/TODO.md +3 -0
- data/bin/methadone +157 -0
- data/features/bootstrap.feature +169 -0
- data/features/license.feature +43 -0
- data/features/multilevel_commands.feature +125 -0
- data/features/readme.feature +26 -0
- data/features/rspec_support.feature +27 -0
- data/features/step_definitions/bootstrap_steps.rb +47 -0
- data/features/step_definitions/license_steps.rb +30 -0
- data/features/step_definitions/readme_steps.rb +26 -0
- data/features/step_definitions/version_steps.rb +4 -0
- data/features/support/env.rb +26 -0
- data/features/version.feature +17 -0
- data/lib/methadone.rb +15 -0
- data/lib/methadone/argv_parser.rb +50 -0
- data/lib/methadone/cli.rb +124 -0
- data/lib/methadone/cli_logger.rb +133 -0
- data/lib/methadone/cli_logging.rb +138 -0
- data/lib/methadone/cucumber.rb +174 -0
- data/lib/methadone/error.rb +32 -0
- data/lib/methadone/execution_strategy/base.rb +34 -0
- data/lib/methadone/execution_strategy/jvm.rb +37 -0
- data/lib/methadone/execution_strategy/mri.rb +16 -0
- data/lib/methadone/execution_strategy/open_3.rb +16 -0
- data/lib/methadone/execution_strategy/open_4.rb +22 -0
- data/lib/methadone/execution_strategy/rbx_open_4.rb +12 -0
- data/lib/methadone/exit_now.rb +40 -0
- data/lib/methadone/main.rb +1039 -0
- data/lib/methadone/process_status.rb +45 -0
- data/lib/methadone/sh.rb +223 -0
- data/lib/methadone/version.rb +3 -0
- data/methadone-rehab.gemspec +32 -0
- data/templates/full/.gitignore.erb +4 -0
- data/templates/full/README.rdoc.erb +25 -0
- data/templates/full/Rakefile.erb +74 -0
- data/templates/full/_license_head.txt.erb +2 -0
- data/templates/full/apache_LICENSE.txt.erb +203 -0
- data/templates/full/bin/executable.erb +47 -0
- data/templates/full/custom_LICENSE.txt.erb +0 -0
- data/templates/full/features/executable.feature.erb +13 -0
- data/templates/full/features/step_definitions/executable_steps.rb.erb +1 -0
- data/templates/full/features/support/env.rb.erb +16 -0
- data/templates/full/gplv2_LICENSE.txt.erb +14 -0
- data/templates/full/gplv3_LICENSE.txt.erb +15 -0
- data/templates/full/mit_LICENSE.txt.erb +7 -0
- data/templates/multicommand/bin/executable.erb +52 -0
- data/templates/multicommand/lib/command.rb.erb +40 -0
- data/templates/multicommand/lib/commands.rb.erb +7 -0
- data/templates/rspec/spec/something_spec.rb.erb +5 -0
- data/templates/test_unit/test/tc_something.rb.erb +7 -0
- data/test/base_test.rb +20 -0
- data/test/command_for_tests.sh +7 -0
- data/test/execution_strategy/test_base.rb +24 -0
- data/test/execution_strategy/test_jvm.rb +77 -0
- data/test/execution_strategy/test_mri.rb +32 -0
- data/test/execution_strategy/test_open_3.rb +70 -0
- data/test/execution_strategy/test_open_4.rb +86 -0
- data/test/execution_strategy/test_rbx_open_4.rb +25 -0
- data/test/test_cli_logger.rb +219 -0
- data/test/test_cli_logging.rb +243 -0
- data/test/test_exit_now.rb +37 -0
- data/test/test_main.rb +1213 -0
- data/test/test_multi.rb +405 -0
- data/test/test_sh.rb +404 -0
- metadata +321 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Methadone
|
|
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 Methadone
|
|
2
|
+
module ExecutionStrategy
|
|
3
|
+
# <b>Methadone Internal - treat as private</b>
|
|
4
|
+
#
|
|
5
|
+
# Methadone::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 Methadone
|
|
2
|
+
module ExecutionStrategy
|
|
3
|
+
# <b>Methadone 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 Methadone
|
|
2
|
+
module ExecutionStrategy
|
|
3
|
+
# <b>Methadone 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 Methadone
|
|
2
|
+
module ExecutionStrategy
|
|
3
|
+
# <b>Methadone 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 Methadone
|
|
2
|
+
module ExecutionStrategy
|
|
3
|
+
# <b>Methadone 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 Methadone
|
|
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 Methadone::Error.new(1,exit_code)
|
|
28
|
+
else
|
|
29
|
+
raise Methadone::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
|
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
begin
|
|
4
|
+
Module.const_get('BasicObject')
|
|
5
|
+
# We are 1.9.x
|
|
6
|
+
rescue NameError
|
|
7
|
+
BasicObject = Object
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module Methadone
|
|
11
|
+
# Include this module to gain access to the "canonical command-line app structure"
|
|
12
|
+
# DSL. This is a *very* lightweight layer on top of what you might
|
|
13
|
+
# normally write that gives you just a bit of help to keep your code structured
|
|
14
|
+
# in a sensible way. You can use as much or as little as you want, though
|
|
15
|
+
# you must at least use #main to get any benefits.
|
|
16
|
+
#
|
|
17
|
+
# Further, you must provide access to a logger via a method named
|
|
18
|
+
# #logger. If you include Methadone::CLILogging, this will be done for you
|
|
19
|
+
#
|
|
20
|
+
# You also get a more expedient interface to OptionParser as well
|
|
21
|
+
# as checking for required arguments to your app. For example, if
|
|
22
|
+
# we want our app to accept a negatable switch named "switch", a flag
|
|
23
|
+
# named "flag", and two arguments "needed" (which is required)
|
|
24
|
+
# and "maybe" which is optional, we can do the following:
|
|
25
|
+
#
|
|
26
|
+
# #!/usr/bin/env ruby
|
|
27
|
+
#
|
|
28
|
+
# require 'methadone'
|
|
29
|
+
#
|
|
30
|
+
# class App
|
|
31
|
+
# include Methadone::Main
|
|
32
|
+
# include Methadone::CLILogging
|
|
33
|
+
#
|
|
34
|
+
# main do |needed, maybe|
|
|
35
|
+
# options[:switch] => true or false, based on command line
|
|
36
|
+
# options[:flag] => value of flag passed on command line
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# # Proxy to an OptionParser instance's on method
|
|
40
|
+
# on("--[no]-switch")
|
|
41
|
+
# on("--flag VALUE")
|
|
42
|
+
#
|
|
43
|
+
# arg :needed
|
|
44
|
+
# arg :maybe, :optional
|
|
45
|
+
#
|
|
46
|
+
# defaults_from_env_var SOME_VAR
|
|
47
|
+
# defaults_from_config_file '.my_app.rc'
|
|
48
|
+
#
|
|
49
|
+
# go!
|
|
50
|
+
# end
|
|
51
|
+
#
|
|
52
|
+
# Our app then acts as follows:
|
|
53
|
+
#
|
|
54
|
+
# $ our_app
|
|
55
|
+
# # => parse error: 'needed' is required
|
|
56
|
+
# $ our_app foo
|
|
57
|
+
# # => succeeds; "maybe" in main is nil
|
|
58
|
+
# $ our_app --flag foo
|
|
59
|
+
# # => options[:flag] has the value "foo"
|
|
60
|
+
# $ SOME_VAR='--flag foo' our_app
|
|
61
|
+
# # => options[:flag] has the value "foo"
|
|
62
|
+
# $ SOME_VAR='--flag foo' our_app --flag bar
|
|
63
|
+
# # => options[:flag] has the value "bar"
|
|
64
|
+
#
|
|
65
|
+
# Note that we've done all of this inside a class that we called +App+. This isn't strictly
|
|
66
|
+
# necessary, and you can just +include+ Methadone::Main and Methadone::CLILogging at the root
|
|
67
|
+
# of your +bin+ file if you like. This is somewhat unsafe, because +self+ inside the +bin+
|
|
68
|
+
# file is Object, and any methods you create (or cause to be created via +include+) will be
|
|
69
|
+
# present on *every* object. This can cause odd problems, so it's recommended that you
|
|
70
|
+
# *not* do this.
|
|
71
|
+
#
|
|
72
|
+
# Subcommands
|
|
73
|
+
# -----------
|
|
74
|
+
#
|
|
75
|
+
# In order to promote modularity and maintainability, complex command line
|
|
76
|
+
# applications should be broken up into subcommands. Subcommands are just
|
|
77
|
+
# like regular Methadone applications, except you don't put a go! call in it.
|
|
78
|
+
# It will be run in by the base methadone app class. Likewise, subcommands
|
|
79
|
+
# can have subcommands of their own.
|
|
80
|
+
#
|
|
81
|
+
# In order to tell a Methadone app class that it has subcommands, use the
|
|
82
|
+
# command method, which takes a hash with the command name as a key and the
|
|
83
|
+
# command class as the value. Multiple subcommands can be specified in a
|
|
84
|
+
# single call, or as separate calls.
|
|
85
|
+
#
|
|
86
|
+
# #!/usr/bin/env ruby
|
|
87
|
+
#
|
|
88
|
+
# require 'methadone'
|
|
89
|
+
#
|
|
90
|
+
# class MySubcommand
|
|
91
|
+
# include Methadone::Main
|
|
92
|
+
# include Methadone::CLILogging
|
|
93
|
+
#
|
|
94
|
+
# on '-f','--foo BAR', 'Some option'
|
|
95
|
+
# arg 'something', :required, "Description","defaults: value"
|
|
96
|
+
#
|
|
97
|
+
# main do |something|
|
|
98
|
+
# # stuff
|
|
99
|
+
# end
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
# class App
|
|
103
|
+
# include Methadone::Main
|
|
104
|
+
# include Methadone::CLILogging
|
|
105
|
+
#
|
|
106
|
+
# command "do" => MySubcommand
|
|
107
|
+
#
|
|
108
|
+
# go!
|
|
109
|
+
# end
|
|
110
|
+
#
|
|
111
|
+
# Apps that have subcommands (currently) don't support arguments and don't
|
|
112
|
+
# need to supply a main, as it doesn't get called. This may change in a
|
|
113
|
+
# future version of Methadone. Options to the app can modify the +options+
|
|
114
|
+
# contents will impactful to the subcommand as it receives those option
|
|
115
|
+
# values as the base for its options.
|
|
116
|
+
#
|
|
117
|
+
module Main
|
|
118
|
+
include Methadone::ExitNow
|
|
119
|
+
include Methadone::ARGVParser
|
|
120
|
+
|
|
121
|
+
def self.included(k)
|
|
122
|
+
k.extend(self)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Declare the main method for your app.
|
|
126
|
+
# This allows you to specify the general logic of your
|
|
127
|
+
# app at the top of your bin file, but can rely on any methods
|
|
128
|
+
# or other code that you define later.
|
|
129
|
+
#
|
|
130
|
+
# For example, suppose you want to process a set of files, but
|
|
131
|
+
# wish to determine that list from another method to keep your
|
|
132
|
+
# code clean.
|
|
133
|
+
#
|
|
134
|
+
# #!/usr/bin/env ruby -w
|
|
135
|
+
#
|
|
136
|
+
# require 'methadone'
|
|
137
|
+
#
|
|
138
|
+
# include Methadone::Main
|
|
139
|
+
#
|
|
140
|
+
# main do
|
|
141
|
+
# files_to_process.each do |file|
|
|
142
|
+
# # process file
|
|
143
|
+
# end
|
|
144
|
+
# end
|
|
145
|
+
#
|
|
146
|
+
# def files_to_process
|
|
147
|
+
# # return list of files
|
|
148
|
+
# end
|
|
149
|
+
#
|
|
150
|
+
# go!
|
|
151
|
+
#
|
|
152
|
+
# The block can accept any parameters, and unparsed arguments
|
|
153
|
+
# from the command line will be passed.
|
|
154
|
+
#
|
|
155
|
+
# *Note*: #go! will modify +ARGV+ to remove any known options and
|
|
156
|
+
# arguments. If there are any values left over, they will remain available
|
|
157
|
+
# in +ARGV+. This behaviour is different from 1.x versions of Methadone,
|
|
158
|
+
# which emptied +ARGV+ completely
|
|
159
|
+
#
|
|
160
|
+
# To run this method, call #go!
|
|
161
|
+
def main(&block)
|
|
162
|
+
@main_block = block
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Configure the auto-handling of StandardError exceptions caught
|
|
166
|
+
# from calling go!.
|
|
167
|
+
#
|
|
168
|
+
# leak:: if true, go! will *not* catch StandardError exceptions, but
|
|
169
|
+
# instead allow them to bubble up. If false, they will be caught
|
|
170
|
+
# and handled as normal. This does *not* affect Methadone::Error
|
|
171
|
+
# exceptions; those will NOT leak through.
|
|
172
|
+
#
|
|
173
|
+
# leak_exceptions only needs to be set once; since it is stored as a
|
|
174
|
+
# class variable, all classes that include this module will handle
|
|
175
|
+
# exceptions the same way.
|
|
176
|
+
def leak_exceptions(leak)
|
|
177
|
+
@@leak_exceptions = leak
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Print the usage help if the command is run without any options or
|
|
181
|
+
# arguments.
|
|
182
|
+
def help_if_bare
|
|
183
|
+
@default_help = true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Set the name of the environment variable where users can place default
|
|
187
|
+
# options for your app. Omit this to disable the feature.
|
|
188
|
+
def defaults_from_env_var(env_var)
|
|
189
|
+
@env_var = env_var
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Set the path to the file where defaults can be configured.
|
|
193
|
+
#
|
|
194
|
+
# The format of this file can be either a simple string of options, like what goes
|
|
195
|
+
# in the environment variable (see #defaults_from_env_var), or YAML, in which case
|
|
196
|
+
# it should be a hash where keys are the option names, and values their defaults.
|
|
197
|
+
#
|
|
198
|
+
# Relative paths will be expanded relative to the user's home directory.
|
|
199
|
+
#
|
|
200
|
+
# filename:: path to the file. If relative, will look in user's HOME directory.
|
|
201
|
+
# If absolute, this is the absolute path to where the file should be.
|
|
202
|
+
def defaults_from_config_file(filename,options={})
|
|
203
|
+
@rc_file = File.expand_path(filename, ENV['HOME'])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Start your command-line app, exiting appropriately when
|
|
207
|
+
# complete.
|
|
208
|
+
#
|
|
209
|
+
# This *will* exit your program when it completes. If your
|
|
210
|
+
# #main block evaluates to an integer, that value will be sent
|
|
211
|
+
# to Kernel#exit, otherwise, this will exit with 0
|
|
212
|
+
#
|
|
213
|
+
# If the command-line options couldn't be parsed, this
|
|
214
|
+
# will exit with 64 and whatever message OptionParser provided.
|
|
215
|
+
#
|
|
216
|
+
# If a required argument (see #arg) is not found, this exits with
|
|
217
|
+
# 64 and a message about that missing argument.
|
|
218
|
+
#
|
|
219
|
+
def go!(parent=nil)
|
|
220
|
+
|
|
221
|
+
if @default_help and ARGV.empty?
|
|
222
|
+
puts opts.to_s
|
|
223
|
+
exit 64 # sysexits.h exit code EX_USAGE
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Get stuff from parent, if there
|
|
227
|
+
set_parent(parent)
|
|
228
|
+
|
|
229
|
+
setup_defaults
|
|
230
|
+
opts.post_setup
|
|
231
|
+
|
|
232
|
+
if opts.commands.empty?
|
|
233
|
+
opts.parse!
|
|
234
|
+
opts.check_args!
|
|
235
|
+
opts.check_option_usage!
|
|
236
|
+
result = call_main
|
|
237
|
+
else
|
|
238
|
+
opts.parse_to_command! # Leaves unknown args and options in once it encounters a non-option.
|
|
239
|
+
opts.check_option_usage!
|
|
240
|
+
if opts.selected_command
|
|
241
|
+
result = call_provider
|
|
242
|
+
else
|
|
243
|
+
logger.error "You must specify a command"
|
|
244
|
+
puts ""
|
|
245
|
+
puts opts.help
|
|
246
|
+
exit 64
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
if result.kind_of? Fixnum
|
|
251
|
+
exit result
|
|
252
|
+
else
|
|
253
|
+
exit 0
|
|
254
|
+
end
|
|
255
|
+
rescue OptionParser::ParseError => ex
|
|
256
|
+
logger.error ex.message
|
|
257
|
+
puts
|
|
258
|
+
puts opts.help
|
|
259
|
+
exit 64 # Linux standard for bad command line
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Returns an OptionParser that you can use
|
|
263
|
+
# to declare your command-line interface. Generally, you
|
|
264
|
+
# won't use this and will use #on directly, but this allows
|
|
265
|
+
# you to have complete control of option parsing.
|
|
266
|
+
#
|
|
267
|
+
# The object returned has
|
|
268
|
+
# an additional feature that implements typical use of OptionParser.
|
|
269
|
+
#
|
|
270
|
+
# opts.on("--flag VALUE")
|
|
271
|
+
#
|
|
272
|
+
# Does this under the covers:
|
|
273
|
+
#
|
|
274
|
+
# opts.on("--flag VALUE") do |value|
|
|
275
|
+
# options[:flag] = value
|
|
276
|
+
# end
|
|
277
|
+
#
|
|
278
|
+
# Since, most of the time, this is all you want to do, this makes it more
|
|
279
|
+
# expedient to do so. The key that is is set in #options will be a symbol
|
|
280
|
+
# <i>and string</i> of the option name, without the leading dashes. Note
|
|
281
|
+
# that if you use multiple option names, a key will be generated for each.
|
|
282
|
+
# Further, if you use the negatable form, only the positive key will be set,
|
|
283
|
+
# e.g. for <tt>--[no-]verbose</tt>, only <tt>:verbose</tt> will be set (to
|
|
284
|
+
# true or false).
|
|
285
|
+
#
|
|
286
|
+
# As an example, this declaration:
|
|
287
|
+
#
|
|
288
|
+
# opts.on("-f VALUE", "--flag")
|
|
289
|
+
#
|
|
290
|
+
# And this command-line invocation:
|
|
291
|
+
#
|
|
292
|
+
# $ my_app -f foo
|
|
293
|
+
#
|
|
294
|
+
# Will result in all of these forms returning the String "foo":
|
|
295
|
+
# * <tt>options['f']</tt>
|
|
296
|
+
# * <tt>options[:f]</tt>
|
|
297
|
+
# * <tt>options['flag']</tt>
|
|
298
|
+
# * <tt>options[:flag]</tt>
|
|
299
|
+
#
|
|
300
|
+
# Further, any one of those keys can be used to determine the default value for the option.
|
|
301
|
+
#
|
|
302
|
+
# Playing well with others
|
|
303
|
+
# ------------------------
|
|
304
|
+
#
|
|
305
|
+
# Sometimes you need the user to specify groups of options, or sometimes
|
|
306
|
+
# one option cannot be used in conjunction with another option. While
|
|
307
|
+
# OptionParser does not natively support this, options defined with
|
|
308
|
+
# Methadone's +on+ method does so by using the following hash arguments:
|
|
309
|
+
#
|
|
310
|
+
# :excludes => <optID>
|
|
311
|
+
# :requires => <optID>
|
|
312
|
+
#
|
|
313
|
+
# The optID can be any of the keys that an option would create in the
|
|
314
|
+
# options hash. You can even specify multiple options by using an array of
|
|
315
|
+
# optIDs:
|
|
316
|
+
#
|
|
317
|
+
# :excludes => [:f, "another-option"]
|
|
318
|
+
#
|
|
319
|
+
# If you specify both an option and another option that excludes that
|
|
320
|
+
# option, an error is logged. Only one side of an exclusion needs to be
|
|
321
|
+
# specified.
|
|
322
|
+
#
|
|
323
|
+
# If you use an option, but do not use an option it requires, an error will
|
|
324
|
+
# be logged. Order of the options do not matter.
|
|
325
|
+
#
|
|
326
|
+
def opts
|
|
327
|
+
@option_parser ||= OptionParserProxy.new(OptionParser.new,options)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Calls the +on+ method of #opts with the given arguments (see RDoc for #opts for the additional
|
|
331
|
+
# help provided).
|
|
332
|
+
def on(*args,&block)
|
|
333
|
+
opts.on(*args,&block)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Calls the +command+ method of #opts with the given arguments (see RDoc
|
|
337
|
+
# for #opts for the additional help provided). Commands are special args
|
|
338
|
+
# that take their own options and other arguments.
|
|
339
|
+
def command(*args)
|
|
340
|
+
opts.command(*args)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Sets the name of an arguments your app accepts.
|
|
344
|
+
# +arg_name+:: name of the argument to appear in documentation
|
|
345
|
+
# This will be converted into a String and used to create
|
|
346
|
+
# the banner (unless you have overridden the banner)
|
|
347
|
+
# +options+:: list (not Hash) of options:
|
|
348
|
+
# <tt>:required</tt>:: this arg is required (this is the default)
|
|
349
|
+
# <tt>:optional</tt>:: this arg is optional
|
|
350
|
+
# <tt>:one</tt>:: only one of this arg should be supplied (default)
|
|
351
|
+
# <tt>:many</tt>:: many of this arg may be supplied, but at least one is required
|
|
352
|
+
# <tt>:any</tt>:: any number, include zero, may be supplied
|
|
353
|
+
# A string:: if present, this will be documentation for the
|
|
354
|
+
# argument and appear in the help. Multiple strings will be
|
|
355
|
+
# listed on multiple lines
|
|
356
|
+
# A Regexp:: Argument values must match the regexp, or an error will be raised.
|
|
357
|
+
# An Array:: Argument values must be found in the array, or an error will be raised.
|
|
358
|
+
#
|
|
359
|
+
# As of version 2.0, best effort is made to ensure values are assigned to
|
|
360
|
+
# your arguments as needed. :required and :many options will take one
|
|
361
|
+
# value if possible, and the first greedy argument (:many or :any) will
|
|
362
|
+
# consume any unallocated count of values remaining in ARGV. Value
|
|
363
|
+
# assignment still goes left to right, but allocation counts are determined
|
|
364
|
+
# by needs of each argument. Filtering rules do not play a part in
|
|
365
|
+
# determining if a value can be allocated to an argument.
|
|
366
|
+
#
|
|
367
|
+
# Greedy arguments that do not receive any values will hold an empty
|
|
368
|
+
# array, while non-greedy arguments that do not receive a value will be
|
|
369
|
+
# nil.
|
|
370
|
+
def arg(arg_name,*options)
|
|
371
|
+
opts.arg(arg_name,*options)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Set the description of your app for inclusion in the help output.
|
|
375
|
+
# +desc+:: a short, one-line description of your app
|
|
376
|
+
def description(desc=nil)
|
|
377
|
+
opts.description(desc)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Returns a Hash that you can use to store or retrieve options
|
|
381
|
+
# parsed from the command line. When you put values in here, if you do so
|
|
382
|
+
# *before* you've declared your command-line interface via #on, the value
|
|
383
|
+
# will be used in the docstring to indicate it is the default.
|
|
384
|
+
# You can use either a String or a Symbol and, after #go! is called and
|
|
385
|
+
# the command-line is parsed, the values will be available as both
|
|
386
|
+
# a String and a Symbol.
|
|
387
|
+
#
|
|
388
|
+
# Example
|
|
389
|
+
#
|
|
390
|
+
# main do
|
|
391
|
+
# puts options[:foo] # put the value of --foo that the user provided
|
|
392
|
+
# end
|
|
393
|
+
#
|
|
394
|
+
# options[:foo] = "bar" # set "bar" as the default value for --foo, which
|
|
395
|
+
# # will cause us to include "(default: bar)" in the
|
|
396
|
+
# # docstring
|
|
397
|
+
#
|
|
398
|
+
# on("--foo FOO","Sets the foo")
|
|
399
|
+
# go!
|
|
400
|
+
#
|
|
401
|
+
def options
|
|
402
|
+
@options ||= {}
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def global_options
|
|
406
|
+
(@parent.nil? ? {} : @parent.global_options).merge(
|
|
407
|
+
opts.global_options
|
|
408
|
+
)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Set the version of your app so it appears in the
|
|
412
|
+
# banner. This also adds --version as an option to your app which,
|
|
413
|
+
# when used, will act just like --help (see version_options to control this)
|
|
414
|
+
#
|
|
415
|
+
# version:: the current version of your app. Should almost always be
|
|
416
|
+
# YourApp::VERSION, where the module YourApp should've been generated
|
|
417
|
+
# by the bootstrap script
|
|
418
|
+
# version_options:: controls how the version option behaves. If this is a string,
|
|
419
|
+
# then the string will be used as documentation for the --version flag.
|
|
420
|
+
# If a Hash, more configuration is available:
|
|
421
|
+
# custom_docs:: the string to document the --version flag if you don't like the default
|
|
422
|
+
# compact:: if true, --version will just show the app name and version - no help
|
|
423
|
+
# format:: if provided, this can give limited control over the format of the compact
|
|
424
|
+
# version string. It should be a printf-style string and will be given
|
|
425
|
+
# two options: the first is the CLI app name, and the second is the version string
|
|
426
|
+
def version(version,version_options={})
|
|
427
|
+
opts.version(version)
|
|
428
|
+
if version_options.kind_of?(Symbol)
|
|
429
|
+
case version_options
|
|
430
|
+
when :terse
|
|
431
|
+
version_options = {
|
|
432
|
+
:custom_docs => "Show version",
|
|
433
|
+
:format => '%0.0s%s',
|
|
434
|
+
:compact => true
|
|
435
|
+
}
|
|
436
|
+
when :basic
|
|
437
|
+
version_options = {
|
|
438
|
+
:custom_docs => "Show version info",
|
|
439
|
+
:compact => true
|
|
440
|
+
}
|
|
441
|
+
else
|
|
442
|
+
version_options = version_options.to_s
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if version_options.kind_of?(String)
|
|
447
|
+
version_options = { :custom_docs => version_options }
|
|
448
|
+
end
|
|
449
|
+
version_options[:custom_docs] ||= "Show help/version info"
|
|
450
|
+
version_options[:format] ||= "%s version %s"
|
|
451
|
+
opts.on("--version",version_options[:custom_docs]) do
|
|
452
|
+
if version_options[:compact]
|
|
453
|
+
puts version_options[:format] % [::File.basename($0),version]
|
|
454
|
+
else
|
|
455
|
+
puts opts.to_s
|
|
456
|
+
end
|
|
457
|
+
exit 0
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
private
|
|
462
|
+
|
|
463
|
+
# Reset internal state - mostly useful for tests
|
|
464
|
+
def reset!
|
|
465
|
+
@options = nil
|
|
466
|
+
@option_parser = nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def setup_defaults
|
|
470
|
+
add_defaults_to_docs
|
|
471
|
+
set_defaults_from_rc_file
|
|
472
|
+
normalize_defaults
|
|
473
|
+
set_defaults_from_env_var
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def set_parent(parent)
|
|
477
|
+
@parent = parent
|
|
478
|
+
if parent
|
|
479
|
+
@options.merge!(parent.global_options)
|
|
480
|
+
opts.extend_help_from_parent(parent.opts)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def add_defaults_to_docs
|
|
485
|
+
|
|
486
|
+
# Remove any pre-existing separator text
|
|
487
|
+
opts.top.list.reject! {|v| v.is_a? String}
|
|
488
|
+
|
|
489
|
+
if @env_var && @rc_file
|
|
490
|
+
opts.separator ''
|
|
491
|
+
opts.separator 'Default values can be placed in:'
|
|
492
|
+
opts.separator ''
|
|
493
|
+
opts.separator " #{@env_var} environment variable, as a String of options"
|
|
494
|
+
opts.separator " #{@rc_file} with contents either a String of options "
|
|
495
|
+
spaces = (0..@rc_file.length).reduce('') { |a,_| a << ' ' }
|
|
496
|
+
opts.separator " #{spaces}or a YAML-encoded Hash"
|
|
497
|
+
elsif @env_var
|
|
498
|
+
opts.separator ''
|
|
499
|
+
opts.separator "Default values can be placed in the #{@env_var} environment variable"
|
|
500
|
+
elsif @rc_file
|
|
501
|
+
opts.separator ''
|
|
502
|
+
opts.separator "Default values can be placed in #{@rc_file}"
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def set_defaults_from_env_var
|
|
507
|
+
if @env_var
|
|
508
|
+
parse_string_for_argv(ENV[@env_var]).each do |arg|
|
|
509
|
+
::ARGV.unshift(arg)
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def set_defaults_from_rc_file
|
|
515
|
+
# TODO: Specify defaults for each command
|
|
516
|
+
if @rc_file && File.exists?(@rc_file)
|
|
517
|
+
File.open(@rc_file) do |file|
|
|
518
|
+
parsed = YAML::load(file)
|
|
519
|
+
if parsed.kind_of? String
|
|
520
|
+
parse_string_for_argv(parsed).each do |arg|
|
|
521
|
+
::ARGV.unshift(arg)
|
|
522
|
+
end
|
|
523
|
+
elsif parsed.kind_of? Hash
|
|
524
|
+
parsed.each do |option,value|
|
|
525
|
+
options[option] = value
|
|
526
|
+
end
|
|
527
|
+
else
|
|
528
|
+
raise OptionParser::ParseError,
|
|
529
|
+
"rc file #{@rc_file} is not parseable, should be a string or YAML-encoded Hash"
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# Normalized all defaults to both string and symbol forms, so
|
|
537
|
+
# the user can access them via either means just as they would for
|
|
538
|
+
# non-defaulted options
|
|
539
|
+
def normalize_defaults
|
|
540
|
+
new_options = {}
|
|
541
|
+
options.each do |key,value|
|
|
542
|
+
unless value.nil?
|
|
543
|
+
new_options[key.to_s] = value
|
|
544
|
+
new_options[key.to_sym] = value
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
options.merge!(new_options)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Handle calling main and trapping any exceptions thrown
|
|
551
|
+
def call_main
|
|
552
|
+
# Backwards compatibility ensured by adding ::ARGV
|
|
553
|
+
# TBD: rework spec so that unspecified args need to be retrieved from ARGV directly and not just passed into main
|
|
554
|
+
@main_block.call(*(opts.args_for_main))
|
|
555
|
+
rescue Methadone::Error => ex
|
|
556
|
+
raise ex if ENV['DEBUG']
|
|
557
|
+
logger.error ex.message unless no_message? ex
|
|
558
|
+
ex.exit_code
|
|
559
|
+
rescue OptionParser::ParseError
|
|
560
|
+
raise
|
|
561
|
+
rescue => ex
|
|
562
|
+
raise ex if ENV['DEBUG']
|
|
563
|
+
raise ex if @@leak_exceptions
|
|
564
|
+
logger.error ex.message unless no_message? ex
|
|
565
|
+
70 # Linux sysexit code for internal software error
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def no_message?(exception)
|
|
569
|
+
exception.message.nil? || exception.message.strip.empty?
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def call_provider
|
|
573
|
+
command = opts.selected_command
|
|
574
|
+
opts.commands[command].send(:go!,self)
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# <b>Methadone Internal - treat as private</b>
|
|
579
|
+
#
|
|
580
|
+
# A proxy to OptionParser that intercepts #on
|
|
581
|
+
# so that we can allow a simpler interface
|
|
582
|
+
class OptionParserProxy < Object
|
|
583
|
+
# Create the proxy
|
|
584
|
+
#
|
|
585
|
+
# +option_parser+:: An OptionParser instance
|
|
586
|
+
# +options+:: a hash that will store the options
|
|
587
|
+
# set via automatic setting. The caller should
|
|
588
|
+
# retain a reference to this
|
|
589
|
+
def initialize(option_parser,options)
|
|
590
|
+
@option_parser = option_parser
|
|
591
|
+
@options = options
|
|
592
|
+
@option_defs ||= {:local => [],:global => []}
|
|
593
|
+
@option_sigs = {}
|
|
594
|
+
@options_used = []
|
|
595
|
+
@usage_rules = {}
|
|
596
|
+
@commands = {}
|
|
597
|
+
@selected_command = nil
|
|
598
|
+
@user_specified_banner = false
|
|
599
|
+
@accept_options = false
|
|
600
|
+
@args = []
|
|
601
|
+
@arg_options = {}
|
|
602
|
+
@arg_filters = {}
|
|
603
|
+
@arg_documentation = {}
|
|
604
|
+
@args_by_name = {}
|
|
605
|
+
@description = nil
|
|
606
|
+
@version = nil
|
|
607
|
+
@banner_stale = true
|
|
608
|
+
document_help
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def parent_opts=(parent_opts)
|
|
612
|
+
@parent_opts = parent_opts
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def parent_opts
|
|
616
|
+
@parent_opts || nil
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def global_options
|
|
620
|
+
global_option_defs = @option_defs.fetch(:global, nil)
|
|
621
|
+
return {} if global_option_defs.nil?
|
|
622
|
+
|
|
623
|
+
keys = global_option_defs.map {|opt_def|
|
|
624
|
+
[opt_def.long, opt_def.short].
|
|
625
|
+
flatten.
|
|
626
|
+
map {|flag| flag.sub(/^--?(\[no-\])?/,'')}.
|
|
627
|
+
map {|flag| [flag,flag.to_sym]}
|
|
628
|
+
}.flatten
|
|
629
|
+
global_hash = @options.select {|k,v| keys.include? k}
|
|
630
|
+
global_hash.is_a?(Array) ? Hash[global_hash] : global_hash # Stupid 1.8.7 => 1.9.3 API change of Hash#select
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def check_args!
|
|
634
|
+
arg_allocation_map = @args.map {|arg_name| @arg_options[arg_name].include?(:required) ? 1 : 0}
|
|
635
|
+
|
|
636
|
+
arg_count = ::ARGV.length - arg_allocation_map.reduce(0,&:+)
|
|
637
|
+
if arg_count > 0
|
|
638
|
+
@args.each.with_index do |arg_name,i|
|
|
639
|
+
if (@arg_options[arg_name] & [:many,:any]).length > 0
|
|
640
|
+
arg_allocation_map[i] += arg_count
|
|
641
|
+
break
|
|
642
|
+
elsif @arg_options[arg_name].include? :optional
|
|
643
|
+
arg_allocation_map[i] += 1
|
|
644
|
+
arg_count -= 1
|
|
645
|
+
break if arg_count == 0
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
@args.zip(arg_allocation_map).each do |arg_name,arg_count|
|
|
651
|
+
if not (@arg_options[arg_name] & [:many,:any]).empty?
|
|
652
|
+
arg_value = ::ARGV.shift(arg_count)
|
|
653
|
+
else
|
|
654
|
+
arg_value = (arg_count == 1) ? ::ARGV.shift : nil
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
if @arg_options[arg_name].include? :required and arg_value.nil?
|
|
658
|
+
message = "'#{arg_name.to_s}' is required"
|
|
659
|
+
raise ::OptionParser::ParseError,message
|
|
660
|
+
elsif @arg_options[arg_name].include?(:many) and arg_value.empty?
|
|
661
|
+
message = "at least one '#{arg_name.to_s}' is required"
|
|
662
|
+
raise ::OptionParser::ParseError,message
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
unless arg_value.nil? or arg_value.empty? or @arg_filters[arg_name].empty?
|
|
666
|
+
match = false
|
|
667
|
+
msg = ''
|
|
668
|
+
@arg_filters[arg_name].each do |filter|
|
|
669
|
+
if not (@arg_options[arg_name] & [:many,:any]).empty?
|
|
670
|
+
if filter.respond_to? :include?
|
|
671
|
+
invalid_values = (filter | arg_value) - filter
|
|
672
|
+
elsif filter.is_a? ::Regexp
|
|
673
|
+
invalid_values = arg_value - arg_value.grep(filter)
|
|
674
|
+
end
|
|
675
|
+
if invalid_values.empty?
|
|
676
|
+
match = true
|
|
677
|
+
break
|
|
678
|
+
end
|
|
679
|
+
msg = "The following value(s) were invalid: '#{invalid_values.join(' ')}'"
|
|
680
|
+
else
|
|
681
|
+
if filter.respond_to? :include?
|
|
682
|
+
if filter.include? arg_value
|
|
683
|
+
match = true
|
|
684
|
+
break
|
|
685
|
+
end
|
|
686
|
+
elsif filter.is_a?(::Regexp)
|
|
687
|
+
if arg_value =~ filter
|
|
688
|
+
match = true
|
|
689
|
+
break
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
msg = "'#{arg_value}' is invalid"
|
|
693
|
+
end
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
raise ::OptionParser::ParseError, "#{arg_name}: #{msg}" unless match
|
|
697
|
+
|
|
698
|
+
end
|
|
699
|
+
@args_by_name[arg_name] = arg_value
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def args_for_main
|
|
704
|
+
@args.map {|name| @args_by_name[name]}
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# If invoked as with OptionParser, behaves the exact same way.
|
|
708
|
+
# If invoked without a block, however, the options hash given
|
|
709
|
+
# to the constructor will be used to store
|
|
710
|
+
# the parsed command-line value. See #opts in the Main module
|
|
711
|
+
# for how that works.
|
|
712
|
+
# Returns reference to the option for exclusive and mutual
|
|
713
|
+
def on(*args,&block)
|
|
714
|
+
|
|
715
|
+
# Group together any of the hash arguments
|
|
716
|
+
(hashes, args) = args.partition {|a| a.respond_to?(:keys)}
|
|
717
|
+
on_opts = hashes.reduce({}) {|h1,h2| h1.merge(h2)}
|
|
718
|
+
|
|
719
|
+
scope = args.delete(:global) || :local
|
|
720
|
+
args = add_default_value_to_docstring(*args)
|
|
721
|
+
sig = option_signature(args)
|
|
722
|
+
opt_names = option_names(*args)
|
|
723
|
+
|
|
724
|
+
opt_names.each do |name|
|
|
725
|
+
@option_sigs[name] = sig
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
block ||= Proc.new do |value|
|
|
729
|
+
opt_names.each do |name|
|
|
730
|
+
@options[name] = value
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
wrapper = Proc.new do |value|
|
|
734
|
+
register_usage opt_names
|
|
735
|
+
block.call(value)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
opt = @option_parser.define(*args,&wrapper)
|
|
739
|
+
@option_defs[scope] << opt
|
|
740
|
+
|
|
741
|
+
set_usage_rules_for(opt_names,on_opts)
|
|
742
|
+
|
|
743
|
+
@accept_options = true
|
|
744
|
+
@banner_stale = true
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def set_usage_rules_for(names,rules_source)
|
|
748
|
+
rule_keys = [:excludes, :requires]
|
|
749
|
+
rules = Hash[rule_keys.zip(rules_source.values_at(*rule_keys))].reject{|k,v| v.nil?}
|
|
750
|
+
return if rules.empty?
|
|
751
|
+
|
|
752
|
+
names.each do |name|
|
|
753
|
+
@usage_rules[name] = rules
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
def register_usage(opt_names)
|
|
758
|
+
opt_names.each do |name|
|
|
759
|
+
@options_used << name
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def check_option_usage!
|
|
764
|
+
requirers = @options_used.select {|name| @usage_rules.fetch(name,{}).key?(:requires)}
|
|
765
|
+
requirers.each do |name|
|
|
766
|
+
required = [@usage_rules[name][:requires]].flatten
|
|
767
|
+
violation = required - @options_used
|
|
768
|
+
unless violation.empty?
|
|
769
|
+
raise OptionParser::OptionConflict.new("Missing option #{@option_sigs[violation.first]} required by option #{@option_sigs[name]}")
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
excluders = @options_used.select {|name| @usage_rules.fetch(name,{}).key?(:excludes)}
|
|
773
|
+
excluders.each do |name|
|
|
774
|
+
excluded = [@usage_rules[name][:excludes]].flatten
|
|
775
|
+
violation = (excluded & @options_used)
|
|
776
|
+
unless violation.empty?
|
|
777
|
+
raise OptionParser::OptionConflict, "#{@option_sigs[name]} cannot be used if already using #{@option_sigs[violation.first]}"
|
|
778
|
+
end
|
|
779
|
+
end
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
# Specify an acceptable command that will be hanlded by the given command provider
|
|
783
|
+
def command(provider_hash={})
|
|
784
|
+
provider_hash.each do |name,cls|
|
|
785
|
+
raise InvalidProvider.new("Provider for #{name} must respond to go!") unless cls.respond_to? :go!
|
|
786
|
+
commands[name.to_s] = cls
|
|
787
|
+
end
|
|
788
|
+
@banner_stale = true
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# Proxies to underlying OptionParser
|
|
792
|
+
def banner=(new_banner)
|
|
793
|
+
@option_parser.banner=new_banner
|
|
794
|
+
@user_specified_banner = true
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# Sets the banner to include these arg names
|
|
799
|
+
def arg(arg_name,*options)
|
|
800
|
+
options << :optional if options.include?(:any) && !options.include?(:optional)
|
|
801
|
+
options << :required unless options.include? :optional
|
|
802
|
+
options << :one unless options.include?(:any) || options.include?(:many)
|
|
803
|
+
@args << arg_name
|
|
804
|
+
@arg_options[arg_name] = options
|
|
805
|
+
@arg_documentation[arg_name]= options.select(&STRINGS_ONLY)
|
|
806
|
+
@arg_filters[arg_name] = options.select {|o| o.is_a?(Array) or o.is_a?(Range) or o.is_a?(::Regexp)}
|
|
807
|
+
@banner_stale = true
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def description(desc)
|
|
811
|
+
|
|
812
|
+
@description = desc if desc
|
|
813
|
+
@banner_stale = true
|
|
814
|
+
@description
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# Defers all calls save #on to
|
|
818
|
+
# the underlying OptionParser instance
|
|
819
|
+
def method_missing(sym,*args,&block)
|
|
820
|
+
@option_parser.send(sym,*args,&block)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def banner
|
|
824
|
+
set_banner if @banner_stale
|
|
825
|
+
@option_parser.banner
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
def help
|
|
829
|
+
set_banner if @banner_stale
|
|
830
|
+
@option_parser.to_s
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
# Since we extend Object on 1.8.x, to_s is defined and thus not proxied by method_missing
|
|
834
|
+
def to_s #::nodoc::
|
|
835
|
+
help
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
# Acess the command provider list
|
|
840
|
+
def commands
|
|
841
|
+
@commands
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def parse_to_command!
|
|
845
|
+
@option_parser.order!
|
|
846
|
+
if command_names.include? ::ARGV[0]
|
|
847
|
+
@selected_command = ::ARGV.shift
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# The selected command
|
|
852
|
+
def selected_command
|
|
853
|
+
@selected_command
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Sets the version for the banner
|
|
857
|
+
def version(version)
|
|
858
|
+
@version = version
|
|
859
|
+
@banner_stale = true
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# List the command names
|
|
863
|
+
def command_names
|
|
864
|
+
@command_names ||= commands.keys.map {|k| k.to_s}
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
# We need some documentation to appear at the end, after all OptionParser setup
|
|
868
|
+
# has occured, but before we actually start. This method serves that purpose
|
|
869
|
+
def post_setup
|
|
870
|
+
if parent_opts and not (global_opts = parent_opts.global_options_help).empty?
|
|
871
|
+
@option_parser.separator ''
|
|
872
|
+
global_opts.split("\n").each {|line| @option_parser.separator line}
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
if @commands.empty? and ! @arg_documentation.empty?
|
|
876
|
+
@option_parser.separator ''
|
|
877
|
+
@option_parser.separator "Arguments:"
|
|
878
|
+
@args.each do |arg|
|
|
879
|
+
option_tag = @arg_options[arg].include?(:optional) ? ' (optional)' : ''
|
|
880
|
+
@option_parser.separator " #{arg}#{option_tag}"
|
|
881
|
+
@arg_documentation[arg].each do |doc|
|
|
882
|
+
@option_parser.separator " #{doc}"
|
|
883
|
+
end
|
|
884
|
+
end
|
|
885
|
+
end
|
|
886
|
+
|
|
887
|
+
unless @commands.empty?
|
|
888
|
+
padding = @commands.keys.map {|name| name.to_s.length}.max + 1
|
|
889
|
+
@option_parser.separator ''
|
|
890
|
+
@option_parser.separator "Commands:"
|
|
891
|
+
@commands.each do |name,provider|
|
|
892
|
+
@option_parser.separator " #{ "%-#{padding}s" % (name.to_s+':')} #{provider.description}"
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
@option_parser.separator ''
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def extend_help_from_parent(parent_opts)
|
|
899
|
+
self.parent_opts = parent_opts
|
|
900
|
+
@banner_stale = true
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
protected
|
|
904
|
+
|
|
905
|
+
def base_usage_line
|
|
906
|
+
line = parent_opts.nil? ? "\nUsage:" : parent_opts.base_usage_line
|
|
907
|
+
cmd = parent_opts && parent_opts.selected_command
|
|
908
|
+
line += ' ' + (cmd || ::File.basename($0)).to_s
|
|
909
|
+
if selected_command && accept_global_options?
|
|
910
|
+
if parent_opts
|
|
911
|
+
line += " [options for #{cmd}]"
|
|
912
|
+
else
|
|
913
|
+
line += " [global options]"
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
line
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def global_options_help
|
|
920
|
+
msg = []
|
|
921
|
+
global_option_defs = @option_defs.fetch(:global,[])
|
|
922
|
+
unless global_option_defs.empty?
|
|
923
|
+
cmd = parent_opts && parent_opts.selected_command
|
|
924
|
+
opt_lines = [cmd.nil? ? "Global options:\n" : "Options for #{cmd}:\n"]
|
|
925
|
+
width = @option_parser.summary_width
|
|
926
|
+
indent = @option_parser.summary_indent
|
|
927
|
+
global_option_defs.each do |opt|
|
|
928
|
+
opt.summarize({},{},width,width - 1,indent) do |line|
|
|
929
|
+
opt_lines << (line.index($/, -1) ? line : line + $/)
|
|
930
|
+
end
|
|
931
|
+
end
|
|
932
|
+
msg << opt_lines.join('')
|
|
933
|
+
end
|
|
934
|
+
msg << parent_opts.global_options_help if parent_opts
|
|
935
|
+
msg.join ("\n")
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def accept_global_options?
|
|
939
|
+
! @option_defs.fetch(:global,[]).empty?
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
private
|
|
943
|
+
|
|
944
|
+
# Because there is always an option for -h, if there are subcommands, they
|
|
945
|
+
# need to show the option holder and Options prefix to differentiate
|
|
946
|
+
# between the command option an previous options.
|
|
947
|
+
def accept_options?
|
|
948
|
+
@accept_options
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def document_help
|
|
952
|
+
@option_parser.on("-h","--help","Show command line help") do
|
|
953
|
+
puts self.to_s
|
|
954
|
+
exit 0
|
|
955
|
+
end
|
|
956
|
+
@banner_stale = true
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
def add_default_value_to_docstring(*args)
|
|
960
|
+
default_value = nil
|
|
961
|
+
option_names_from(args).each do |option|
|
|
962
|
+
option = option.sub(/\A\[no-\]/,'')
|
|
963
|
+
default_value = (@options[option.to_s] || @options[option.to_sym]) if default_value.nil?
|
|
964
|
+
end
|
|
965
|
+
if default_value.nil?
|
|
966
|
+
args
|
|
967
|
+
else
|
|
968
|
+
args + ["(default: #{default_value})"]
|
|
969
|
+
end
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def option_names_from(args)
|
|
973
|
+
args.select(&STRINGS_ONLY).select { |_|
|
|
974
|
+
_ =~ /^\-/
|
|
975
|
+
}.map { |_|
|
|
976
|
+
_.gsub(/^\-+/,'').gsub(/\s.*$/,'')
|
|
977
|
+
}
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
def option_signature(args)
|
|
981
|
+
args.select(&STRINGS_ONLY).select {|s| s =~ /\A-/}.join('|')
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def set_banner
|
|
986
|
+
return if @user_specified_banner
|
|
987
|
+
return unless @banner_stale
|
|
988
|
+
|
|
989
|
+
new_banner = base_usage_line
|
|
990
|
+
new_banner += " [options]" if (@commands.empty? or parent_opts.nil?) and accept_options?
|
|
991
|
+
new_banner += " command [command options and args...]" unless @commands.empty?
|
|
992
|
+
|
|
993
|
+
if @commands.empty? and !@args.empty?
|
|
994
|
+
new_banner += " "
|
|
995
|
+
new_banner += @args.map { |arg|
|
|
996
|
+
if @arg_options[arg].include? :any
|
|
997
|
+
"[#{arg.to_s}...]"
|
|
998
|
+
elsif @arg_options[arg].include? :optional
|
|
999
|
+
"[#{arg.to_s}]"
|
|
1000
|
+
elsif @arg_options[arg].include? :many
|
|
1001
|
+
"#{arg.to_s}..."
|
|
1002
|
+
else
|
|
1003
|
+
arg.to_s
|
|
1004
|
+
end
|
|
1005
|
+
}.join(' ')
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
new_banner += "\n\n#{@description}" if @description
|
|
1009
|
+
new_banner += "\n\nv#{@version}" if @version
|
|
1010
|
+
|
|
1011
|
+
new_banner += "\n\nOptions:"
|
|
1012
|
+
@option_parser.banner=new_banner
|
|
1013
|
+
@banner_stale = false
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
def option_names(*opts_on_args,&block)
|
|
1017
|
+
opts_on_args.select(&STRINGS_ONLY).map { |arg|
|
|
1018
|
+
if arg =~ /^--\[no-\]([^-\s][^\s]*)/
|
|
1019
|
+
$1.to_sym
|
|
1020
|
+
elsif arg =~ /^--([^-\s][^\s]*)/
|
|
1021
|
+
$1.to_sym
|
|
1022
|
+
elsif arg =~ /^-([^-\s][^\s]*)/
|
|
1023
|
+
$1.to_sym
|
|
1024
|
+
else
|
|
1025
|
+
nil
|
|
1026
|
+
end
|
|
1027
|
+
}.reject(&:nil?).map {|name| [name,name.to_s]}.flatten
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
STRINGS_ONLY = lambda { |o| o.kind_of?(::String) }
|
|
1031
|
+
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
InvalidProvider = Class.new(TypeError)
|
|
1035
|
+
|
|
1036
|
+
OptionParser::OptionConflict = Class.new(OptionParser::ParseError)
|
|
1037
|
+
OptionParser::MissingRequiredOption = Class.new(OptionParser::ParseError)
|
|
1038
|
+
|
|
1039
|
+
end
|