thor 0.16.0 → 0.17.0

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.
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)
OSZAR »