thor 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. data/.rspec +1 -0
  2. data/.travis.yml +2 -1
  3. data/CHANGELOG.rdoc +8 -0
  4. data/Gemfile +12 -8
  5. data/lib/thor.rb +79 -10
  6. data/lib/thor/actions.rb +13 -13
  7. data/lib/thor/actions/directory.rb +29 -10
  8. data/lib/thor/actions/file_manipulation.rb +8 -2
  9. data/lib/thor/base.rb +24 -11
  10. data/lib/thor/core_ext/hash_with_indifferent_access.rb +5 -0
  11. data/lib/thor/group.rb +5 -5
  12. data/lib/thor/parser/options.rb +63 -25
  13. data/lib/thor/rake_compat.rb +3 -2
  14. data/lib/thor/runner.rb +1 -1
  15. data/lib/thor/shell/basic.rb +16 -16
  16. data/lib/thor/shell/color.rb +9 -9
  17. data/lib/thor/shell/html.rb +9 -9
  18. data/lib/thor/task.rb +2 -2
  19. data/lib/thor/version.rb +1 -1
  20. data/spec/actions/create_file_spec.rb +30 -30
  21. data/spec/actions/create_link_spec.rb +12 -12
  22. data/spec/actions/directory_spec.rb +34 -27
  23. data/spec/actions/empty_directory_spec.rb +16 -16
  24. data/spec/actions/file_manipulation_spec.rb +62 -50
  25. data/spec/actions/inject_into_file_spec.rb +18 -18
  26. data/spec/actions_spec.rb +56 -56
  27. data/spec/base_spec.rb +69 -69
  28. data/spec/core_ext/hash_with_indifferent_access_spec.rb +19 -14
  29. data/spec/core_ext/ordered_hash_spec.rb +29 -29
  30. data/spec/exit_condition_spec.rb +3 -3
  31. data/spec/fixtures/preserve/script.sh +3 -0
  32. data/spec/fixtures/script.thor +5 -0
  33. data/spec/group_spec.rb +55 -55
  34. data/spec/invocation_spec.rb +26 -26
  35. data/spec/parser/argument_spec.rb +12 -12
  36. data/spec/parser/arguments_spec.rb +12 -12
  37. data/spec/parser/option_spec.rb +47 -47
  38. data/spec/parser/options_spec.rb +137 -72
  39. data/spec/rake_compat_spec.rb +11 -11
  40. data/spec/register_spec.rb +70 -8
  41. data/spec/runner_spec.rb +38 -38
  42. data/spec/shell/basic_spec.rb +49 -37
  43. data/spec/shell/color_spec.rb +13 -13
  44. data/spec/shell/html_spec.rb +3 -3
  45. data/spec/shell_spec.rb +7 -7
  46. data/spec/spec_helper.rb +4 -0
  47. data/spec/task_spec.rb +11 -11
  48. data/spec/thor_spec.rb +161 -91
  49. data/spec/util_spec.rb +42 -42
  50. data/thor.gemspec +1 -7
  51. metadata +8 -118
  52. data/lib/thor/core_ext/dir_escape.rb +0 -0
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --color
2
+ --fail-fast
2
3
  --order random
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
1
+ bundler_args: --without development
1
2
  language: ruby
2
3
  rvm:
3
4
  - 1.8.7
4
5
  - 1.9.2
5
6
  - 1.9.3
6
- - ruby-head
7
+ - 2.0.0
7
8
  script: bundle exec thor spec
data/CHANGELOG.rdoc CHANGED
@@ -1,3 +1,11 @@
1
+ == 0.17.0, release 2013-01-24
2
+ * Add better support for tasks that accept arbitrary additional arguments (e.g. things like `bundle exec`)
3
+ * Add #stop_on_unknown_option!
4
+ * Only strip from stdin.gets if it wasn't ended with EOF
5
+ * Allow "send" as a task name
6
+ * Allow passing options as arguments after "--"
7
+ * Autoload Thor::Group
8
+
1
9
  == 0.16.0, release 2012-08-14
2
10
  * Add enum to string arguments
3
11
 
data/Gemfile CHANGED
@@ -1,15 +1,19 @@
1
- source 'https://rubygems.org'
1
+ source :rubygems
2
2
 
