diff --git a/Library/Homebrew/cli/parser.rb b/Library/Homebrew/cli/parser.rb index 75ff6ed78c..db4353a325 100644 --- a/Library/Homebrew/cli/parser.rb +++ b/Library/Homebrew/cli/parser.rb @@ -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: "|`/``/`", + 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? + " []" + 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 << "" 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 diff --git a/Library/Homebrew/test/cli/parser_spec.rb b/Library/Homebrew/test/cli/parser_spec.rb index 6c99d1bb3d..cc4214a752 100644 --- a/Library/Homebrew/test/cli/parser_spec.rb +++ b/Library/Homebrew/test/cli/parser_spec.rb @@ -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] " + 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] " + 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