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