1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219
|
# frozen_string_literal: true
module Cri
# The {HelpRenderer} class is responsible for generating a string containing
# the help for a given command, intended to be printed on the command line.
class HelpRenderer
# The line width of the help output
LINE_WIDTH = 78
# The indentation of descriptions
DESC_INDENT = 4
# The spacing between an option name and option description
OPT_DESC_SPACING = 6
# Creates a new help renderer for the given command.
#
# @param [Cri::Command] cmd The command to generate the help for
#
# @option params [Boolean] :verbose true if the help output should be
# verbose, false otherwise.
def initialize(cmd, **params)
@cmd = cmd
@is_verbose = params.fetch(:verbose, false)
@io = params.fetch(:io, $stdout)
end
# @return [String] The help text for this command
def render
text = +''
append_summary(text)
append_usage(text)
append_description(text)
append_subcommands(text)
append_options(text)
text
end
private
def fmt
@fmt ||= Cri::StringFormatter.new
end
def append_summary(text)
return if @cmd.name.nil?
text << fmt.format_as_title('name', @io) << "\n"
text << ' ' << fmt.format_as_command(@cmd.name, @io)
if @cmd.summary
text << ' - ' << @cmd.summary
end
text << "\n"
unless @cmd.aliases.empty?
text << ' aliases: ' << @cmd.aliases.map { |a| fmt.format_as_command(a, @io) }.join(' ') << "\n"
end
end
def append_usage(text)
return if @cmd.usage.nil?
path = [@cmd.supercommand]
path.unshift(path[0].supercommand) until path[0].nil?
formatted_usage = @cmd.usage.gsub(/^([^\s]+)/) { |m| fmt.format_as_command(m, @io) }
full_usage = path[1..-1].map { |c| fmt.format_as_command(c.name, @io) + ' ' }.join + formatted_usage
text << "\n"
text << fmt.format_as_title('usage', @io) << "\n"
text << fmt.wrap_and_indent(full_usage, LINE_WIDTH, DESC_INDENT) << "\n"
end
def append_description(text)
return if @cmd.description.nil?
text << "\n"
text << fmt.format_as_title('description', @io) << "\n"
text << (fmt.wrap_and_indent(@cmd.description, LINE_WIDTH, DESC_INDENT) + "\n")
end
def append_subcommands(text)
return if @cmd.subcommands.empty?
text << "\n"
text << fmt.format_as_title(@cmd.supercommand ? 'subcommands' : 'commands', @io)
text << "\n"
shown_subcommands = @cmd.subcommands.select { |c| !c.hidden? || @is_verbose }
length = shown_subcommands.map { |c| fmt.format_as_command(c.name, @io).size }.max
# Command
shown_subcommands.sort_by(&:name).each do |cmd|
text <<
format(
" %<name>-#{length + DESC_INDENT}s %<summary>s\n",
name: fmt.format_as_command(cmd.name, @io),
summary: cmd.summary,
)
end
# Hidden notice
unless @is_verbose
diff = @cmd.subcommands.size - shown_subcommands.size
if diff == 1
text << " (1 hidden command omitted; show it with --verbose)\n"
elsif diff > 1
text << " (#{diff} hidden commands omitted; show them with --verbose)\n"
end
end
end
def length_for_opt_defns(opt_defns)
opt_defns.map do |opt_defn|
string = +''
# Always pretend there is a short option
string << '-X'
if opt_defn.long
string << (' --' + opt_defn.long)
end
case opt_defn.argument
when :required
string << '=<value>'
when :optional
string << '=[<value>]'
end
string.size
end.max
end
def append_options(text)
groups = { 'options' => @cmd.option_definitions }
if @cmd.supercommand
groups["options for #{@cmd.supercommand.name}"] = @cmd.supercommand.global_option_definitions
end
length = length_for_opt_defns(groups.values.inject(&:+))
groups.keys.sort.each do |name|
defs = groups[name]
append_option_group(text, name, defs, length)
end
end
def append_option_group(text, name, defs, length)
return if defs.empty?
text << "\n"
text << fmt.format_as_title(name.to_s, @io)
text << "\n"
ordered_defs = defs.sort_by { |x| x.short || x.long }
ordered_defs.reject(&:hidden).each do |opt_defn|
text << format_opt_defn(opt_defn, length)
desc = opt_defn.desc + (opt_defn.default ? " (default: #{opt_defn.default})" : '')
text << fmt.wrap_and_indent(desc, LINE_WIDTH, length + OPT_DESC_SPACING + DESC_INDENT, true) << "\n"
end
end
def short_value_postfix_for(opt_defn)
value_postfix =
case opt_defn.argument
when :required
'<value>'
when :optional
'[<value>]'
end
if value_postfix
opt_defn.long ? '' : ' ' + value_postfix
else
''
end
end
def long_value_postfix_for(opt_defn)
value_postfix =
case opt_defn.argument
when :required
'=<value>'
when :optional
'[=<value>]'
end
if value_postfix
opt_defn.long ? value_postfix : ''
else
''
end
end
def format_opt_defn(opt_defn, length)
short_value_postfix = short_value_postfix_for(opt_defn)
long_value_postfix = long_value_postfix_for(opt_defn)
opt_text = +''
opt_text_len = 0
if opt_defn.short
opt_text << fmt.format_as_option('-' + opt_defn.short, @io)
opt_text << short_value_postfix
opt_text << ' '
opt_text_len += 1 + opt_defn.short.size + short_value_postfix.size + 1
else
opt_text << ' '
opt_text_len += 3
end
opt_text << fmt.format_as_option('--' + opt_defn.long, @io) if opt_defn.long
opt_text << long_value_postfix
opt_text_len += 2 + opt_defn.long.size if opt_defn.long
opt_text_len += long_value_postfix.size
' ' + opt_text + (' ' * (length + OPT_DESC_SPACING - opt_text_len))
end
end
end
|