3
3
  gemspec
4
4
 
5
- platforms :mri_18 do
6
- gem 'ruby-debug', '>= 0.10.3'
7
- end
8
-
9
- platforms :mri_19 do
10
- gem 'ruby-debug19'
11
- end
5
+ gem 'rake', '~> 0.9'
6
+ gem 'rdoc', '~> 3.9'
12
7
 
13
8
  group :development do
14
9
  gem 'pry'
10
+ gem 'pry-debugger', :platforms => :mri_19
11
+ end
12
+
13
+ group :test do
14
+ gem 'childlabor'
15
+ gem 'fakeweb', '~> 1.3'
16
+ gem 'rspec', '~> 2.11'
17
+ gem 'rspec-mocks', :git => 'git://github.com/rspec/rspec-mocks.git'
18
+ gem 'simplecov'
15
19
  end
data/lib/thor.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require 'set'
1
2
  require 'thor/base'
2
3
 
3
4
  class Thor
@@ -8,13 +9,13 @@ class Thor
8
9
  # meth<Symbol>:: name of the default task
9
10
  #
10
11
  def default_task(meth=nil)
11
- case meth
12
- when :none
13
- @default_task = 'help'
14
- when nil
15
- @default_task ||= from_superclass(:default_task, 'help')
16
- else
17
- @default_task = meth.to_s
12
+ @default_task = case meth
13
+ when :none
14
+ 'help'
15
+ when nil
16
+ @default_task || from_superclass(:default_task, 'help')
17
+ else
18
+ meth.to_s
18
19
  end
19
20
  end
20
21
 
@@ -210,7 +211,7 @@ class Thor
210
211
 
211
212
  define_method(subcommand) do |*args|
212
213
  args, opts = Thor::Arguments.split(args)
213
- invoke subcommand_class, args, opts
214
+ invoke subcommand_class, args, opts, :invoked_via_subcommand => true
214
215
  end
215
216
  end
216
217
 
@@ -251,15 +252,83 @@ class Thor
251
252
  end
252
253
  end
253
254
 
255
+ # Stop parsing of options as soon as an unknown option or a regular
256
+ # argument is encountered. All remaining arguments are passed to the task.
257
+ # This is useful if you have a task that can receive arbitrary additional
258
+ # options, and where those additional options should not be handled by
259
+ # Thor.
260
+ #
261
+ # ==== Example
262
+ #
263
+ # To better understand how this is useful, let's consider a task that calls
264
+ # an external command. A user may want to pass arbitrary options and
265
+ # arguments to that command. The task itself also accepts some options,
266
+ # which should be handled by Thor.
267
+ #
268
+ # class_option "verbose", :type => :boolean
269
+ # stop_on_unknown_option! :exec
270
+ # check_unknown_options! :except => :exec
271
+ #
272
+ # desc "exec", "Run a shell command"
273
+ # def exec(*args)
274
+ # puts "diagnostic output" if options[:verbose]
275
+ # Kernel.exec(*args)
276
+ # end
277
+ #
278
+ # Here +exec+ can be called with +--verbose+ to get diagnostic output,
279
+ # e.g.:
280
+ #
281
+ # $ thor exec --verbose echo foo
282
+ # diagnostic output
283
+ # foo
284
+ #
285
+ # But if +--verbose+ is given after +echo+, it is passed to +echo+ instead:
286
+ #
287
+ # $ thor exec echo --verbose foo
288
+ # --verbose foo
289
+ #
290
+ # ==== Parameters
291
+ # Symbol ...:: A list of tasks that should be affected.
292
+ def stop_on_unknown_option!(*task_names)
293
+ @stop_on_unknown_option ||= Set.new
294
+ @stop_on_unknown_option.merge(task_names)
295
+ end
296
+
297
+ def stop_on_unknown_option?(task) #:nodoc:
298
+ !!@stop_on_unknown_option && @stop_on_unknown_option.include?(task.name.to_sym)
299
+ end
300
+
254
301
  protected
255
302
 
256
303
  # The method responsible for dispatching given the args.
257
304
  def dispatch(meth, given_args, given_opts, config) #:nodoc:
