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.
@@ -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
@@ -1,7 +1,7 @@
1
1
 
2
2
  module Commander
3
- MAJOR = 1
4
- MINOR = 2
5
- TINY = 2
6
- VERSION = [MAJOR, MINOR, TINY].join('.')
3
+ module VERSION #:nodoc:
4
+ MAJOR, MINOR, TINY = [2, 4, 2]
5
+ STRING = [MAJOR, MINOR, TINY].join '.'
6
+ end
7
7
  end
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(File.expand_path(File.dirname(__FILE__)))
24
+ $:.unshift File.dirname(__FILE__)
3
25
 
4
- require 'commander/commander'
26
+ require 'rubygems'
5
27
  require 'highline/import'
6
-
7
- class Object
8
- include Commander
9
- end
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! }