gli 2.5.6 → 2.6.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.travis.yml +1 -0
  2. data/Gemfile +1 -1
  3. data/features/step_definitions/todo_steps.rb +4 -0
  4. data/features/todo.feature +22 -0
  5. data/features/todo_legacy.feature +128 -0
  6. data/lib/gli.rb +3 -1
  7. data/lib/gli/app.rb +10 -2
  8. data/lib/gli/app_support.rb +39 -31
  9. data/lib/gli/command.rb +3 -2
  10. data/lib/gli/command_finder.rb +41 -0
  11. data/lib/gli/command_line_token.rb +0 -4
  12. data/lib/gli/command_support.rb +37 -64
  13. data/lib/gli/commands/doc.rb +12 -2
  14. data/lib/gli/commands/help.rb +3 -0
  15. data/lib/gli/commands/help_modules/command_help_format.rb +22 -5
  16. data/lib/gli/commands/scaffold.rb +1 -1
  17. data/lib/gli/exceptions.rb +17 -5
  18. data/lib/gli/gli_option_block_parser.rb +84 -0
  19. data/lib/gli/gli_option_parser.rb +116 -96
  20. data/lib/gli/option_parser_factory.rb +42 -10
  21. data/lib/gli/option_parsing_result.rb +19 -0
  22. data/lib/gli/version.rb +1 -1
  23. data/test/apps/todo/bin/todo +2 -0
  24. data/test/apps/todo/lib/todo/commands/make.rb +52 -0
  25. data/test/apps/todo_legacy/Gemfile +2 -0
  26. data/test/apps/todo_legacy/README.rdoc +6 -0
  27. data/test/apps/todo_legacy/Rakefile +23 -0
  28. data/test/apps/todo_legacy/bin/todo +61 -0
  29. data/test/apps/todo_legacy/lib/todo/commands/create.rb +24 -0
  30. data/test/apps/todo_legacy/lib/todo/commands/list.rb +63 -0
  31. data/test/apps/todo_legacy/lib/todo/commands/ls.rb +47 -0
  32. data/test/apps/todo_legacy/lib/todo/version.rb +3 -0
  33. data/test/apps/todo_legacy/test/tc_nothing.rb +14 -0
  34. data/test/apps/todo_legacy/todo.gemspec +23 -0
  35. data/test/apps/todo_legacy/todo.rdoc +5 -0
  36. data/test/tc_command.rb +84 -59
  37. data/test/{tc_compount_command.rb → tc_compound_command.rb} +0 -0
  38. data/test/tc_flag.rb +0 -1
  39. data/test/tc_gli.rb +2 -2
  40. data/test/tc_help.rb +11 -3
  41. data/test/tc_subcommand_parsing.rb +104 -0
  42. data/test/tc_subcommands.rb +1 -0
  43. data/test/tc_switch.rb +0 -1
  44. data/test/test_helper.rb +5 -0
  45. metadata +74 -13
  46. data/lib/gli/copy_options_to_aliases.rb +0 -33
data/.travis.yml CHANGED
@@ -13,3 +13,4 @@ branches:
13
13
  - 'master'
14
14
  - 'gli-2'
15
15
  - 'quick-bugifxes'
16
+ - 'fully-nested-subcommands'
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
@@ -1,3 +1,7 @@
1
+ Given /^todo_legacy's bin directory is in my path/ do
2
+ add_to_path(File.expand_path(File.join(File.dirname(__FILE__),'..','..','test','apps','todo_legacy','bin')))
3
+ end
4
+
1
5
  Given /^todo's bin directory is in my path/ do
2
6
  add_to_path(File.expand_path(File.join(File.dirname(__FILE__),'..','..','test','apps','todo','bin')))
3
7
  end
@@ -47,6 +47,7 @@ Feature: The todo app has a nice user interface
47
47
  initconfig - Initialize the config file using current global options
48
48
  list - List things, such as tasks or contexts
49
49
  ls - LS things, such as tasks or contexts
50
+ make -
50
51
  second -
51
52
  third -
52
53
  """
@@ -59,6 +60,7 @@ Feature: The todo app has a nice user interface
59
60
  When I successfully run `todo help -c`
60
61
  Then the output should contain:
61
62
  """