258
- meth ||= retrieve_task_name(given_args)
259
- task = all_tasks[normalize_task_name(meth)]
305
+ # There is an edge case when dispatching from a subcommand.
306
+ # A problem occurs invoking the default task. This case occurs
307
+ # when arguments are passed and a default task is defined, and
308
+ # the first given_args does not match the default task.
309
+ # Thor use "help" by default so we skip that case.
310
+ # Note the call to retrieve_task_name. It's called with
311
+ # given_args.dup since that method calls args.shift. Then lookup
312
+ # the task normally. If the first item in given_args is not
313
+ # a task then use the default task. The given_args will be
314
+ # intact later since dup was used.
315
+ if config[:invoked_via_subcommand] && given_args.size >= 1 && default_task != "help" && given_args.first != default_task
316
+ meth ||= retrieve_task_name(given_args.dup)
317
+ task = all_tasks[normalize_task_name(meth)]
318
+ task ||= all_tasks[normalize_task_name(default_task)]
319
+ else
320
+ meth ||= retrieve_task_name(given_args)
321
+ task = all_tasks[normalize_task_name(meth)]
322
+ end
260
323
 
261
324
  if task
262
325
  args, opts = Thor::Options.split(given_args)
326
+ if stop_on_unknown_option?(task) && !args.empty?
327
+ # given_args starts with a non-option, so we treat everything as
328
+ # ordinary arguments
329
+ args.concat opts
330
+ opts.clear
331
+ end
263
332
  else
264
333
  args, opts = given_args, nil
265
334
  task = Thor::DynamicTask.new(meth)
data/lib/thor/actions.rb CHANGED
@@ -73,13 +73,13 @@ class Thor
73
73
  #
74
74
  def initialize(args=[], options={}, config={})
75
75
  self.behavior = case config[:behavior].to_s
76
- when "force", "skip"
77
- _cleanup_options_and_set(options, config[:behavior])
78
- :invoke
79
- when "revoke"
80
- :revoke
81
- else
82
- :invoke
76
+ when "force", "skip"
77
+ _cleanup_options_and_set(options, config[:behavior])
78
+ :invoke
79
+ when "revoke"
80
+ :revoke
81
+ else
82
+ :invoke
83
83
  end
84
84
 
85
85
  super
@@ -305,12 +305,12 @@ class Thor
305
305
 
306
306
  def _cleanup_options_and_set(options, key) #:nodoc:
307
307
  case options
308
- when Array
309
- %w(--force -f --skip -s).each { |i| options.delete(i) }
310
- options << "--#{key}"
311
- when Hash
312
- [:force, :skip, "force", "skip"].each { |i| options.delete(i) }
313
- options.merge!(key => true)
308
+ when Array
309
+ %w(--force -f --skip -s).each { |i| options.delete(i) }
310
+ options << "--#{key}"
311
+ when Hash
312
+ [:force, :skip, "force", "skip"].each { |i| options.delete(i) }
313
+ options.merge!(key => true)
314
314
  end
315
315
  end
316
316
 
@@ -38,6 +38,7 @@ class Thor
38
38
  # destination<String>:: the relative path to the destination root.
39
39
  # config<Hash>:: give :verbose => false to not log the status.
40
40
  # If :recursive => false, does not look for paths recursively.
41
+ # If :mode => :preserve, preserve the file mode from the source.
41
42
  #
42
43
  # ==== Examples
43
44
  #
@@ -73,26 +74,44 @@ class Thor
73
74
  def execute!
74
75
  lookup = Util.escape_globs(source)
75
76
  lookup = config[:recursive] ? File.join(lookup, '**') : lookup
76
- lookup = File.join(lookup, '{*,.[a-z]*}')
77
+ lookup = file_level_lookup(lookup)
77
78
 
78
- Dir[lookup].sort.each do |file_source|
79
+ files(lookup).sort.each do |file_source|
79
80
  next if File.directory?(file_source)
80
81
  file_destination = File.join(given_destination, file_source.gsub(source, '.'))
81
82
  file_destination.gsub!('/./', '/')
82
83
 
83
84
  case file_source
