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.
- data/.travis.yml +1 -0
- data/Gemfile +1 -1
- data/features/step_definitions/todo_steps.rb +4 -0
- data/features/todo.feature +22 -0
- data/features/todo_legacy.feature +128 -0
- data/lib/gli.rb +3 -1
- data/lib/gli/app.rb +10 -2
- data/lib/gli/app_support.rb +39 -31
- data/lib/gli/command.rb +3 -2
- data/lib/gli/command_finder.rb +41 -0
- data/lib/gli/command_line_token.rb +0 -4
- data/lib/gli/command_support.rb +37 -64
- data/lib/gli/commands/doc.rb +12 -2
- data/lib/gli/commands/help.rb +3 -0
- data/lib/gli/commands/help_modules/command_help_format.rb +22 -5
- data/lib/gli/commands/scaffold.rb +1 -1
- data/lib/gli/exceptions.rb +17 -5
- data/lib/gli/gli_option_block_parser.rb +84 -0
- data/lib/gli/gli_option_parser.rb +116 -96
- data/lib/gli/option_parser_factory.rb +42 -10
- data/lib/gli/option_parsing_result.rb +19 -0
- data/lib/gli/version.rb +1 -1
- data/test/apps/todo/bin/todo +2 -0
- data/test/apps/todo/lib/todo/commands/make.rb +52 -0
- data/test/apps/todo_legacy/Gemfile +2 -0
- data/test/apps/todo_legacy/README.rdoc +6 -0
- data/test/apps/todo_legacy/Rakefile +23 -0
- data/test/apps/todo_legacy/bin/todo +61 -0
- data/test/apps/todo_legacy/lib/todo/commands/create.rb +24 -0
- data/test/apps/todo_legacy/lib/todo/commands/list.rb +63 -0
- data/test/apps/todo_legacy/lib/todo/commands/ls.rb +47 -0
- data/test/apps/todo_legacy/lib/todo/version.rb +3 -0
- data/test/apps/todo_legacy/test/tc_nothing.rb +14 -0
- data/test/apps/todo_legacy/todo.gemspec +23 -0
- data/test/apps/todo_legacy/todo.rdoc +5 -0
- data/test/tc_command.rb +84 -59
- data/test/{tc_compount_command.rb → tc_compound_command.rb} +0 -0
- data/test/tc_flag.rb +0 -1
- data/test/tc_gli.rb +2 -2
- data/test/tc_help.rb +11 -3
- data/test/tc_subcommand_parsing.rb +104 -0
- data/test/tc_subcommands.rb +1 -0
- data/test/tc_switch.rb +0 -1
- data/test/test_helper.rb +5 -0
- metadata +74 -13
- data/lib/gli/copy_options_to_aliases.rb +0 -33
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
@@ -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
|
data/features/todo.feature
CHANGED
@@ -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)
|
data/lib/gli/app_support.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
69
|
+
parsing_result = gli_option_parser.parse_options(args)
|
70
|
+
parsing_result.convert_to_openstruct! if @use_openstruct
|
64
71
|
|
65
|
-
|
66
|
-
options = convert_to_openstruct_if_needed(options)
|
72
|
+
the_command = parsing_result.command
|
67
73
|
|
68
|
-
if proceed?(
|
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
|
-
|
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
|
-
|
176
|
-
|
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
|
-
|
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?(
|
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(
|
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(
|
261
|
-
|
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
|