gli 2.1.0 → 2.2.0
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/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
|