63
+ _doc
62
64
  ch2
63
65
  chained
64
66
  chained2
@@ -68,8 +70,10 @@ Feature: The todo app has a nice user interface
68
70
  initconfig
69
71
  list
70
72
  ls
73
+ make
71
74
  new
72
75
  second
76
+ third
73
77
  """
74
78
 
75
79
  Scenario: Help completion mode for partial match
@@ -126,6 +130,7 @@ Feature: The todo app has a nice user interface
126
130
  create, new - Create a new task or context
127
131
  list - List things, such as tasks or contexts
128
132
  ls - LS things, such as tasks or contexts
133
+ make -
129
134
  third -
130
135
  first -
131
136
  second -
@@ -346,6 +351,23 @@ Feature: The todo app has a nice user interface
346
351
  tasks - List tasks
347
352
  """
348
353
 
354
+ Scenario: Access to the complex command-line options for nested subcommands
355
+ Given I run `todo make -l MAKE task -l TASK bug -l BUG other args`
356
+ Then the exit status should be 0
357
+ And the stdout should contain:
358
+ """
359
+ new task bug
360
+ other,args
361
+ BUG
362
+
363
+ BUG
364
+ TASK
365
+ TASK
366
+
367
+ MAKE
368
+ MAKE
369
+
370
+ """
349
371
 
350
372
  Scenario: Init Config makes a reasonable config file
351
373
  Given a clean home directory
@@ -0,0 +1,128 @@
1
+ Feature: The todo app is backwards compatible with legacy subcommand parsing
2
+ As a user of GLI
3
+ My apps with subcommands should support the old, legacy way, by default
4
+
5
+ Background:
6
+ Given I have GLI installed
7
+ And GLI's libs are in my path
8
+ And my terminal size is "80x24"
9
+ And todo_legacy's bin directory is in my path
10
+
11
+ Scenario: Help completion mode for subcommands
12
+ When I successfully run `todo help -c list`
13
+ Then the output should contain:
14
+ """
15
+ contexts
16
+ tasks
17
+ """
18
+
19
+ Scenario: Help completion mode partial match for subcommands
20
+ When I successfully run `todo help -c list con`
21
+ Then the output should contain:
22
+ """
23
+ contexts
24
+ """
25
+
26
+ Scenario Outline: Getting Help for a top level command of todo
27
+ When I successfully run `todo <help_invocation>`
28
+ Then the output should contain:
29
+ """
30
+ NAME
31
+ list - List things, such as tasks or contexts
32
+
33
+ SYNOPSIS
34
+ todo [global options] list [command options] [--flag arg] [-x arg] [tasks]
35
+ todo [global options] list [command options] [--otherflag arg] [-b] [-f|--foobar] contexts
36
+
37
+ DESCRIPTION
38
+ List a whole lot of things that you might be keeping track of in your
39
+ overall todo list.
40
+
41
+ This is your go-to place or finding all of the things that you might have
42
+ stored in your todo databases.
43
+
44
+ COMMAND OPTIONS
45
+ -l, --[no-]long - Show long form
46
+
47
+ COMMANDS
48
+ contexts - List contexts
49
+ tasks - List tasks (default)
50
+ """
51
+
52
+ Examples:
53
+ | help_invocation |
54
+ | help list |
55
+ | list -h |
56
+ | list --help |
57
+
58
+
59
+ Scenario: Getting Help for a sub command of todo list
60
+ When I successfully run `todo help list tasks`
61
+ Then the output should contain:
62
+ """
63
+ NAME
64
+ tasks - List tasks
65
+
66
+ SYNOPSIS
67
+ todo [global options] list tasks [command options]
68
+ todo [global options] list tasks [command options] open
69
+
70
+ DESCRIPTION
71
+ Lists all of your tasks that you have, in varying orders, and all that
72
+ stuff. Yes, this is long, but I need a long description.
73
+
74
+ COMMAND OPTIONS
75
+ --flag=arg - (default: none)
76
+ -x arg - blah blah crud x whatever (default: none)
77
+
78
+ COMMANDS
79
+ <default> - list all tasks
80
+ open - list open tasks
81
+ """
82
+
83
+ Scenario: Getting Help for a sub command with no command options
84
+ When I successfully run `todo help new`
85
+ Then the output should contain:
86
+ """
87
+ NAME
88
+ create - Create a new task or context
89
+
90
+ SYNOPSIS
91
+ todo [global options] create [command options]
92
+ todo [global options] create [command options] contexts [context_name]
93
+ todo [global options] create [command options] tasks task_name[, task_name]*
94
+
95
+ COMMANDS
96
+ <default> - Makes a new task
97
+ contexts - Make a new context
98
+ tasks - Make a new task
99
+ """
100
+ And the output should not contain "COMMAND OPTIONS"
101
+
102
+ Scenario: Running ls w/out subcommand shows help and an error
103
+ When I run `todo ls`
104
+ Then the exit status should not be 0
105
+ And the stderr should contain "error: Command 'ls' requires a subcommand"
106
+ And the stdout should contain:
107
+ """
108
+ NAME
109
+ ls - LS things, such as tasks or contexts
110
+
111
+ SYNOPSIS
112
+ todo [global options] ls [command options] [-b] [-f|--foobar] contexts
113
+ todo [global options] ls [command options] [-x arg] tasks
114
+
115
+ DESCRIPTION
116
+ List a whole lot of things that you might be keeping track of in your
117
+ overall todo list.
118
+
119
+ This is your go-to place or finding all of the things that you might have
120
+ stored in your todo databases.
121
+
122
+ COMMAND OPTIONS
123
+ -l, --[no-]long - Show long form
124
+
125
+ COMMANDS
126
+ contexts - List contexts
127
+ tasks - List tasks
128
+ """
data/lib/gli.rb CHANGED
@@ -1,4 +1,7 @@
1
+ require 'gli/command_finder.rb'
2
+ require 'gli/gli_option_block_parser.rb'
1
3
  require 'gli/option_parser_factory.rb'
