gli 2.5.6 → 2.6.0.rc1

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 (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