brew/Library/Homebrew/cli_parser.rb

303 lines
9.0 KiB
Ruby
Raw Normal View History

require "optparse"
require "ostruct"
require "set"
module Homebrew
module CLI
class Parser
attr_reader :processed_options, :hide_from_man_page
2018-09-08 22:21:04 +05:30
2018-06-01 14:19:00 +02:00
def self.parse(args = ARGV, &block)
new(&block).parse(args)
end
def self.global_options
{
2018-11-02 17:18:07 +00:00
quiet: [["-q", "--quiet"], :quiet, "Suppress any warnings."],
verbose: [["-v", "--verbose"], :verbose, "Make some output more verbose."],
2018-11-02 17:18:07 +00:00
debug: [["-d", "--debug"], :debug, "Display any debugging information."],
force: [["-f", "--force"], :force, "Override warnings and enable potentially unsafe operations."],
}
end
def initialize(&block)
@parser = OptionParser.new
Homebrew.args = OpenStruct.new
# undefine tap to allow --tap argument
Homebrew.args.instance_eval { undef tap }
@constraints = []
@conflicts = []
@switch_sources = {}
2018-09-08 22:21:04 +05:30
@processed_options = []
@desc_line_length = 43
@hide_from_man_page = false
instance_eval(&block)
post_initialize
end
def post_initialize
@parser.on_tail("-h", "--help", "Show this message.") do
puts generate_help_text
exit 0
end
end
def switch(*names, description: nil, env: nil, required_for: nil, depends_on: nil)
global_switch = names.first.is_a?(Symbol)
names, env, default_description = common_switch(*names) if global_switch
if description.nil? && global_switch
description = default_description
elsif description.nil?
description = option_to_description(*names)
end
2018-09-08 22:21:04 +05:30
process_option(*names, description)
@parser.on(*names, *wrap_option_desc(description)) do
2019-02-15 14:15:34 -05:00
enable_switch(*names, from: :args)
end
names.each do |name|
set_constraints(name, required_for: required_for, depends_on: depends_on)
end
2019-02-15 11:01:03 -05:00
enable_switch(*names, from: :env) if !env.nil? && !ENV["HOMEBREW_#{env.to_s.upcase}"].nil?
end
alias switch_option switch
2018-09-08 22:21:04 +05:30
def usage_banner(text)
2018-09-22 09:31:30 +05:30
@parser.banner = "#{text}\n"
end
2018-09-08 22:21:04 +05:30
def usage_banner_text
@parser.banner
end
def comma_array(name, description: nil)
description = option_to_description(name) if description.nil?
2018-09-08 22:21:04 +05:30
process_option(name, description)
@parser.on(name, OptionParser::REQUIRED_ARGUMENT, Array, *wrap_option_desc(description)) do |list|
Homebrew.args[option_to_name(name)] = list
end
end
def flag(*names, description: nil, required_for: nil, depends_on: nil)
if names.any? { |name| name.end_with? "=" }
required = OptionParser::REQUIRED_ARGUMENT
else
required = OptionParser::OPTIONAL_ARGUMENT
end
names.map! { |name| name.chomp "=" }
description = option_to_description(*names) if description.nil?
process_option(*names, description)
@parser.on(*names, *wrap_option_desc(description), required) do |option_value|
names.each do |name|
Homebrew.args[option_to_name(name)] = option_value
end
end
names.each do |name|
set_constraints(name, required_for: required_for, depends_on: depends_on)
end
end
def conflicts(*options)
@conflicts << options.map { |option| option_to_name(option) }
end
2018-06-01 14:19:00 +02:00
def option_to_name(option)
option.sub(/\A--?/, "")
.tr("-", "_")
.delete("=")
end
def name_to_option(name)
if name.length == 1
"-#{name}"
else
2018-11-07 23:44:34 +05:30
"--#{name.tr("_", "-")}"
2018-06-01 14:19:00 +02:00
end
end
def option_to_description(*names)
names.map { |name| name.to_s.sub(/\A--?/, "").tr("-", " ") }.max
end
def summary
@parser.to_s
end
def parse(cmdline_args = ARGV)
begin
remaining_args = @parser.parse(cmdline_args)
rescue OptionParser::InvalidOption => e
$stderr.puts generate_help_text
raise e
end
check_constraint_violations
2018-06-01 14:19:00 +02:00
Homebrew.args[:remaining] = remaining_args
Homebrew.args.freeze
cmdline_args.freeze
@parser
end
def global_option?(name)
2018-10-02 19:54:22 +05:30
Homebrew::CLI::Parser.global_options.key?(name.to_sym)
end
def generate_help_text
@parser.to_s.sub(/^/, "#{Tty.bold}Usage: brew#{Tty.reset} ")
.gsub(/`(.*?)`/m, "#{Tty.bold}\\1#{Tty.reset}")
.gsub(%r{<([^\s]+?://[^\s]+?)>}) { |url| Formatter.url(url) }
.gsub(/<(.*?)>/m, "#{Tty.underline}\\1#{Tty.reset}")
.gsub(/\*(.*?)\*/m, "#{Tty.underline}\\1#{Tty.reset}")
end
def formula_options
ARGV.formulae.each do |f|
next if f.options.empty?
f.options.each do |o|
name = o.flag
description = "`#{f.name}`: #{o.description}"
if name.end_with? "="
flag name, description: description
else
switch name, description: description
end
end
end
rescue FormulaUnavailableError
[]
end
def hide_from_man_page!
@hide_from_man_page = true
end
private
2019-02-15 14:15:34 -05:00
def enable_switch(*names, from:)
names.each do |name|
2019-02-15 11:01:03 -05:00
@switch_sources[option_to_name(name)] = from
Homebrew.args["#{option_to_name(name)}?"] = true
end
end
def disable_switch(*names)
names.each do |name|
Homebrew.args.delete_field("#{option_to_name(name)}?")
end
end
# These are common/global switches accessible throughout Homebrew
def common_switch(name)
Homebrew::CLI::Parser.global_options.fetch(name, name)
end
def option_passed?(name)
Homebrew.args.respond_to?(name) || Homebrew.args.respond_to?("#{name}?")
end
def wrap_option_desc(desc)
Formatter.wrap(desc, @desc_line_length).split("\n")
end
def set_constraints(name, depends_on:, required_for:)
secondary = option_to_name(name)
unless required_for.nil?
primary = option_to_name(required_for)
@constraints << [primary, secondary, :mandatory]
end
return if depends_on.nil?
2018-09-17 02:45:00 +02:00
primary = option_to_name(depends_on)
@constraints << [primary, secondary, :optional]
end
def check_constraints
@constraints.each do |primary, secondary, constraint_type|
primary_passed = option_passed?(primary)
secondary_passed = option_passed?(secondary)
if :mandatory.equal?(constraint_type) && primary_passed && !secondary_passed
raise OptionConstraintError.new(primary, secondary)
end
raise OptionConstraintError.new(primary, secondary, missing: true) if secondary_passed && !primary_passed
end
end
def check_conflicts
@conflicts.each do |mutually_exclusive_options_group|
violations = mutually_exclusive_options_group.select do |option|
option_passed? option
end
2018-06-01 14:19:00 +02:00
next if violations.count < 2
2018-09-17 02:45:00 +02:00
env_var_options = violations.select do |option|
@switch_sources[option_to_name(option)] == :env_var
end
select_cli_arg = violations.count - env_var_options.count == 1
raise OptionConflictError, violations.map(&method(:name_to_option)) unless select_cli_arg
env_var_options.each(&method(:disable_switch))
end
end
def check_invalid_constraints
@conflicts.each do |mutually_exclusive_options_group|
@constraints.each do |p, s|
next unless Set[p, s].subset?(Set[*mutually_exclusive_options_group])
2018-09-17 02:45:00 +02:00
raise InvalidConstraintError.new(p, s)
end
end
end
def check_constraint_violations
check_invalid_constraints
check_conflicts
check_constraints
end
2018-09-08 22:21:04 +05:30
def process_option(*args)
option, = @parser.make_switch(args)
@processed_options << [option.short.first, option.long.first, option.arg, option.desc.first]
end
end
class OptionConstraintError < RuntimeError
def initialize(arg1, arg2, missing: false)
if !missing
message = <<~EOS
`#{arg1}` and `#{arg2}` should be passed together
EOS
else
message = <<~EOS
`#{arg2}` cannot be passed without `#{arg1}`
EOS
end
super message
end
end
class OptionConflictError < RuntimeError
def initialize(args)
2018-06-01 14:19:00 +02:00
args_list = args.map(&Formatter.public_method(:option))
.join(" and ")
super <<~EOS
2018-06-01 14:19:00 +02:00
Options #{args_list} are mutually exclusive.
EOS
end
end
class InvalidConstraintError < RuntimeError
def initialize(arg1, arg2)
super <<~EOS
`#{arg1}` and `#{arg2}` cannot be mutually exclusive and mutually dependent simultaneously
EOS
end
end
end
end