visionmedia-commander 1.2.2 → 2.4.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.
- data/History.rdoc +85 -0
- data/Manifest +33 -0
- data/README.rdoc +178 -0
- data/Rakefile +8 -30
- data/Todo.rdoc +12 -0
- data/bin/commander +23 -26
- data/commander.gemspec +39 -0
- data/lib/commander/command.rb +227 -31
- data/lib/commander/core_ext/array.rb +24 -0
- data/lib/commander/core_ext/kernel.rb +12 -0
- data/lib/commander/core_ext/object.rb +13 -0
- data/lib/commander/core_ext/string.rb +33 -0
- data/lib/commander/core_ext.rb +5 -0
- data/lib/commander/fileutils.rb +30 -0
- data/lib/commander/help_formatters/base.rb +38 -0
- data/lib/commander/help_formatters/terminal/command_help.erb +35 -0
- data/lib/commander/help_formatters/terminal/help.erb +21 -0
- data/lib/commander/help_formatters/terminal.rb +27 -0
- data/lib/commander/help_formatters.rb +6 -0
- data/lib/commander/import.rb +28 -0
- data/lib/commander/runner.rb +219 -0
- data/lib/commander/user_interaction.rb +202 -0
- data/lib/commander/version.rb +4 -4
- data/lib/commander.rb +46 -6
- data/spec/commander_spec.rb +208 -27
- data/spec/help_formatter_spec.rb +31 -0
- data/spec/spec_helper.rb +25 -0
- data/tasks/docs.rake +13 -0
- data/tasks/gemspec.rake +3 -0
- data/tasks/spec.rake +25 -0
- metadata +65 -25
- data/History.txt +0 -48
- data/Manifest.txt +0 -17
- data/README.txt +0 -76
- data/lib/commander/commander.rb +0 -35
- data/lib/commander/help_generators/default.rb +0 -126
- data/lib/commander/help_generators.rb +0 -2
- data/lib/commander/manager.rb +0 -129
- data/spec/all_spec.rb +0 -6
- data/spec/manager_spec.rb +0 -19
@@ -0,0 +1,219 @@
|
|
1
|
+
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module Commander
|
5
|
+
class Runner
|
6
|
+
|
7
|
+
#--
|
8
|
+
# Exceptions
|
9
|
+
#++
|
10
|
+
|
11
|
+
class CommandError < StandardError; end
|
12
|
+
class InvalidCommandError < CommandError; end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Commands within the runner.
|
16
|
+
|
17
|
+
attr_reader :commands
|
18
|
+
|
19
|
+
##
|
20
|
+
# Global options.
|
21
|
+
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
##
|
25
|
+
# Initialize a new command runner.
|
26
|
+
|
27
|
+
def initialize args = ARGV
|
28
|
+
@args = args
|
29
|
+
@commands, @options = {}, {}
|
30
|
+
@program = {
|
31
|
+
:help_formatter => Commander::HelpFormatter::Terminal,
|
32
|
+
:int_message => "\nProcess interrupted",
|
33
|
+
}
|
34
|
+
create_default_commands
|
35
|
+
parse_global_options
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Run the command parsing and execution process immediately.
|
40
|
+
|
41
|
+
def run!
|
42
|
+
%w[ name version description ].each { |k| ensure_program_key_set k.to_sym }
|
43
|
+
case
|
44
|
+
when options[:version] : $terminal.say "#{@program[:name]} #{@program[:version]}"
|
45
|
+
when options[:help] : get_command(:help).run
|
46
|
+
else active_command.run args_without_command
|
47
|
+
end
|
48
|
+
rescue InvalidCommandError
|
49
|
+
$terminal.say "invalid command. Use --help for more information"
|
50
|
+
rescue OptionParser::InvalidOption,
|
51
|
+
OptionParser::InvalidArgument,
|
52
|
+
OptionParser::MissingArgument => e
|
53
|
+
$terminal.say e
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Assign program information.
|
58
|
+
#
|
59
|
+
# === Examples:
|
60
|
+
#
|
61
|
+
# # Set data
|
62
|
+
# program :name, 'Commander'
|
63
|
+
# program :version, Commander::VERSION
|
64
|
+
# program :description, 'Commander utility program.'
|
65
|
+
# program :help, 'Copyright', '2008 TJ Holowaychuk'
|
66
|
+
# program :help, 'Anything', 'You want'
|
67
|
+
# program :int_message 'Bye bye!'
|
68
|
+
#
|
69
|
+
# # Get data
|
70
|
+
# program :name # => 'Commander'
|
71
|
+
#
|
72
|
+
# === Keys:
|
73
|
+
#
|
74
|
+
# :name (required) Program name
|
75
|
+
# :version (required) Program version triple, ex: '0.0.1'
|
76
|
+
# :description (required) Program description
|
77
|
+
# :help_formatter Defaults to Commander::HelpFormatter::Terminal
|
78
|
+
# :help Allows addition of arbitrary global help blocks
|
79
|
+
# :int_message Message to display when interrupted (CTRL + C)
|
80
|
+
#
|
81
|
+
|
82
|
+
def program key, *args
|
83
|
+
if key == :help and !args.empty?
|
84
|
+
@program[:help] ||= {}
|
85
|
+
@program[:help][args.first] = args[1]
|
86
|
+
else
|
87
|
+
@program[key] = *args unless args.empty?
|
88
|
+
@program[key] if args.empty?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Generate a command object instance using a block
|
94
|
+
# evaluated with the command as its scope.
|
95
|
+
#
|
96
|
+
# === Examples:
|
97
|
+
#
|
98
|
+
# command :my_command do |c|
|
99
|
+
# c.when_called do |args|
|
100
|
+
# # Code
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# === See:
|
105
|
+
#
|
106
|
+
# * Commander::Command
|
107
|
+
# * Commander::Runner#add_command
|
108
|
+
#
|
109
|
+
|
110
|
+
def command name, &block
|
111
|
+
command = Commander::Command.new(name) and yield command
|
112
|
+
add_command command
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Add a command object to this runner.
|
117
|
+
|
118
|
+
def add_command command
|
119
|
+
@commands[command.name] = command
|
120
|
+
end
|
121
|
+
|
122
|
+
##
|
123
|
+
# Get a command object if available or nil.
|
124
|
+
|
125
|
+
def get_command name
|
126
|
+
@commands[name.to_s] or raise InvalidCommandError, "Invalid command '#{name || "nil"}'", caller
|
127
|
+
end
|
128
|
+
|
129
|
+
##
|
130
|
+
# Check if a command exists.
|
131
|
+
|
132
|
+
def command_exists? name
|
133
|
+
@commands[name.to_s]
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Get active command within arguments passed to this runner.
|
138
|
+
#
|
139
|
+
# === See:
|
140
|
+
#
|
141
|
+
# * Commander::Runner#parse_global_options
|
142
|
+
#
|
143
|
+
|
144
|
+
def active_command
|
145
|
+
@_active_command ||= get_command(command_name_from_args)
|
146
|
+
end
|
147
|
+
|
148
|
+
##
|
149
|
+
# Attemps to locate command from @args. Supports multi-word
|
150
|
+
# command names.
|
151
|
+
|
152
|
+
def command_name_from_args
|
153
|
+
@args.delete_switches.inject do |name, arg|
|
154
|
+
return name if command_exists? name
|
155
|
+
name << " #{arg}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
##
|
160
|
+
# Help formatter instance.
|
161
|
+
|
162
|
+
def help_formatter
|
163
|
+
@_help_formatter ||= @program[:help_formatter].new self
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
##
|
169
|
+
# Creates default commands such as 'help' which is
|
170
|
+
# essentially the same as using the --help switch.
|
171
|
+
|
172
|
+
def create_default_commands
|
173
|
+
command :help do |c|
|
174
|
+
c.syntax = "command help <sub_command>"
|
175
|
+
c.summary = "Display help documentation"
|
176
|
+
c.description = "Display help documentation for the global or sub commands"
|
177
|
+
c.example "Display global help", "command help"
|
178
|
+
c.example "Display help for 'foo'", "command help foo"
|
179
|
+
c.when_called do |args, options|
|
180
|
+
gen = help_formatter
|
181
|
+
if args.empty?
|
182
|
+
$terminal.say gen.render
|
183
|
+
else
|
184
|
+
$terminal.say gen.render_command(get_command(args.shift.to_sym))
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Parse global command options.
|
192
|
+
#
|
193
|
+
# These options are used by commander itself
|
194
|
+
# as well as allowing your program to specify
|
195
|
+
# global commands such as '--verbose'.
|
196
|
+
#
|
197
|
+
# TODO: allow 'option' method for global program
|
198
|
+
#
|
199
|
+
|
200
|
+
def parse_global_options
|
201
|
+
opts = OptionParser.new
|
202
|
+
opts.on("--help") { @options[:help] = true }
|
203
|
+
opts.on("--version") { @options[:version] = true }
|
204
|
+
opts.parse! @args.dup
|
205
|
+
rescue OptionParser::InvalidOption
|
206
|
+
# Ignore invalid options since options will be further
|
207
|
+
# parsed by our sub commands.
|
208
|
+
end
|
209
|
+
|
210
|
+
def ensure_program_key_set key #:nodoc:
|
211
|
+
raise CommandError, "Program #{key} required (use #program method)" if (@program[key].nil? || @program[key].empty?)
|
212
|
+
end
|
213
|
+
|
214
|
+
def args_without_command #:nodoc:
|
215
|
+
@args.join(' ').sub(/^[\s]*#{active_command.name}/, '').split
|
216
|
+
end
|
217
|
+
|
218
|
+
end
|
219
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
|
2
|
+
module Commander
|
3
|
+
|
4
|
+
##
|
5
|
+
# = User Interaction
|
6
|
+
#
|
7
|
+
# Commander's user interacton module mixes in common
|
8
|
+
# methods which extend HighLine's functionality such
|
9
|
+
# as a unified +password+ method rather than calling
|
10
|
+
# +ask+ directly.
|
11
|
+
|
12
|
+
module UI
|
13
|
+
|
14
|
+
##
|
15
|
+
# Format used within #log.
|
16
|
+
|
17
|
+
LOG_FORMAT = "%15s %s"
|
18
|
+
|
19
|
+
##
|
20
|
+
# Ask the user for a password. Specify a custom
|
21
|
+
# _msg_ other than 'Password: ' or override the
|
22
|
+
# default +mask+ of '*'.
|
23
|
+
|
24
|
+
def password msg = "Password: ", mask = '*'
|
25
|
+
pass = ask(msg) { |q| q.echo = mask }
|
26
|
+
pass = password msg, mask if pass.empty?
|
27
|
+
pass
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# 'Log' an _action_ to the terminal. This is typically used
|
32
|
+
# for verbose output regarding actions performed. For example:
|
33
|
+
#
|
34
|
+
# create path/to/file.rb
|
35
|
+
# remove path/to/old_file.rb
|
36
|
+
# remove path/to/old_file2.rb
|
37
|
+
#
|
38
|
+
# To alter this format simply change the Commander::UI::LOG_FORMAT
|
39
|
+
# constant to whatever you like.
|
40
|
+
|
41
|
+
def log action, *args
|
42
|
+
say LOG_FORMAT % [action, args.join(' ')]
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# = Progress Bar
|
47
|
+
#
|
48
|
+
# Terminal progress bar utility. In its most basic form
|
49
|
+
# requires that the developer specifies when the bar should
|
50
|
+
# be incremented:
|
51
|
+
#
|
52
|
+
# uris = %w[
|
53
|
+
# http://vision-media.ca
|
54
|
+
# http://yahoo.com
|
55
|
+
# http://google.com
|
56
|
+
# ]
|
57
|
+
#
|
58
|
+
# bar = Commander::UI::ProgressBar.new uris.length, options
|
59
|
+
# threads = []
|
60
|
+
# uris.each do |uri|
|
61
|
+
# threads << Thread.new do
|
62
|
+
# begin
|
63
|
+
# res = open uri
|
64
|
+
# bar.inc :uri => uri
|
65
|
+
# rescue Exception => e
|
66
|
+
# bar.inc :uri => "#{uri} failed"
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
# threads.each { |t| t.join }
|
71
|
+
#
|
72
|
+
# The Kernel method #progress is also available, below are
|
73
|
+
# single and multi-threaded examples:
|
74
|
+
#
|
75
|
+
# progress uris, :width => 10 do |uri|
|
76
|
+
# res = open uri
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# threads = uris.collect { |uri| Thread.new { res = open uri } }
|
80
|
+
# progress threads, :progress_char => '-' do |thread|
|
81
|
+
# thread.join
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
|
85
|
+
class ProgressBar
|
86
|
+
|
87
|
+
##
|
88
|
+
# Creates a new progress bar.
|
89
|
+
#
|
90
|
+
# === Options:
|
91
|
+
#
|
92
|
+
# :title Title, defaults to "Progress"
|
93
|
+
# :width Width of :progress_bar
|
94
|
+
# :progress_char Progress character, defaults to "="
|
95
|
+
# :incomplete_char Incomplete bar character, defaults to '.'
|
96
|
+
# :format Defaults to ":title |:progress_bar| :percent_complete% complete "
|
97
|
+
# :tokens Additional tokens replaced within the format string
|
98
|
+
# :complete_message Defaults to "Process complete"
|
99
|
+
#
|
100
|
+
# === Tokens:
|
101
|
+
#
|
102
|
+
# :title
|
103
|
+
# :percent_complete
|
104
|
+
# :progress_bar
|
105
|
+
# :current
|
106
|
+
# :remaining
|
107
|
+
# :total
|
108
|
+
# :output
|
109
|
+
# :time_elapsed
|
110
|
+
# :time_remaining
|
111
|
+
#
|
112
|
+
|
113
|
+
def initialize total, options = {}
|
114
|
+
@total, @options, @current, @start = total, options, 0, Time.now
|
115
|
+
@options = {
|
116
|
+
:title => "Progress",
|
117
|
+
:width => 25,
|
118
|
+
:progress_char => "=",
|
119
|
+
:incomplete_char => ".",
|
120
|
+
:complete_message => "Process complete",
|
121
|
+
:format => ":title |:progress_bar| :percent_complete% complete ",
|
122
|
+
:output => $stderr,
|
123
|
+
:tokens => {},
|
124
|
+
}.merge! options
|
125
|
+
show
|
126
|
+
end
|
127
|
+
|
128
|
+
##
|
129
|
+
# Output the progress bar.
|
130
|
+
|
131
|
+
def show
|
132
|
+
unless @current >= (@total + 1)
|
133
|
+
erase_line
|
134
|
+
percent = (@current * 100) / @total
|
135
|
+
elapsed = Time.now - @start
|
136
|
+
remaining = @total - @current
|
137
|
+
tokens = {
|
138
|
+
:title => @options[:title],
|
139
|
+
:percent_complete => percent,
|
140
|
+
:progress_bar => (@options[:progress_char] * (@options[:width] * percent / 100)).ljust(@options[:width], @options[:incomplete_char]),
|
141
|
+
:current => @current,
|
142
|
+
:remaining => remaining,
|
143
|
+
:total => @total,
|
144
|
+
:time_elapsed => "%0.2fs" % [elapsed],
|
145
|
+
:time_remaining => "%0.2fs" % [(elapsed / @current) * remaining],
|
146
|
+
}.merge! @options[:tokens]
|
147
|
+
if completed?
|
148
|
+
@options[:output].print @options[:complete_message].tokenize(tokens) << "\n" if @options[:complete_message].is_a? String
|
149
|
+
else
|
150
|
+
@options[:output].print @options[:format].tokenize(tokens)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Weither or not the operation has completed.
|
157
|
+
|
158
|
+
def completed?
|
159
|
+
@current == @total
|
160
|
+
end
|
161
|
+
alias :finished? :completed?
|
162
|
+
|
163
|
+
##
|
164
|
+
# Increment progress. Optionally pass _tokens_ which
|
165
|
+
# can be displayed in the output format.
|
166
|
+
|
167
|
+
def increment tokens = {}
|
168
|
+
@current += 1
|
169
|
+
@options[:tokens].merge! tokens if tokens.is_a? Hash
|
170
|
+
show
|
171
|
+
end
|
172
|
+
alias :inc :increment
|
173
|
+
|
174
|
+
##
|
175
|
+
# Erase previous terminal line.
|
176
|
+
|
177
|
+
def erase_line
|
178
|
+
@options[:output].print "\r\e[K"
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Output progress while iterating _enum_.
|
183
|
+
#
|
184
|
+
# === Example:
|
185
|
+
#
|
186
|
+
# uris = %[ http://vision-media.ca http://google.com ]
|
187
|
+
# ProgressBar.progress uris, :format => "Remaining: :time_remaining" do |uri|
|
188
|
+
# res = open uri
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# === See:
|
192
|
+
#
|
193
|
+
# * Kernel#progress
|
194
|
+
|
195
|
+
def self.progress enum, options = {}, &block
|
196
|
+
threads = []
|
197
|
+
bar = ProgressBar.new enum.length, options
|
198
|
+
enum.each { |v| bar.inc yield(v) }
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
data/lib/commander/version.rb
CHANGED
data/lib/commander.rb
CHANGED
@@ -1,9 +1,49 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2008 TJ Holowaychuk <tj@vision-media.ca>
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
#++
|
1
23
|
|
2
|
-
$:.unshift
|
24
|
+
$:.unshift File.dirname(__FILE__)
|
3
25
|
|
4
|
-
require '
|
26
|
+
require 'rubygems'
|
5
27
|
require 'highline/import'
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
28
|
+
require 'commander/version'
|
29
|
+
require 'commander/user_interaction'
|
30
|
+
require 'commander/fileutils'
|
31
|
+
require 'commander/core_ext'
|
32
|
+
require 'commander/runner'
|
33
|
+
require 'commander/command'
|
34
|
+
require 'commander/help_formatters'
|
35
|
+
require 'commander/import'
|
36
|
+
|
37
|
+
$command_runner = Commander::Runner.new
|
38
|
+
|
39
|
+
# Highline terminal settings
|
40
|
+
$terminal.wrap_at = HighLine::SystemExtensions.terminal_size.first - 10 rescue 80
|
41
|
+
|
42
|
+
# Display friendly interruption message
|
43
|
+
trap 'INT' do
|
44
|
+
say program(:int_message)
|
45
|
+
exit
|
46
|
+
end
|
47
|
+
|
48
|
+
# Auto-execute command runner
|
49
|
+
at_exit { $command_runner.run! }
|