4
+ require 'gli/option_parsing_result.rb'
2
5
  require 'gli/gli_option_parser.rb'
3
6
  require 'gli/app_support.rb'
4
7
  require 'gli/app.rb'
@@ -6,7 +9,6 @@ require 'gli/command_support.rb'
6
9
  require 'gli/command.rb'
7
10
  require 'gli/command_line_token.rb'
8
11
  require 'gli/command_line_option.rb'
9
- require 'gli/copy_options_to_aliases.rb'
10
12
  require 'gli/exceptions.rb'
11
13
  require 'gli/flag.rb'
12
14
  require 'gli/options.rb'
data/lib/gli/app.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'etc'
2
2
  require 'optparse'
3
- require 'gli/copy_options_to_aliases'
4
3
  require 'gli/dsl'
5
4
  require 'pathname'
6
5
 
@@ -9,7 +8,6 @@ module GLI
9
8
  # Git's does, in that you specify global options, a command name, command
10
9
  # specific options, and then command arguments.
11
10
  module App
12
- include CopyOptionsToAliases
13
11
  include DSL
14
12
  include AppSupport
15
13
 
@@ -261,6 +259,16 @@ module GLI
261
259
  @default_command = command.to_sym
262
260
  end
263
261
 
262
+ # How to handle subcommand options. In general, you want to set this to +:normal+, which
263
+ # treats each subcommand as establishing its own namespace for options. This is what
264
+ # the scaffolding should generate, but it is *not* what GLI 2.5.x and lower apps had as a default.
265
+ # To maintain backwards compatibility, the default is +:legacy+, which is that all subcommands of
266
+ # a particular command share a namespace for options, making it impossible for two subcommands
267
+ # to have options of the same name.
268
+ def subcommand_option_handling(handling_strategy)
269
+ @subcommand_option_handling_strategy = handling_strategy
270
+ end
271
+
264
272
  private
265
273
 
266
274
  def load_commands(path)
@@ -27,6 +27,7 @@ module GLI
27
27
  @post_block = false
28
28
  @default_command = :help
29
29
  @around_block = nil
30
+ @subcommand_option_handling_strategy = :legacy
30
31
  clear_nexts
31
32
  end
32
33
 
