parser: automatically generate usage banners

This commit is contained in:
Rylan Polster 2021-01-15 14:47:51 -05:00
parent 893ec3284f
commit b7977244ea
No known key found for this signature in database
GPG Key ID: 46A744940CFF4D64
2 changed files with 159 additions and 1 deletions

View File

@ -122,6 +122,8 @@ module Homebrew
@args = Homebrew::CLI::Args.new
@command_name = caller_locations(2, 1).first.label.chomp("_args")
@constraints = []
@conflicts = []
@switch_sources = {}
@ -129,6 +131,8 @@ module Homebrew
@named_args_type = nil
@max_named_args = nil
@min_named_args = nil
@description = nil
@usage_banner = nil
@hide_from_man_page = false
@formula_options = false
@ -137,6 +141,8 @@ module Homebrew
end
instance_eval(&block) if block
generate_banner
end
def switch(*names, description: nil, replacement: nil, env: nil, required_for: nil, depends_on: nil,
@ -176,8 +182,12 @@ module Homebrew
Homebrew::EnvConfig.try(:"#{env}?")
end
def description(text)
@description = text.chomp
end
def usage_banner(text)
@parser.banner = "#{text}\n"
@usage_banner, @description = text.chomp.split("\n\n", 2)
end
def usage_banner_text
@ -432,6 +442,70 @@ module Homebrew
private
SYMBOL_TO_USAGE_MAPPING = {
text_or_regex: "<text>|`/`<regex>`/`",
url: "<URL>",
}.freeze
def generate_usage_banner
command_names = ["`#{@command_name.tr("_", "-")}`"]
aliases_to_skip = %w[instal uninstal]
command_names += Commands::HOMEBREW_INTERNAL_COMMAND_ALIASES.map do |command_alias, command|
next if aliases_to_skip.include? command_alias
"`#{command_alias.tr("_", "-")}`" if command == @command_name
end.compact.sort
options = if @processed_options.any?
" [<options>]"
else
""
end
named_args = ""
if @named_args_type.present? && @named_args_type != :none
arg_type = if @named_args_type.is_a? Array
types = @named_args_type.map do |type|
next unless type.is_a? Symbol
next SYMBOL_TO_USAGE_MAPPING[type] if SYMBOL_TO_USAGE_MAPPING.key?(type)
"<#{type}>"
end.compact
types << "<subcommand>" if @named_args_type.any? { |type| type.is_a? String }
types.join("|")
elsif SYMBOL_TO_USAGE_MAPPING.key? @named_args_type
SYMBOL_TO_USAGE_MAPPING[@named_args_type]
else
"<#{@named_args_type}>"
end
named_args = if @min_named_args.blank? && @max_named_args == 1
" [#{arg_type}]"
elsif @min_named_args.blank?
" [#{arg_type} ...]"
elsif @min_named_args == 1 && @max_named_args == 1
" #{arg_type}"
elsif @min_named_args == 1
" #{arg_type} [...]"
else
" #{arg_type} ..."
end
end
"#{command_names.join(", ")}#{options}#{named_args}"
end
def generate_banner
@usage_banner ||= generate_usage_banner
@parser.banner = <<~BANNER
#{@usage_banner}
#{@description}
BANNER
end
def set_switch(*names, value:, from:)
names.each do |name|
@switch_sources[option_to_name(name)] = from

View File

@ -318,6 +318,90 @@ describe Homebrew::CLI::Parser do
end
end
describe "usage banner generation" do
it "includes `[options]` if options are available" do
parser = described_class.new do
switch "--foo"
end
expect(parser.generate_help_text).to match(/\[options\]/)
end
it "doesn't include `[options]` if options are available" do
allow(described_class).to receive(:global_options).and_return([])
parser = described_class.new
expect(parser.generate_help_text).not_to match(/\[options\]/)
end
it "includes a description" do
parser = described_class.new do
description <<~EOS
This command does something
EOS
end
expect(parser.generate_help_text).to match(/This command does something/)
end
it "allows the usage banner to be overriden" do
parser = described_class.new do
usage_banner "`test` [foo] <bar>"
end
expect(parser.generate_help_text).to match(/test \[foo\] bar/)
end
it "allows a usage banner and a description to be overriden" do
parser = described_class.new do
usage_banner "`test` [foo] <bar>"
description <<~EOS
This command does something
EOS
end
expect(parser.generate_help_text).to match(/test \[foo\] bar/)
expect(parser.generate_help_text).to match(/This command does something/)
end
it "shows the correct usage for no named argument" do
parser = described_class.new do
named_args :none
end
expect(parser.generate_help_text).to match(/\[options\]\n/)
end
it "shows the correct usage for a single typed argument" do
parser = described_class.new do
named_args :formula, number: 1
end
expect(parser.generate_help_text).to match(/\[options\] formula\n/)
end
it "shows the correct usage for a subcommand argument with a maximum" do
parser = described_class.new do
named_args %w[off on], max: 1
end
expect(parser.generate_help_text).to match(/\[options\] \[subcommand\]\n/)
end
it "shows the correct usage for multiple typed argument with no maximum or minimum" do
parser = described_class.new do
named_args [:tap, :command]
end
expect(parser.generate_help_text).to match(/\[options\] \[tap|command ...\]\n/)
end
it "shows the correct usage for a subcommand argument with a minimum of 1" do
parser = described_class.new do
named_args :installed_formula, min: 1
end
expect(parser.generate_help_text).to match(/\[options\] installed_formula \[...\]\n/)
end
it "shows the correct usage for a subcommand argument with a minimum greater than 1" do
parser = described_class.new do
named_args :installed_formula, min: 2
end
expect(parser.generate_help_text).to match(/\[options\] installed_formula ...\n/)
end
end
describe "named_args" do
let(:parser_none) {
described_class.new do