From d89344b41de6f9a51840869300924bd59962620a Mon Sep 17 00:00:00 2001 From: hyuraku <32809703+hyuraku@users.noreply.github.com> Date: Tue, 8 Nov 2022 20:33:30 +0900 Subject: [PATCH] add manpage.rb to generating homebrew manual --- .../dev-cmd/generate-man-completions.rb | 223 +---------------- Library/Homebrew/manpages.rb | 228 ++++++++++++++++++ Library/Homebrew/manpages.rbi | 7 + 3 files changed, 237 insertions(+), 221 deletions(-) create mode 100644 Library/Homebrew/manpages.rb create mode 100644 Library/Homebrew/manpages.rbi diff --git a/Library/Homebrew/dev-cmd/generate-man-completions.rb b/Library/Homebrew/dev-cmd/generate-man-completions.rb index dfdd8dbebe..cacae0ff9e 100644 --- a/Library/Homebrew/dev-cmd/generate-man-completions.rb +++ b/Library/Homebrew/dev-cmd/generate-man-completions.rb @@ -2,20 +2,15 @@ # frozen_string_literal: true require "formula" -require "erb" require "ostruct" -require "cli/parser" require "completions" +require "manpages" module Homebrew extend T::Sig module_function - SOURCE_PATH = (HOMEBREW_LIBRARY_PATH/"manpages").freeze - TARGET_MAN_PATH = (HOMEBREW_REPOSITORY/"manpages").freeze - TARGET_DOC_PATH = (HOMEBREW_REPOSITORY/"docs").freeze - sig { returns(CLI::Parser) } def generate_man_completions_args Homebrew::CLI::Parser.new do @@ -38,7 +33,7 @@ module Homebrew odeprecated "brew generate-man-completions --fail-if-not-changed" if args.fail_if_not_changed? Commands.rebuild_internal_commands_completion_list - regenerate_man_pages(quiet: args.quiet?) + Manpages.regenerate_man_pages(quiet: args.quiet?) Completions.update_shell_completions! diff = system_command "git", args: [ @@ -50,218 +45,4 @@ module Homebrew puts "Manpage and completions updated." end end - - # TODO: move this method and all called functions to manpages.rb - def regenerate_man_pages(quiet:) - Homebrew.install_bundler_gems! - - markup = build_man_page(quiet: quiet) - convert_man_page(markup, TARGET_DOC_PATH/"Manpage.md") - markup = I18n.transliterate(markup, locale: :en) - convert_man_page(markup, TARGET_MAN_PATH/"brew.1") - end - - def build_man_page(quiet:) - template = (SOURCE_PATH/"brew.1.md.erb").read - variables = OpenStruct.new - - variables[:commands] = generate_cmd_manpages(Commands.internal_commands_paths) - variables[:developer_commands] = generate_cmd_manpages(Commands.internal_developer_commands_paths) - variables[:official_external_commands] = - generate_cmd_manpages(Commands.official_external_commands_paths(quiet: quiet)) - variables[:global_cask_options] = global_cask_options_manpage - variables[:global_options] = global_options_manpage - variables[:environment_variables] = env_vars_manpage - - readme = HOMEBREW_REPOSITORY/"README.md" - variables[:lead] = - readme.read[/(Homebrew's \[Project Leader.*\.)/, 1] - .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') - variables[:plc] = - readme.read[/(Homebrew's \[Project Leadership Committee.*\.)/, 1] - .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') - variables[:tsc] = - readme.read[/(Homebrew's \[Technical Steering Committee.*\.)/, 1] - .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') - variables[:maintainers] = - readme.read[/(Homebrew's other current maintainers .*\.)/, 1] - .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') - variables[:alumni] = - readme.read[/(Former maintainers .*\.)/, 1] - .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') - - ERB.new(template, trim_mode: ">").result(variables.instance_eval { binding }) - end - - def 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 convert_man_page(markup, target) - manual = target.basename(".1") - organisation = "Homebrew" - - # Set the manpage date to the existing one if we're updating. - # This avoids the only change being e.g. a new date. - date = if target.extname == ".1" && target.exist? - /"(\d{1,2})" "([A-Z][a-z]+) (\d{4})" "#{organisation}" "#{manual}"/ =~ target.read - Date.parse("#{Regexp.last_match(1)} #{Regexp.last_match(2)} #{Regexp.last_match(3)}") - else - Date.today - end - date = date.strftime("%Y-%m-%d") - - shared_args = %W[ - --pipe - --organization=#{organisation} - --manual=#{target.basename(".1")} - --date=#{date} - ] - - format_flag, format_desc = target_path_to_format(target) - - puts "Writing #{format_desc} to #{target}" - Utils.popen(["ronn", format_flag] + shared_args, "rb+") do |ronn| - ronn.write markup - ronn.close_write - ronn_output = ronn.read - odie "Got no output from ronn!" if ronn_output.blank? - case format_flag - when "--markdown" - ronn_output = ronn_output.gsub(%r{(.*?)}, "*`\\1`*") - .gsub(/\n\n\n+/, "\n\n") - .gsub(/^(- `[^`]+`):/, "\\1") # drop trailing colons from definition lists - .gsub(/(?<=\n\n)([\[`].+):\n/, "\\1\n
") # replace colons with
on subcommands - when "--roff" - ronn_output = ronn_output.gsub(%r{(.*?)}, "\\fB\\1\\fR") - .gsub(%r{(.*?)}, "\\fI\\1\\fR") - .gsub(/(^\[?\\fB.+): /, "\\1\n ") - end - target.atomic_write ronn_output - end - end - - def target_path_to_format(target) - case target.basename - when /\.md$/ then ["--markdown", "markdown"] - when /\.\d$/ then ["--roff", "man page"] - else - odie "Failed to infer output format from '#{target.basename}'." - end - end - - def generate_cmd_manpages(cmd_paths) - man_page_lines = [] - - # preserve existing manpage order - cmd_paths.sort_by(&method(:sort_key_for_path)) - .each do |cmd_path| - cmd_man_page_lines = if (cmd_parser = CLI::Parser.from_cmd_path(cmd_path)) - next if cmd_parser.hide_from_man_page - - cmd_parser_manpage_lines(cmd_parser).join - else - cmd_comment_manpage_lines(cmd_path) - end - - man_page_lines << cmd_man_page_lines - end - - man_page_lines.compact.join("\n") - end - - def cmd_parser_manpage_lines(cmd_parser) - lines = [format_usage_banner(cmd_parser.usage_banner_text)] - lines += cmd_parser.processed_options.map do |short, long, _, desc, hidden| - next if hidden - - if long.present? - next if Homebrew::CLI::Parser.global_options.include?([short, long, desc]) - next if Homebrew::CLI::Parser.global_cask_options.any? do |_, option, description:, **| - [long, "#{long}="].include?(option) && description == desc - end - end - - generate_option_doc(short, long, desc) - end.compact - lines - end - - def 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. - lines << line.gsub(/^ +(-+[a-z-]+), (-+[a-z-]+) +/, "* `\\1`, `\\2`:\n ") - .gsub(/^ +(-+[a-z-]+) +/, "* `\\1`:\n ") - end - lines.last << "\n" - lines - end - - sig { returns(String) } - def global_cask_options_manpage - lines = ["These options are applicable to the `install`, `reinstall`, and `upgrade` " \ - "subcommands with the `--cask` flag.\n"] - lines += Homebrew::CLI::Parser.global_cask_options.map do |_, long, description:, **| - generate_option_doc(nil, long.chomp("="), description) - end - lines.join("\n") - end - - sig { returns(String) } - def 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 env_vars_manpage - lines = Homebrew::EnvConfig::ENVS.flat_map do |env, hash| - entry = "- `#{env}`:\n
#{hash[:description]}\n" - default = hash[:default_text] - default ||= "`#{hash[:default]}`." if hash[:default] - entry += "\n\n *Default:* #{default}\n" if default - - entry - end - lines.join("\n") - end - - def generate_option_doc(short, long, desc) - comma = (short && long) ? ", " : "" - <<~EOS - * #{format_short_opt(short)}#{comma}#{format_long_opt(long)}: - #{desc} - EOS - end - - def format_short_opt(opt) - "`#{opt}`" unless opt.nil? - end - - def format_long_opt(opt) - "`#{opt}`" unless opt.nil? - end - - def format_usage_banner(usage_banner) - usage_banner&.sub(/^(#: *\* )?/, "### ") - end end diff --git a/Library/Homebrew/manpages.rb b/Library/Homebrew/manpages.rb new file mode 100644 index 0000000000..9b222573ac --- /dev/null +++ b/Library/Homebrew/manpages.rb @@ -0,0 +1,228 @@ +# typed: true +# frozen_string_literal: true + +require "cli/parser" +require "erb" + +SOURCE_PATH = (HOMEBREW_LIBRARY_PATH/"manpages").freeze +TARGET_MAN_PATH = (HOMEBREW_REPOSITORY/"manpages").freeze +TARGET_DOC_PATH = (HOMEBREW_REPOSITORY/"docs").freeze +module Homebrew + # Helper functions for generating homebrew manual. + # + # @api private + module Manpages + extend T::Sig + + module_function + + def regenerate_man_pages(quiet:) + Homebrew.install_bundler_gems! + + markup = build_man_page(quiet: quiet) + convert_man_page(markup, TARGET_DOC_PATH/"Manpage.md") + markup = I18n.transliterate(markup, locale: :en) + convert_man_page(markup, TARGET_MAN_PATH/"brew.1") + end + + def build_man_page(quiet:) + template = (SOURCE_PATH/"brew.1.md.erb").read + variables = OpenStruct.new + + variables[:commands] = generate_cmd_manpages(Commands.internal_commands_paths) + variables[:developer_commands] = generate_cmd_manpages(Commands.internal_developer_commands_paths) + variables[:official_external_commands] = + generate_cmd_manpages(Commands.official_external_commands_paths(quiet: quiet)) + variables[:global_cask_options] = global_cask_options_manpage + variables[:global_options] = global_options_manpage + variables[:environment_variables] = env_vars_manpage + + readme = HOMEBREW_REPOSITORY/"README.md" + variables[:lead] = + readme.read[/(Homebrew's \[Project Leader.*\.)/, 1] + .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') + variables[:plc] = + readme.read[/(Homebrew's \[Project Leadership Committee.*\.)/, 1] + .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') + variables[:tsc] = + readme.read[/(Homebrew's \[Technical Steering Committee.*\.)/, 1] + .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') + variables[:maintainers] = + readme.read[/(Homebrew's other current maintainers .*\.)/, 1] + .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') + variables[:alumni] = + readme.read[/(Former maintainers .*\.)/, 1] + .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') + + ERB.new(template, trim_mode: ">").result(variables.instance_eval { binding }) + end + + def 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 convert_man_page(markup, target) + manual = target.basename(".1") + organisation = "Homebrew" + + # Set the manpage date to the existing one if we're updating. + # This avoids the only change being e.g. a new date. + date = if target.extname == ".1" && target.exist? + /"(\d{1,2})" "([A-Z][a-z]+) (\d{4})" "#{organisation}" "#{manual}"/ =~ target.read + Date.parse("#{Regexp.last_match(1)} #{Regexp.last_match(2)} #{Regexp.last_match(3)}") + else + Date.today + end + date = date.strftime("%Y-%m-%d") + + shared_args = %W[ + --pipe + --organization=#{organisation} + --manual=#{target.basename(".1")} + --date=#{date} + ] + + format_flag, format_desc = target_path_to_format(target) + + puts "Writing #{format_desc} to #{target}" + Utils.popen(["ronn", format_flag] + shared_args, "rb+") do |ronn| + ronn.write markup + ronn.close_write + ronn_output = ronn.read + odie "Got no output from ronn!" if ronn_output.blank? + ronn_output = case format_flag + when "--markdown" + onn_output.gsub(%r{(.*?)}, "*`\\1`*") + .gsub(/\n\n\n+/, "\n\n") + .gsub(/^(- `[^`]+`):/, "\\1") # drop trailing colons from definition lists + .gsub(/(?<=\n\n)([\[`].+):\n/, "\\1\n
") # replace colons with
on subcommands + when "--roff" + ronn_output.gsub(%r{(.*?)}, "\\fB\\1\\fR") + .gsub(%r{(.*?)}, "\\fI\\1\\fR") + .gsub(/(^\[?\\fB.+): /, "\\1\n ") + end + target.atomic_write ronn_output + end + end + + def target_path_to_format(target) + case target.basename + when /\.md$/ then ["--markdown", "markdown"] + when /\.\d$/ then ["--roff", "man page"] + else + odie "Failed to infer output format from '#{target.basename}'." + end + end + + def generate_cmd_manpages(cmd_paths) + man_page_lines = [] + + # preserve existing manpage order + cmd_paths.sort_by(&method(:sort_key_for_path)) + .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 + cmd_comment_manpage_lines(cmd_path) + end + + man_page_lines << cmd_man_page_lines + end + + man_page_lines.compact.join("\n") + end + + def cmd_parser_manpage_lines(cmd_parser) + lines = [format_usage_banner(cmd_parser.usage_banner_text)] + lines += cmd_parser.processed_options.map do |short, long, _, desc, hidden| + next if hidden + + if long.present? + next if Homebrew::CLI::Parser.global_options.include?([short, long, desc]) + next if Homebrew::CLI::Parser.global_cask_options.any? do |_, option, description:, **| + [long, "#{long}="].include?(option) && description == desc + end + end + + generate_option_doc(short, long, desc) + end.compact + lines + end + + def 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. + lines << line.gsub(/^ +(-+[a-z-]+), (-+[a-z-]+) +/, "* `\\1`, `\\2`:\n ") + .gsub(/^ +(-+[a-z-]+) +/, "* `\\1`:\n ") + end + lines.last << "\n" + lines + end + + sig { returns(String) } + def global_cask_options_manpage + lines = ["These options are applicable to the `install`, `reinstall`, and `upgrade` " \ + "subcommands with the `--cask` flag.\n"] + lines += Homebrew::CLI::Parser.global_cask_options.map do |_, long, description:, **| + generate_option_doc(nil, long.chomp("="), description) + end + lines.join("\n") + end + + sig { returns(String) } + def 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 env_vars_manpage + lines = Homebrew::EnvConfig::ENVS.flat_map do |env, hash| + entry = "- `#{env}`:\n
#{hash[:description]}\n" + default = hash[:default_text] + default ||= "`#{hash[:default]}`." if hash[:default] + entry += "\n\n *Default:* #{default}\n" if default + + entry + end + lines.join("\n") + end + + def format_opt(opt) + "`#{opt}`" unless opt.nil? + end + + def generate_option_doc(short, long, desc) + comma = (short && long) ? ", " : "" + <<~EOS + * #{format_opt(short)}#{comma}#{format_opt(long)}: + #{desc} + EOS + end + + def format_usage_banner(usage_banner) + usage_banner&.sub(/^(#: *\* )?/, "### ") + end + end +end diff --git a/Library/Homebrew/manpages.rbi b/Library/Homebrew/manpages.rbi new file mode 100644 index 0000000000..d3765f73a3 --- /dev/null +++ b/Library/Homebrew/manpages.rbi @@ -0,0 +1,7 @@ +# typed: strict + +module Homebrew + module Manpages + include Kernel + end +end