@@ -52,25 +53,31 @@ module GLI
52
53
  # Returns a number that would be a reasonable exit code
53
54
  def run(args) #:nodoc:
54
55
  args = args.dup if @preserve_argv
55
- command = nil
56
+ the_command = nil
56
57
  begin
57
58
  override_defaults_based_on_config(parse_config)
58
59
 
59
60
  add_help_switch_if_needed(switches)
60
61
 
61
- global_options,command,options,arguments = GLIOptionParser.new(commands,flags,switches,accepts,@default_command).parse_options(args)
62
+ gli_option_parser = GLIOptionParser.new(commands,
63
+ flags,
64
+ switches,
65
+ accepts,
66
+ @default_command,
67
+ self.subcommand_option_handling_strategy)
62
68
 
63
- copy_options_to_aliased_versions(global_options,command,options)
69
+ parsing_result = gli_option_parser.parse_options(args)
70
+ parsing_result.convert_to_openstruct! if @use_openstruct
64
71
 
65
- global_options = convert_to_openstruct_if_needed(global_options)
66
- options = convert_to_openstruct_if_needed(options)
72
+ the_command = parsing_result.command
67
73
 
68
- if proceed?(global_options,command,options,arguments)
69
- call_command(command,global_options,options,arguments)
70
- end
74
+ call_command(parsing_result) if proceed?(parsing_result)
71
75
  0
72
76
  rescue Exception => ex
73
- handle_exception(ex,command)
77
+ if the_command.nil? && ex.respond_to?(:command_in_context)
78
+ the_command = ex.command_in_context
79
+ end
80
+ handle_exception(ex,the_command)
74
81
  end
75
82
  end
76
83
 
@@ -79,17 +86,9 @@ module GLI
79
86
  def config_file_name #:nodoc:
80
87
  @config_file
81
88
  end
82
-
83
89
  def accepts #:nodoc:
84
90
  @accepts ||= {}
85
- end
86
91
 
87
- # Copies all options in both global_options and options to keys for the aliases of those flags.
88
- # For example, if a flag works with either -f or --flag, this will copy the value from [:f] to [:flag]
89
- # to allow the user to access the options by any alias
90
- def copy_options_to_aliased_versions(global_options,command,options) # :nodoc:
91
- copy_options_to_aliases(global_options)
92
- command.copy_options_to_aliases(options)
93
92
  end
94
93
 
95
94
  def parse_config # :nodoc:
@@ -172,8 +171,13 @@ module GLI
172
171
  next if command_name == :initconfig || command.nil?
173
172
  command_config = (config['commands'] || {})[command_name] || {}
174
173
 
175
- override_default(command.topmost_ancestor.flags,command_config)
176
- override_default(command.topmost_ancestor.switches,command_config)
174
+ if @subcommand_option_handling_strategy == :legacy
175
+ override_default(command.topmost_ancestor.flags,command_config)
176
+ override_default(command.topmost_ancestor.switches,command_config)
177
+ else
178
+ override_default(command.flags,command_config)
179
+ override_default(command.switches,command_config)
180
+ end
177
181
 
178
182
  override_command_defaults(command.commands,command_config)
179
183
  end
@@ -185,13 +189,19 @@ module GLI
185
189
  end
186
190
  end
187
191
 
192
+ def subcommand_option_handling_strategy
193
+ @subcommand_option_handling_strategy || :legacy
194
+ end
195
+
188
196
  private
189
197
 
190
198
  def handle_exception(ex,command)
191
199
  if regular_error_handling?(ex)
192
200
  output_error_message(ex)
193
201
  if ex.kind_of?(OptionParser::ParseError) || ex.kind_of?(BadCommandLine)
194
- commands[:help] and commands[:help].execute({},{},command.nil? ? [] : [command.name.to_s])
202
+ if commands[:help]
203
+ commands[:help].execute({},{},command.nil? ? [] : [command.name.to_s])
204
+ end
195
205
  end
196
206
  end
197
207
 
@@ -210,13 +220,7 @@ module GLI
210
220
 
211
221
  def no_message_given?(ex)
212
222
  ex.message == ex.class.name
213
- end
214
223
 
