gli 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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