84
- when /\.empty_directory$/
85
- dirname = File.dirname(file_destination).gsub(/\/\.$/, '')
86
- next if dirname == given_destination
87
- base.empty_directory(dirname, config)
88
- when /\.tt$/
89
- destination = base.template(file_source, file_destination[0..-4], config, &@block)
90
- else
91
- destination = base.copy_file(file_source, file_destination, config, &@block)
85
+ when /\.empty_directory$/
86
+ dirname = File.dirname(file_destination).gsub(/\/\.$/, '')
87
+ next if dirname == given_destination
88
+ base.empty_directory(dirname, config)
89
+ when /\.tt$/
90
+ destination = base.template(file_source, file_destination[0..-4], config, &@block)
91
+ else
92
+ destination = base.copy_file(file_source, file_destination, config, &@block)
92
93
  end
93
94
  end
94
95
  end
95
96
 
97
+ if RUBY_VERSION < '2.0'
98
+ def file_level_lookup(previous_lookup)
99
+ File.join(previous_lookup, '{*,.[a-z]*}')
100
+ end
101
+
102
+ def files(lookup)
103
+ Dir[lookup]
104
+ end
105
+ else
106
+ def file_level_lookup(previous_lookup)
107
+ File.join(previous_lookup, '*')
108
+ end
109
+
110
+ def files(lookup)
111
+ Dir.glob(lookup, File::FNM_DOTMATCH)
112
+ end
113
+ end
114
+
96
115
  end
97
116
  end
98
117
  end
@@ -10,7 +10,9 @@ class Thor
10
10
  # ==== Parameters
11
11
  # source<String>:: the relative path to the source root.
12
12
  # destination<String>:: the relative path to the destination root.
13
- # config<Hash>:: give :verbose => false to not log the status.
13
+ # config<Hash>:: give :verbose => false to not log the status, and
14
+ # :mode => :preserve, to preserve the file mode from the source.
15
+
14
16
  #
15
17
  # ==== Examples
16
18
  #
@@ -28,6 +30,10 @@ class Thor
28
30
  content = block.call(content) if block
29
31
  content
30
32
  end
33
+ if config[:mode] == :preserve
34
+ mode = File.stat(source).mode
35
+ chmod(destination, mode, config)
36
+ end
31
37
  end
32
38
 
33
39
  # Links the file from the relative source to the relative destination. If
@@ -245,7 +251,7 @@ class Thor
245
251
  def uncomment_lines(path, flag, *args)
246
252
  flag = flag.respond_to?(:source) ? flag.source : flag
247
253
 
