brew/Library/Homebrew/manpages.rb

197 lines
6.8 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "cli/parser"
require "erb"
2023-03-08 09:50:04 -08:00
module Homebrew
# Helper functions for generating homebrew manual.
module Manpages
2023-02-28 12:24:22 -08:00
Variables = Struct.new(
:alumni,
:commands,
:developer_commands,
:environment_variables,
:global_cask_options,
:global_options,
:lead,
:maintainers,
:official_external_commands,
:plc,
:tsc,
keyword_init: true,
)
2024-12-11 11:03:23 -08:00
SOURCE_PATH = (HOMEBREW_LIBRARY_PATH/"manpages").freeze
TARGET_MAN_PATH = (HOMEBREW_REPOSITORY/"manpages").freeze
TARGET_DOC_PATH = (HOMEBREW_REPOSITORY/"docs").freeze
def self.regenerate_man_pages(quiet:)
require "kramdown"
require "manpages/parser/ronn"
require "manpages/converter/kramdown"
require "manpages/converter/roff"
2024-03-07 16:20:20 +00:00
markup = build_man_page(quiet:)
2024-03-10 03:22:53 +00:00
root, warnings = Parser::Ronn.parse(markup)
$stderr.puts(warnings)
roff, warnings = Converter::Kramdown.convert(root)
$stderr.puts(warnings)
File.write(TARGET_DOC_PATH/"Manpage.md", roff)
roff, warnings = Converter::Roff.convert(root)
$stderr.puts(warnings)
File.write(TARGET_MAN_PATH/"brew.1", roff)
end
def self.build_man_page(quiet:)
template = (SOURCE_PATH/"brew.1.md.erb").read
readme = HOMEBREW_REPOSITORY/"README.md"
2023-02-28 12:24:22 -08:00
variables = Variables.new(
commands: generate_cmd_manpages(Commands.internal_commands_paths),
developer_commands: generate_cmd_manpages(Commands.internal_developer_commands_paths),
2024-03-07 16:20:20 +00:00
official_external_commands: generate_cmd_manpages(Commands.official_external_commands_paths(quiet:)),
2023-02-28 12:24:22 -08:00
global_cask_options: global_cask_options_manpage,
global_options: global_options_manpage,
environment_variables: env_vars_manpage,
lead: readme.read[/(Homebrew's \[Project Leader.*\.)/, 1]
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1'),
plc: readme.read[/(Homebrew's \[Project Leadership Committee.*\.)/, 1]
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1'),
tsc: readme.read[/(Homebrew's \[Technical Steering Committee.*\.)/, 1]
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1'),
maintainers: readme.read[/(Homebrew's maintainers .*\.)/, 1]
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1'),
alumni: readme.read[/(Former maintainers .*\.)/, 1]
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1'),
)
ERB.new(template, trim_mode: ">").result(variables.instance_eval { binding })
end
def self.sort_key_for_path(path)
# Options after regular commands (`~` comes after `z` in ASCII table).
path.basename.to_s.sub(/\.(rb|sh)$/, "").sub(/^--/, "~~")
end
def self.generate_cmd_manpages(cmd_paths)
man_page_lines = []
# preserve existing manpage order
cmd_paths.sort_by { sort_key_for_path(_1) }
.each do |cmd_path|
cmd_man_page_lines = if (cmd_parser = Homebrew::CLI::Parser.from_cmd_path(cmd_path))
next if cmd_parser.hide_from_man_page
cmd_parser_manpage_lines(cmd_parser).join
else
2024-03-10 03:22:53 +00:00
cmd_comment_manpage_lines(cmd_path)&.join("\n")
end
2024-03-10 03:22:53 +00:00
# Convert subcommands to definition lists
cmd_man_page_lines&.gsub!(/(?<=\n\n)([\\?\[`].+):\n/, "\\1\n\n: ")
man_page_lines << cmd_man_page_lines
end
man_page_lines.compact.join("\n")
end
2024-08-19 13:15:34 -07:00
sig { params(cmd_parser: CLI::Parser).returns(T::Array[String]) }
def self.cmd_parser_manpage_lines(cmd_parser)
lines = [format_usage_banner(cmd_parser.usage_banner_text)]
2024-08-19 13:15:34 -07:00
lines += cmd_parser.processed_options.filter_map do |short, long, desc, hidden|
next if hidden
if long.present?
next if Homebrew::CLI::Parser.global_options.include?([short, long, desc])
2023-11-05 00:54:59 +00:00
next if Homebrew::CLI::Parser.global_cask_options.any? do |_, option, kwargs|
[long, "#{long}="].include?(option) && kwargs.fetch(:description) == desc
end
end
generate_option_doc(short, long, desc)
end
lines
end
def self.cmd_comment_manpage_lines(cmd_path)
comment_lines = cmd_path.read.lines.grep(/^#:/)
return if comment_lines.empty?
return if comment_lines.first.include?("@hide_from_man_page")
lines = [format_usage_banner(comment_lines.first).chomp]
comment_lines.slice(1..-1)
.each do |line|
line = line.slice(4..-2)
unless line
lines.last << "\n"
next
end
# Omit the common global_options documented separately in the man page.
next if line.match?(/--(debug|help|quiet|verbose) /)
# Format one option or a comma-separated pair of short and long options.
2024-03-10 03:22:53 +00:00
line.gsub!(/^ +(-+[a-z-]+), (-+[a-z-]+) +(.*)$/, "`\\1`, `\\2`\n\n: \\3\n")
line.gsub!(/^ +(-+[a-z-]+) +(.*)$/, "`\\1`\n\n: \\2\n")
lines << line
end
lines.last << "\n"
lines
end
sig { returns(String) }
def self.global_cask_options_manpage
2024-04-30 11:10:23 +02:00
lines = ["These options are applicable to the `install`, `reinstall` and `upgrade` " \
"subcommands with the `--cask` switch.\n"]
2023-11-05 00:54:59 +00:00
lines += Homebrew::CLI::Parser.global_cask_options.map do |_, long, kwargs|
generate_option_doc(nil, long.chomp("="), kwargs.fetch(:description))
end
lines.join("\n")
end
sig { returns(String) }
def self.global_options_manpage
lines = ["These options are applicable across multiple subcommands.\n"]
lines += Homebrew::CLI::Parser.global_options.map do |short, long, desc|
generate_option_doc(short, long, desc)
end
lines.join("\n")
end
sig { returns(String) }
def self.env_vars_manpage
lines = Homebrew::EnvConfig::ENVS.flat_map do |env, hash|
2024-03-10 03:22:53 +00:00
entry = "`#{env}`\n\n: #{hash[:description]}\n"
default = hash[:default_text]
default ||= "`#{hash[:default]}`." if hash[:default]
2024-03-10 03:22:53 +00:00
entry += "\n\n *Default:* #{default}\n" if default
entry
end
lines.join("\n")
end
def self.format_opt(opt)
"`#{opt}`" unless opt.nil?
end
def self.generate_option_doc(short, long, desc)
comma = (short && long) ? ", " : ""
<<~EOS
2024-03-10 03:22:53 +00:00
#{format_opt(short)}#{comma}#{format_opt(long)}
: #{desc}
EOS
end
def self.format_usage_banner(usage_banner)
usage_banner&.sub(/^(#: *\* )?/, "### ")
2024-03-10 03:22:53 +00:00
&.gsub(/(?<!`)\[([^\[\]]*)\](?!`)/, "\\[\\1\\]") # escape [] character (except those in code spans)
end
end
end