visionmedia-commander 1.2.2 → 2.4.2
Sign up to get free protection for your applications and to get access to all the features.
- 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! }
|