gli 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/features/gli_executable.feature +0 -2
- data/features/step_definitions/todo_steps.rb +11 -0
- data/features/support/env.rb +4 -0
- data/features/todo.feature +94 -2
- data/gli.gemspec +1 -0
- data/gli.rdoc +11 -18
- data/lib/gli/app.rb +34 -3
- data/lib/gli/app_support.rb +34 -4
- data/lib/gli/command.rb +8 -0
- data/lib/gli/command_support.rb +5 -0
- data/lib/gli/commands/doc.rb +22 -8
- data/lib/gli/commands/help.rb +18 -2
- data/lib/gli/commands/help_modules/arg_name_formatter.rb +20 -0
- data/lib/gli/commands/help_modules/command_help_format.rb +9 -18
- data/lib/gli/commands/help_modules/global_help_format.rb +14 -5
- data/lib/gli/commands/help_modules/list_formatter.rb +3 -2
- data/lib/gli/commands/help_modules/no_wrapping_wrapper.rb +18 -0
- data/lib/gli/commands/help_modules/options_formatter.rb +4 -3
- data/lib/gli/commands/help_modules/tty_only_wrapper.rb +23 -0
- data/lib/gli/commands/rdoc_document_listener.rb +13 -9
- data/lib/gli/dsl.rb +2 -0
- data/lib/gli/flag.rb +20 -8
- data/lib/gli/gli_option_parser.rb +19 -0
- data/lib/gli/option_parser_factory.rb +3 -3
- data/lib/gli/terminal.rb +1 -1
- data/lib/gli/version.rb +1 -1
- data/test/apps/todo/bin/todo +4 -0
- data/test/tc_doc.rb +6 -0
- data/test/tc_flag.rb +16 -8
- data/test/test_helper.rb +2 -0
- metadata +258 -364
data/lib/gli/commands/help.rb
CHANGED
@@ -3,14 +3,27 @@ require 'gli/command'
|
|
3
3
|
require 'gli/terminal'
|
4
4
|
require 'gli/commands/help_modules/list_formatter'
|
5
5
|
require 'gli/commands/help_modules/text_wrapper'
|
6
|
+
require 'gli/commands/help_modules/no_wrapping_wrapper'
|
7
|
+
require 'gli/commands/help_modules/tty_only_wrapper'
|
6
8
|
require 'gli/commands/help_modules/options_formatter'
|
7
9
|
require 'gli/commands/help_modules/global_help_format'
|
8
10
|
require 'gli/commands/help_modules/command_help_format'
|
9
11
|
require 'gli/commands/help_modules/help_completion_format'
|
10
12
|
require 'gli/commands/help_modules/command_finder'
|
13
|
+
require 'gli/commands/help_modules/arg_name_formatter'
|
11
14
|
|
12
15
|
module GLI
|
13
16
|
module Commands
|
17
|
+
SORTERS = {
|
18
|
+
:manually => lambda { |list| list },
|
19
|
+
:alpha => lambda { |list| list.sort },
|
20
|
+
}
|
21
|
+
|
22
|
+
WRAPPERS = {
|
23
|
+
:to_terminal => HelpModules::TextWrapper,
|
24
|
+
:never => HelpModules::NoWrappingWrapper,
|
25
|
+
:tty_only => HelpModules::TTYOnlyWrapper,
|
26
|
+
}
|
14
27
|
# The help command used for the two-level interactive help system
|
15
28
|
class Help < Command
|
16
29
|
def initialize(app,output=$stdout,error=$stderr)
|
@@ -22,6 +35,9 @@ module GLI
|
|
22
35
|
:skips_post => true,
|
23
36
|
:skips_around => true)
|
24
37
|
@app = app
|
38
|
+
@sorter = SORTERS[@app.help_sort_type]
|
39
|
+
@text_wrapping_class = WRAPPERS[@app.help_text_wrap_type]
|
40
|
+
|
25
41
|
desc 'List commands one per line, to assist with shell completion'
|
26
42
|
switch :c
|
27
43
|
|
@@ -38,12 +54,12 @@ module GLI
|
|
38
54
|
help_output = HelpModules::HelpCompletionFormat.new(@app,command_finder,arguments).format
|
39
55
|
out.puts help_output unless help_output.nil?
|
40
56
|
elsif arguments.empty? || options[:c]
|
41
|
-
out.puts HelpModules::GlobalHelpFormat.new(@app).format
|
57
|
+
out.puts HelpModules::GlobalHelpFormat.new(@app,@sorter,@text_wrapping_class).format
|
42
58
|
else
|
43
59
|
name = arguments.shift
|
44
60
|
command = command_finder.find_command(name)
|
45
61
|
unless command.nil?
|
46
|
-
out.puts HelpModules::CommandHelpFormat.new(command,@app,File.basename($0).to_s).format
|
62
|
+
out.puts HelpModules::CommandHelpFormat.new(command,@app,File.basename($0).to_s,@sorter,@text_wrapping_class).format
|
47
63
|
end
|
48
64
|
end
|
49
65
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module GLI
|
2
|
+
module Commands
|
3
|
+
module HelpModules
|
4
|
+
# Handles wrapping text
|
5
|
+
class ArgNameFormatter
|
6
|
+
def format(arguments_description,arguments_options)
|
7
|
+
return '' if String(arguments_description).strip == ''
|
8
|
+
desc = arguments_description
|
9
|
+
if arguments_options.include? :optional
|
10
|
+
desc = "[#{desc}]"
|
11
|
+
end
|
12
|
+
if arguments_options.include? :multiple
|
13
|
+
desc = "#{desc}[, #{desc}]*"
|
14
|
+
end
|
15
|
+
" " + desc
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -4,17 +4,19 @@ module GLI
|
|
4
4
|
module Commands
|
5
5
|
module HelpModules
|
6
6
|
class CommandHelpFormat
|
7
|
-
def initialize(command,app,basic_invocation)
|
7
|
+
def initialize(command,app,basic_invocation,sorter,wrapper_class=TextWrapper)
|
8
8
|
@basic_invocation = basic_invocation
|
9
9
|
@app = app
|
10
10
|
@command = command
|
11
|
+
@sorter = sorter
|
12
|
+
@wrapper_class = wrapper_class
|
11
13
|
end
|
12
14
|
|
13
15
|
def format
|
14
|
-
command_wrapper =
|
15
|
-
wrapper =
|
16
|
+
command_wrapper = @wrapper_class.new(Terminal.instance.size[0],4 + @command.name.to_s.size + 3)
|
17
|
+
wrapper = @wrapper_class.new(Terminal.instance.size[0],4)
|
16
18
|
flags_and_switches = Hash[@command.topmost_ancestor.flags.merge(@command.topmost_ancestor.switches).select { |_,option| option.associated_command == @command }]
|
17
|
-
options_description = OptionsFormatter.new(flags_and_switches).format
|
19
|
+
options_description = OptionsFormatter.new(flags_and_switches,@wrapper_class).format
|
18
20
|
commands_description = format_subcommands(@command)
|
19
21
|
|
20
22
|
synopses = []
|
@@ -72,21 +74,10 @@ COMMANDS
|
|
72
74
|
else
|
73
75
|
usage << sub.name.to_s
|
74
76
|
end
|
75
|
-
usage <<
|
77
|
+
usage << ArgNameFormatter.new.format(sub.arguments_description,sub.arguments_options)
|
76
78
|
usage
|
77
79
|
end
|
78
80
|
|
79
|
-
def format_arg_name(command)
|
80
|
-
return '' if String(command.arguments_description).strip == ''
|
81
|
-
desc = command.arguments_description
|
82
|
-
if command.arguments_options.include? :optional
|
83
|
-
desc = "[#{desc}]"
|
84
|
-
end
|
85
|
-
if command.arguments_options.include? :multiple
|
86
|
-
desc = "#{desc}[, #{desc}]*"
|
87
|
-
end
|
88
|
-
" " + desc
|
89
|
-
end
|
90
81
|
|
91
82
|
def basic_usage(flags_and_switches)
|
92
83
|
usage = @basic_invocation.dup
|
@@ -110,7 +101,7 @@ COMMANDS
|
|
110
101
|
end
|
111
102
|
|
112
103
|
def format_subcommands(command)
|
113
|
-
commands_array = command.
|
104
|
+
commands_array = @sorter.call(command.commands_declaration_order).map { |cmd|
|
114
105
|
if command.get_default_command == cmd.name
|
115
106
|
[cmd.names,cmd.description + " (default)"]
|
116
107
|
else
|
@@ -120,7 +111,7 @@ COMMANDS
|
|
120
111
|
if command.has_action?
|
121
112
|
commands_array.unshift(["<default>",command.default_description])
|
122
113
|
end
|
123
|
-
formatter = ListFormatter.new(commands_array)
|
114
|
+
formatter = ListFormatter.new(commands_array,@wrapper_class)
|
124
115
|
StringIO.new.tap { |io| formatter.output(io) }.string
|
125
116
|
end
|
126
117
|
|
@@ -4,21 +4,30 @@ module GLI
|
|
4
4
|
module Commands
|
5
5
|
module HelpModules
|
6
6
|
class GlobalHelpFormat
|
7
|
-
def initialize(app)
|
7
|
+
def initialize(app,sorter,wrapper_class)
|
8
8
|
@app = app
|
9
|
+
@sorter = sorter
|
10
|
+
@wrapper_class = wrapper_class
|
9
11
|
end
|
10
12
|
|
11
13
|
def format
|
12
14
|
program_desc = @app.program_desc
|
15
|
+
program_long_desc = @app.program_long_desc
|
16
|
+
if program_long_desc
|
17
|
+
wrapper = @wrapper_class.new(Terminal.instance.size[0],4)
|
18
|
+
program_long_desc = "\n #{wrapper.wrap(program_long_desc)}\n\n" if program_long_desc
|
19
|
+
else
|
20
|
+
program_long_desc = "\n"
|
21
|
+
end
|
13
22
|
|
14
|
-
command_formatter = ListFormatter.new(@app.
|
23
|
+
command_formatter = ListFormatter.new(@sorter.call(@app.commands_declaration_order.reject(&:nodoc)).map { |command|
|
15
24
|
[[command.name,Array(command.aliases)].flatten.join(', '),command.description]
|
16
|
-
})
|
25
|
+
}, @wrapper_class)
|
17
26
|
stringio = StringIO.new
|
18
27
|
command_formatter.output(stringio)
|
19
28
|
commands = stringio.string
|
20
29
|
|
21
|
-
global_option_descriptions = OptionsFormatter.new(global_flags_and_switches).format
|
30
|
+
global_option_descriptions = OptionsFormatter.new(global_flags_and_switches,@wrapper_class).format
|
22
31
|
|
23
32
|
GLOBAL_HELP.result(binding)
|
24
33
|
end
|
@@ -27,7 +36,7 @@ module GLI
|
|
27
36
|
|
28
37
|
GLOBAL_HELP = ERB.new(%q(NAME
|
29
38
|
<%= File.basename($0) %> - <%= program_desc %>
|
30
|
-
|
39
|
+
<%= program_long_desc %>
|
31
40
|
SYNOPSIS
|
32
41
|
<%= usage_string %>
|
33
42
|
|
@@ -3,15 +3,16 @@ module GLI
|
|
3
3
|
module HelpModules
|
4
4
|
# Given a list of two-element lists, formats on the terminal
|
5
5
|
class ListFormatter
|
6
|
-
def initialize(list)
|
6
|
+
def initialize(list,wrapper_class=TextWrapper)
|
7
7
|
@list = list
|
8
|
+
@wrapper_class = wrapper_class
|
8
9
|
end
|
9
10
|
|
10
11
|
# Output the list to the output_device
|
11
12
|
def output(output_device)
|
12
13
|
return if @list.empty?
|
13
14
|
max_width = @list.map { |_| _[0].length }.max
|
14
|
-
wrapper =
|
15
|
+
wrapper = @wrapper_class.new(Terminal.instance.size[0],4 + max_width + 3)
|
15
16
|
@list.each do |(name,description)|
|
16
17
|
output_device.printf(" %-#{max_width}s - %s\n",name,wrapper.wrap(String(description).strip))
|
17
18
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module GLI
|
2
|
+
module Commands
|
3
|
+
module HelpModules
|
4
|
+
# Formats text in one line, stripping newlines and NOT wrapping
|
5
|
+
class NoWrappingWrapper
|
6
|
+
# Args are ignored entirely; this keeps it consistent with the TextWrapper interface
|
7
|
+
def initialize(width,indent)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Return a wrapped version of text, assuming that the first line has already been
|
11
|
+
# indented by @indent characters. Resulting text does NOT have a newline in it.
|
12
|
+
def wrap(text)
|
13
|
+
return String(text).gsub(/\n+/,' ').strip
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -2,8 +2,9 @@ module GLI
|
|
2
2
|
module Commands
|
3
3
|
module HelpModules
|
4
4
|
class OptionsFormatter
|
5
|
-
def initialize(flags_and_switches)
|
5
|
+
def initialize(flags_and_switches,wrapper_class)
|
6
6
|
@flags_and_switches = flags_and_switches
|
7
|
+
@wrapper_class = wrapper_class
|
7
8
|
end
|
8
9
|
|
9
10
|
def format
|
@@ -15,7 +16,7 @@ module GLI
|
|
15
16
|
else
|
16
17
|
[option_names_for_help_string(option),description_with_default(option)]
|
17
18
|
end
|
18
|
-
})
|
19
|
+
},@wrapper_class)
|
19
20
|
stringio = StringIO.new
|
20
21
|
list_formatter.output(stringio)
|
21
22
|
stringio.string
|
@@ -25,7 +26,7 @@ module GLI
|
|
25
26
|
|
26
27
|
def description_with_default(option)
|
27
28
|
if option.kind_of? Flag
|
28
|
-
String(option.description) + " (default: #{option.
|
29
|
+
String(option.description) + " (default: #{option.safe_default_value || 'none'})"
|
29
30
|
else
|
30
31
|
String(option.description)
|
31
32
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GLI
|
2
|
+
module Commands
|
3
|
+
module HelpModules
|
4
|
+
# Formats text in one line, stripping newlines and NOT wrapping
|
5
|
+
class TTYOnlyWrapper
|
6
|
+
# Args are ignored entirely; this keeps it consistent with the TextWrapper interface
|
7
|
+
def initialize(width,indent)
|
8
|
+
@proxy = if STDOUT.tty?
|
9
|
+
TextWrapper.new(width,indent)
|
10
|
+
else
|
11
|
+
NoWrappingWrapper.new(width,indent)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return a wrapped version of text, assuming that the first line has already been
|
16
|
+
# indented by @indent characters. Resulting text does NOT have a newline in it.
|
17
|
+
def wrap(text)
|
18
|
+
@proxy.wrap(text)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'stringio'
|
2
|
+
require 'gli/commands/help_modules/arg_name_formatter'
|
2
3
|
module GLI
|
3
4
|
module Commands
|
4
5
|
class RdocDocumentListener
|
@@ -6,6 +7,7 @@ module GLI
|
|
6
7
|
def initialize(global_options,options,arguments)
|
7
8
|
@io = File.new(File.basename($0) + ".rdoc",'w')
|
8
9
|
@nest = ''
|
10
|
+
@arg_name_formatter = GLI::Commands::HelpModules::ArgNameFormatter.new
|
9
11
|
end
|
10
12
|
|
11
13
|
def beginning
|
@@ -22,6 +24,11 @@ module GLI
|
|
22
24
|
@io.puts
|
23
25
|
end
|
24
26
|
|
27
|
+
def program_long_desc(desc)
|
28
|
+
@io.puts desc
|
29
|
+
@io.puts
|
30
|
+
end
|
31
|
+
|
25
32
|
# Gives you the program version
|
26
33
|
def version(version)
|
27
34
|
@io.puts "v#{version}"
|
@@ -38,12 +45,12 @@ module GLI
|
|
38
45
|
|
39
46
|
# Gives you a flag in the current context
|
40
47
|
def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type)
|
41
|
-
|
48
|
+
invocations = ([name] + Array(aliases)).map { |_| add_dashes(_) }.join('|')
|
49
|
+
usage = "#{invocations} #{arg_name || 'arg'}"
|
42
50
|
@io.puts "#{@nest}=== #{usage}"
|
43
51
|
@io.puts
|
44
52
|
@io.puts String(desc).strip
|
45
53
|
@io.puts
|
46
|
-
@io.puts "[Aliases] #{aliases.map { |_| add_dashes(_) }.join(',')}" unless aliases.empty?
|
47
54
|
@io.puts "[Default Value] #{default_value || 'None'}"
|
48
55
|
@io.puts "[Must Match] #{must_match.to_s}" unless must_match.nil?
|
49
56
|
@io.puts String(long_desc).strip
|
@@ -56,11 +63,10 @@ module GLI
|
|
56
63
|
name = "[no-]#{name}" if name.length > 1
|
57
64
|
aliases = aliases.map { |_| _.length > 1 ? "[no-]#{_}" : _ }
|
58
65
|
end
|
59
|
-
|
66
|
+
invocations = ([name] + aliases).map { |_| add_dashes(_) }.join('|')
|
67
|
+
@io.puts "#{@nest}=== #{invocations}"
|
60
68
|
@io.puts String(desc).strip
|
61
69
|
@io.puts
|
62
|
-
@io.puts "[Aliases] #{aliases.map { |_| add_dashes(_) }.join(',')}\n" unless aliases.empty?
|
63
|
-
@io.puts
|
64
70
|
@io.puts String(long_desc).strip
|
65
71
|
@io.puts
|
66
72
|
end
|
@@ -74,12 +80,10 @@ module GLI
|
|
74
80
|
end
|
75
81
|
|
76
82
|
# Gives you a command in the current context and creates a new context of this command
|
77
|
-
def command(name,aliases,desc,long_desc,arg_name)
|
78
|
-
@io.puts "#{@nest}===
|
83
|
+
def command(name,aliases,desc,long_desc,arg_name,arg_options)
|
84
|
+
@io.puts "#{@nest}=== Command: <tt>#{([name] + aliases).join('|')} #{@arg_name_formatter.format(arg_name,arg_options)}</tt>"
|
79
85
|
@io.puts String(desc).strip
|
80
86
|
@io.puts
|
81
|
-
@io.puts "[Aliases] #{aliases.join(',')}\n" unless aliases.empty?
|
82
|
-
@io.puts
|
83
87
|
@io.puts String(long_desc).strip
|
84
88
|
@nest = "#{@nest}="
|
85
89
|
end
|
data/lib/gli/dsl.rb
CHANGED
data/lib/gli/flag.rb
CHANGED
@@ -15,20 +15,32 @@ module GLI
|
|
15
15
|
|
16
16
|
# Creates a new option
|
17
17
|
#
|
18
|
-
# names
|
19
|
-
# options
|
20
|
-
# :desc
|
21
|
-
# :long_desc
|
22
|
-
# :default_value
|
23
|
-
# :arg_name
|
24
|
-
# :must_match
|
25
|
-
# :type
|
18
|
+
# names:: Array of symbols or strings representing the names of this switch
|
19
|
+
# options:: hash of options:
|
20
|
+
# :desc:: the short description
|
21
|
+
# :long_desc:: the long description
|
22
|
+
# :default_value:: the default value of this option
|
23
|
+
# :arg_name:: the name of the flag's argument, default is "arg"
|
24
|
+
# :must_match:: a regexp that the flag's value must match
|
25
|
+
# :type:: a class to convert the value to
|
26
|
+
# :mask:: if true, the default value of this flag will not be output in the help.
|
27
|
+
# This is useful for password flags where you might not want to show it
|
28
|
+
# on the command-line.
|
26
29
|
def initialize(names,options)
|
27
30
|
super(names,options)
|
28
31
|
@argument_name = options[:arg_name] || "arg"
|
29
32
|
@default_value = options[:default_value]
|
30
33
|
@must_match = options[:must_match]
|
31
34
|
@type = options[:type]
|
35
|
+
@mask = options[:mask]
|
36
|
+
end
|
37
|
+
|
38
|
+
def safe_default_value
|
39
|
+
if @mask
|
40
|
+
"********"
|
41
|
+
else
|
42
|
+
default_value
|
43
|
+
end
|
32
44
|
end
|
33
45
|
|
34
46
|
def arguments_for_option_parser
|
@@ -53,6 +53,25 @@ module GLI
|
|
53
53
|
|
54
54
|
def parse_command_options(option_parser_factory,command,args)
|
55
55
|
option_parser,command_options = option_parser_factory.option_parser
|
56
|
+
help_args = %w(-h --help).reject { |_| command.has_option?(_) }
|
57
|
+
|
58
|
+
unless help_args.empty?
|
59
|
+
option_parser.on(*help_args) do
|
60
|
+
# We need to raise the help exception later in the process, after
|
61
|
+
# the command-line has been parsed, so we know what command
|
62
|
+
# to show help for. Unfortunately, we can't just call #action
|
63
|
+
# on the command to override what to do, so...metaprogramming.
|
64
|
+
class << command
|
65
|
+
def execute(*help_args)
|
66
|
+
exception = BadCommandLine.new(nil)
|
67
|
+
class << exception
|
68
|
+
def exit_code; 0; end
|
69
|
+
end
|
70
|
+
raise exception
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
56
75
|
option_parser.parse!(args)
|
57
76
|
[command_options,args]
|
58
77
|
rescue OptionParser::InvalidOption => ex
|
@@ -23,9 +23,9 @@ module GLI
|
|
23
23
|
private
|
24
24
|
|
25
25
|
def self.setup_accepts(opts,accepts)
|
26
|
-
accepts.each do |object,block|
|
27
|
-
opts.accept(object) do |arg_as_string|
|
28
|
-
block.call(arg_as_string)
|
26
|
+
accepts.each do |object,block|
|
27
|
+
opts.accept(object) do |arg_as_string|
|
28
|
+
block.call(arg_as_string)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
end
|
data/lib/gli/terminal.rb
CHANGED
@@ -74,7 +74,7 @@ module GLI
|
|
74
74
|
#
|
75
75
|
# Returns an Array of size two Ints representing the terminal width and height
|
76
76
|
def size
|
77
|
-
SIZE_DETERMINERS.select { |predicate,ignore| predicate.call }.first[1].call
|
77
|
+
SIZE_DETERMINERS.select { |(predicate,ignore)| predicate.call }.first[1].call
|
78
78
|
rescue Exception => ex
|
79
79
|
raise ex if @unsafe
|
80
80
|
Terminal.default_size
|