215
- # Possibly returns a copy of the passed-in Hash as an instance of GLI::Option.
216
- # By default, it will *not*. However by putting <tt>use_openstruct true</tt>
217
- # in your CLI definition, it will
218
- def convert_to_openstruct_if_needed(options) # :nodoc:
219
- @use_openstruct ? Options.new(options) : options
220
224
  end
221
225
 
222
226
  def add_help_switch_if_needed(switches)
@@ -233,11 +237,11 @@ module GLI
233
237
 
234
238
  # True if we should proceed with executing the command; this calls
235
239
  # the pre block if it's defined
236
- def proceed?(global_options,command,options,arguments) #:nodoc:
237
- if command && command.skips_pre
240
+ def proceed?(parsing_result) #:nodoc:
241
+ if parsing_result.command && parsing_result.command.skips_pre
238
242
  true
239
243
  else
240
- pre_block.call(global_options,command,options,arguments)
244
+ pre_block.call(*parsing_result)
241
245
  end
242
246
  end
243
247
 
@@ -257,8 +261,12 @@ module GLI
257
261
  "error: #{ex.message}"
258
262
  end
259
263
 
260
- def call_command(command,global_options,options,arguments)
261
- arguments = arguments.map { |arg| arg.dup } # unfreeze
264
+ def call_command(parsing_result)
265
+ command = parsing_result.command
266
+ global_options = parsing_result.global_options
267
+ options = parsing_result.command_options
268
+ arguments = parsing_result.arguments.map { |arg| arg.dup } # unfreeze
269
+
262
270
  code = lambda { command.execute(global_options,options,arguments) }
263
271
  nested_arounds = unless command.skips_around
264
272
  around_blocks.inject do |outer_around, inner_around|
data/lib/gli/command.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require 'gli/command_line_token.rb'
2
- require 'gli/copy_options_to_aliases.rb'
3
2
  require 'gli/dsl.rb'
4
3
 
5
4
  module GLI
@@ -28,10 +27,12 @@ module GLI
28
27
  # end
29
28
  #
30
29
  class Command < CommandLineToken
31
- include CopyOptionsToAliases
32
30
  include DSL
33
31
  include CommandSupport
34
32
 
33
+ # Key in an options hash to find the parent's parsed options
34
+ PARENT = Object.new
35
+
35
36
  # Create a new command.
36
37
  #
37
38
  # options:: Keys should be:
@@ -0,0 +1,41 @@
1
+ module GLI
2
+ class CommandFinder
3
+ # Initialize a finder on the given list of commands, using default_command as the default if none found
4
+ def initialize(commands,default_command)
5
+ @default_command = default_command
6
+ @names_to_commands = {}
7
+ commands.each do |command_name,command|
8
+ @names_to_commands[command_name.to_s] = command
9
+ Array(command.aliases).each do |command_alias|
10
+ @names_to_commands[command_alias.to_s] = command
11
+ end
12
+ end
13
+ end
14
+
15
+ # Finds the command with the given name, allowing for partial matches. Returns the command named by
16
+ # the default command if no command with +name+ matched
17
+ def find_command(name)
18
+ name ||= @default_command
19
+
20
+ raise UnknownCommand.new("No command name given nor default available") if String(name).strip == ''
21
+
22
+ command_found = @names_to_commands.fetch(name.to_s) do |command_to_match|
23
+ find_command_by_partial_name(@names_to_commands, command_to_match)
24
+ end
25
+ if Array(command_found).empty?
26
+ raise UnknownCommand.new("Unknown command '#{name}'")
27
+ elsif command_found.kind_of? Array
28
+ raise AmbiguousCommand.new("Ambiguous command '#{name}'. It matches #{command_found.sort.join(',')}")
29
+ end
30
+ command_found
31
+ end
32
+
33
+ private
34
+
35
+ def find_command_by_partial_name(names_to_commands, command_to_match)
36
+ partial_matches = names_to_commands.keys.select { |command_name| command_name =~ /^#{command_to_match}/ }
37
+ return names_to_commands[partial_matches[0]] if partial_matches.size == 1
38
+ partial_matches
39
+ end
40
+ end
41
+ end