248
- gsub_file(path, /^(\s*)#\s*(.*#{flag})/, '\1\2', *args)
254
+ gsub_file(path, /^(\s*)#[[:blank:]]*(.*#{flag})/, '\1\2', *args)
249
255
  end
250
256
 
251
257
  # Comment all lines matching a given regex. It will leave the space
data/lib/thor/base.rb CHANGED
@@ -10,6 +10,7 @@ require 'thor/util'
10
10
  class Thor
11
11
  autoload :Actions, 'thor/actions'
12
12
  autoload :RakeCompat, 'thor/rake_compat'
13
+ autoload :Group, 'thor/group'
13
14
 
14
15
  # Shortcuts for help.
15
16
  HELP_MAPPINGS = %w(-h -? --help -D)
@@ -58,7 +59,8 @@ class Thor
58
59
  # Let Thor::Options parse the options first, so it can remove
59
60
  # declared options from the array. This will leave us with
60
61
  # a list of arguments that weren't declared.
61
- opts = Thor::Options.new(parse_options, hash_options)
62
+ stop_on_unknown = self.class.stop_on_unknown_option? config[:current_task]
63
+ opts = Thor::Options.new(parse_options, hash_options, stop_on_unknown)
62
64
  self.options = opts.parse(array_options)
63
65
 
64
66
  # If unknown options are disallowed, make sure that none of the
@@ -74,7 +76,7 @@ class Thor
74
76
  to_parse += opts.remaining unless self.class.strict_args_position?(config)
75
77
 
76
78
  thor_args = Thor::Arguments.new(self.class.arguments)
77
- thor_args.parse(to_parse).each { |k,v| send("#{k}=", v) }
79
+ thor_args.parse(to_parse).each { |k,v| __send__("#{k}=", v) }
78
80
  @args = thor_args.remaining
79
81
  end
80
82
 
@@ -142,6 +144,13 @@ class Thor
142
144
  !!check_unknown_options
143
145
  end
144
146
 
147
+ # If true, option parsing is suspended as soon as an unknown option or a
148
+ # regular argument is encountered. All remaining arguments are passed to
149
+ # the task as regular arguments.
150
+ def stop_on_unknown_option?(task_name) #:nodoc:
151
+ false
152
+ end
153
+
145
154
  # If you want only strict string args (useful when cascading thor classes),
146
155
  # call strict_args_position! This is disabled by default to allow dynamic
147
156
  # invocations.
@@ -304,11 +313,11 @@ class Thor
304
313
  # name<String|Symbol>
305
314
  #
306
315
  def group(name=nil)
307
- case name
308
- when nil
309
- @group ||= from_superclass(:group, 'standard')
310
- else
311
- @group = name.to_s
316
+ @group = case name
317
+ when nil
318
+ @group || from_superclass(:group, 'standard')
319
+ else
320
+ name.to_s
312
321
  end
313
322
  end
314
323
 
@@ -404,9 +413,9 @@ class Thor
404
413
  # thor :my_task
405
414
  #
406
415
  def namespace(name=nil)
407
- case name
416
+ @namespace = case name
408
417
  when nil
409
- @namespace ||= Thor::Util.namespace_from_thor_class(self)
418
+ @namespace || Thor::Util.namespace_from_thor_class(self)
410
419
  else
411
420
  @namespace = name.to_s
412
421
  end
@@ -462,8 +471,12 @@ class Thor
462
471
  msg = "#{basename} #{task.name}"
463
472
  if arity
464
473
  required = arity < 0 ? (-1 - arity) : arity
465
- msg << " requires at least #{required} argument"
466
- msg << "s" if required > 1
474
+ if required == 0
475
+ msg << " should have no arguments"
476
+ else
477
+ msg << " requires at least #{required} argument"
478
+ msg << "s" if required > 1
479
+ end
467
480
  else
468
481
  msg = "call #{msg} as"
469
482
  end
@@ -45,6 +45,11 @@ class Thor
45
45
  self
46
46
  end
47
47
 
48
+ # Convert to a Hash with String keys.
49
+ def to_hash
50
+ Hash.new(default).merge!(self)
51
+ end
52
+
48
53
  protected
49
54
 
50
55
  def convert_key(key)
data/lib/thor/group.rb CHANGED
@@ -14,11 +14,11 @@ class Thor::Group
14
14
  # description<String>:: The description for this Thor::Group.
15
15
  #
16
16
  def desc(description=nil)
17
- case description
18
- when nil
19
- @desc ||= from_superclass(:desc, nil)
20
- else
21
- @desc = description
17
+ @desc = case description
18
+ when nil
19
+ @desc || from_superclass(:desc, nil)
20
+ else
21
+ description
22
22
  end
23
23
  end
24
24
 
@@ -5,27 +5,32 @@ class Thor
5
5
  EQ_RE = /^(--\w+(?:-\w+)*|-[a-z])=(.*)$/i
6
6
  SHORT_SQ_RE = /^-([a-z]{2,})$/i # Allow either -x -v or -xv style for single char args
7
7
  SHORT_NUM = /^(-[a-z])#{NUMERIC}$/i
8
+ OPTS_END = '--'.freeze
8
9
 
9
10
  # Receives a hash and makes it switches.
10
11
  def self.to_switches(options)
11
12
  options.map do |key, value|
12
13
  case value
13
- when true
14
- "--#{key}"
15
- when Array
16
- "--#{key} #{value.map{ |v| v.inspect }.join(' ')}"
17
- when Hash
18
- "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}"
19
- when nil, false
20
- ""
21
- else
22
- "--#{key} #{value.inspect}"
14
+ when true
15
+ "--#{key}"
16
+ when Array
17
+ "--#{key} #{value.map{ |v| v.inspect }.join(' ')}"
18
+ when Hash
19
+ "--#{key} #{value.map{ |k,v| "#{k}:#{v}" }.join(' ')}"
20
+ when nil, false
21
+ ""
22
+ else
23
+ "--#{key} #{value.inspect}"
23
24
  end
24
25
  end.join(" ")
25
26
  end
26
27
 
27
28
  # Takes a hash of Thor::Option and a hash with defaults.
28
- def initialize(hash_options={}, defaults={})
29
+ #
30
+ # If +stop_on_unknown+ is true, #parse will stop as soon as it encounters
31
+ # an unknown option or a regular argument.
32
+ def initialize(hash_options={}, defaults={}, stop_on_unknown=false)
33
+ @stop_on_unknown = stop_on_unknown
29
34
  options = hash_options.values
30
35
  super(options)
31
36
 
@@ -50,15 +55,30 @@ class Thor
50
55
  @extra
51
56
  end
52
57
 
58
+ def peek
59
+ return super unless @parsing_options
60
+
61
+ result = super
62
+ if result == OPTS_END
63
+ shift
64
+ @parsing_options = false
65
+ super
66
+ else
67
+ result
68
+ end
69
+ end
70
+
53
71
  def parse(args)
54
72
  @pile = args.dup
73
+ @parsing_options = true
55
74
 
56
75
  while peek
57
- match, is_switch = current_is_switch?
58
- shifted = shift
76
+ if parsing_options?
77
+ match, is_switch = current_is_switch?
78
+ shifted = shift
59
79
 
60
- if is_switch
61
- case shifted
80
+ if is_switch
81
+ case shifted
62
82
  when SHORT_SQ_RE
63
83
  unshift($1.split('').map { |f| "-#{f}" })
64
84
  next
@@ -67,16 +87,23 @@ class Thor
67
87
  switch = $1
68
88
  when LONG_RE, SHORT_RE
69
89
  switch = $1
90
+ end
91
+
92
+ switch = normalize_switch(switch)
93
+ option = switch_option(switch)
94
+ @assigns[option.human_name] = parse_peek(switch, option)
95
+ elsif @stop_on_unknown
96
+ @extra << shifted
97
+ @extra << shift while peek
98
+ break
99
+ elsif match
100
+ @extra << shifted
101
+ @extra << shift while peek && peek !~ /^-/
102
+ else
103
+ @extra << shifted
70
104
  end
71
-
72
- switch = normalize_switch(switch)
73
- option = switch_option(switch)
74
- @assigns[option.human_name] = parse_peek(switch, option)
75
- elsif match
76
- @extra << shifted
77
- @extra << shift while peek && peek !~ /^-/
78
105
  else
79
- @extra << shifted
106
+ @extra << shift
80
107
  end
81
108
  end
82
109
 
@@ -95,8 +122,10 @@ class Thor
95
122
 
96
123
  protected
97
124
 
98
- # Returns true if the current value in peek is a registered switch.
125
+ # Check if the current value in peek is a registered switch.
99
126
  #
127
+ # Two booleans are returned. The first is true if the current value
128
+ # starts with a hyphen; the second is true if it is a registered switch.
100
129
  def current_is_switch?
101
130
  case peek
102
131
  when LONG_RE, SHORT_RE, EQ_RE, SHORT_NUM
@@ -117,6 +146,10 @@ class Thor
117
146
  end
118
147
  end
119
148
 
149
+ def current_is_value?
150
+ peek && (!parsing_options? || super)
151
+ end
152
+
120
153
  def switch?(arg)
121
154
  switch_option(normalize_switch(arg))
122
155
  end
@@ -135,6 +168,11 @@ class Thor
135
168
  (@shorts[arg] || arg).tr('_', '-')
136
169
  end
137
170
 
171
+ def parsing_options?
172
+ peek
173
+ @parsing_options
174
+ end
175
+
138
176
  # Parse boolean values which can be given as --foo=true, --foo or --no-foo.
139
177
  #
140
178
  def parse_boolean(switch)
@@ -156,7 +194,7 @@ class Thor
156
194
  # Parse the value at the peek analyzing if it requires an input or not.
157
195
  #
158
196
  def parse_peek(switch, option)
159
- if current_is_switch_formatted? || last?
197
+ if parsing_options? && (current_is_switch_formatted? || last?)
160
198
  if option.boolean?
161
199
  # No problem for boolean types
162
200
  elsif no_or_skip?(switch)