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.
@@ -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 = TextWrapper.new(Terminal.instance.size[0],4 + @command.name.to_s.size + 3)
15
- wrapper = TextWrapper.new(Terminal.instance.size[0],4)
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 << format_arg_name(sub)
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.commands.values.sort.map { |cmd|
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.commands.values.sort.reject(&:nodoc).map { |command|
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 = TextWrapper.new(Terminal.instance.size[0],4 + max_width + 3)
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.default_value || 'none'})"
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
- usage = "#{add_dashes(name)} #{arg_name || 'arg'}"
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
- @io.puts "#{@nest}=== #{add_dashes(name)}"
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}=== #{name} #{arg_name}"
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
@@ -163,6 +163,8 @@ module GLI
163
163
  commands[command.name] = command
164
164
  yield command
165
165
  end
166
+ @commands_declaration_order ||= []
167
+ @commands_declaration_order << command
166
168
  clear_nexts
167
169
  end
168
170
  alias :c :command
data/lib/gli/flag.rb CHANGED
@@ -15,20 +15,32 @@ module GLI
15
15
 
16
16
  # Creates a new option
17
17
  #
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
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