aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/dev-cmd/man.rb
blob: 4e510391082335e692bd66455e4ca5fe751f648c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#:  * `man` [`--fail-if-changed`]:
#:    Generate Homebrew's manpages.
#:
#:    If `--fail-if-changed` is passed, the command will return a failing
#:    status code if changes are detected in the manpage outputs.
#:    This can be used for CI to be notified when the manpages are out of date.
#:    Additionally, the date used in new manpages will match those in the existing
#:    manpages (to allow comparison without factoring in the date).

require "formula"
require "erb"
require "ostruct"

module Homebrew
  module_function

  SOURCE_PATH = HOMEBREW_LIBRARY_PATH/"manpages"
  TARGET_MAN_PATH = HOMEBREW_REPOSITORY/"manpages"
  TARGET_DOC_PATH = HOMEBREW_REPOSITORY/"docs"

  def man
    raise UsageError unless ARGV.named.empty?

    if ARGV.flag? "--link"
      odie "`brew man --link` is now done automatically by `brew update`."
    end

    regenerate_man_pages

    if system "git", "-C", HOMEBREW_REPOSITORY, "diff", "--quiet", "docs/Manpage.md", "manpages"
      puts "No changes to manpage output detected."
    elsif ARGV.include?("--fail-if-changed")
      Homebrew.failed = true
    end
  end

  def regenerate_man_pages
    Homebrew.install_gem_setup_path! "ronn"

    markup = build_man_page
    convert_man_page(markup, TARGET_DOC_PATH/"Manpage.md")
    convert_man_page(markup, TARGET_MAN_PATH/"brew.1")

    cask_markup = (SOURCE_PATH/"brew-cask.1.md").read
    convert_man_page(cask_markup, TARGET_MAN_PATH/"brew-cask.1")
  end

  def path_glob_commands(glob)
    Pathname.glob(glob)
            .sort_by { |source_file| sort_key_for_path(source_file) }
            .map do |source_file|
      source_file.read.lines
                 .grep(/^#:/)
                 .map { |line| line.slice(2..-1) }
                 .join
    end.reject { |s| s.strip.empty? || s.include?("@hide_from_man_page") }
  end

  def build_man_page
    template = (SOURCE_PATH/"brew.1.md.erb").read
    variables = OpenStruct.new

    variables[:commands] = path_glob_commands("#{HOMEBREW_LIBRARY_PATH}/cmd/*.{rb,sh}")
    variables[:developer_commands] = path_glob_commands("#{HOMEBREW_LIBRARY_PATH}/dev-cmd/*.{rb,sh}")
    readme = HOMEBREW_REPOSITORY/"README.md"
    variables[:lead_maintainer] = readme.read[/(Homebrew's lead maintainer .*\.)/, 1]
                                        .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:maintainers] = readme.read[/(Homebrew's current maintainers .*\.)/, 1]
                                    .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
    variables[:former_maintainers] = readme.read[/(Former maintainers .*\.)/, 1]
                                           .gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')

    ERB.new(template, nil, ">").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 checking for changes.
    # This avoids the only change being e.g. a new date.
    date = if ARGV.include?("--fail-if-changed") &&
              target.extname == ".1" && target.exist?
      /"(\d{1,2})" "([A-Z][a-z]+) (\d{4})" "#{organisation}" "#{manual}"/ =~ target.read
      Date.parse("#{$1} #{$2} #{$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
      ronn_output.gsub!(%r{</?var>}, "`") if format_flag == "--markdown"
      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
end