module ManLookup
Constants
- COMMAND_PATTERN
- MAN_BIN
- MAX_DESCRIPTION_LENGTH
- MAX_OPTION_LINES
- MAX_SYNOPSIS_LENGTH
- ONLINE_BASE
- SECTION_HEADERS
- SECTION_PATTERN
- STYLE_TITLE
- USER_AGENT
Public Instance Methods
Source
# File lib/man_lookup.rb, line 64 def build_argv(command, section) argv = [MAN_BIN, "-P", "cat"] argv << section.to_s if section argv << command.to_s argv end
Source
# File lib/man_lookup.rb, line 76 def clean_man_line(line) TextUtil.squish(line.to_s.gsub(/.\x08/, "").tr("\t", " ")) end
Source
# File lib/man_lookup.rb, line 52 def command_token?(token) token.to_s.match?(COMMAND_PATTERN) end
Source
# File lib/man_lookup.rb, line 206 def default_fetch lambda do |url| RabbotHttp.fetch(url, user_agent: USER_AGENT) end end
Source
# File lib/man_lookup.rb, line 199 def default_run lambda do |argv| require "open3" Open3.capture3(*argv) end end
Source
# File lib/man_lookup.rb, line 60 def effective_section(section) section || 1 end
Source
# File lib/man_lookup.rb, line 158 def empty_parsed { name: nil, synopsis: nil, description: nil, options: nil } end
Source
# File lib/man_lookup.rb, line 135 def extract_sections(raw) sections = {} current = nil buffer = [] raw.to_s.each_line do |line| header = line.strip if SECTION_HEADERS.include?(header) sections[current] = buffer.join("\n") if current current = header buffer = [] next end next unless current buffer << line end sections[current] = buffer.join("\n") if current sections end
Source
# File lib/man_lookup.rb, line 212 def fetch_online(command, section, fetch:) html = fetch.call(online_url(command, section)) parse_man_html(html) rescue StandardError empty_parsed end
Source
# File lib/man_lookup.rb, line 114 def first_paragraph(text) paragraphs = text.to_s.split(/\n\s*\n+/).map { |part| join_paragraph(part.lines) }.reject(&:empty?) paragraphs.first end
Source
# File lib/man_lookup.rb, line 219 def format_header_title(command, section) bits = [command.to_s] bits << "section #{effective_section(section)}" bits.join(" ยท ") end
Source
# File lib/man_lookup.rb, line 252 def format_invalid_query [ IrcFormat.report_header(tag: "MAN", title: "invalid query", style: STYLE_TITLE), "Use letters, numbers, dots, underscores, hyphens, and an optional section (1-9).", IrcFormat.report_footer(usage_message) ].join("\n") end
Source
# File lib/man_lookup.rb, line 243 def format_no_results(command, section:) title = format_header_title(command, section) [ IrcFormat.report_header(tag: "MAN", title: title, style: STYLE_TITLE), "No manual page found locally or on man7.org โ check the command name and section.", IrcFormat.report_footer("man7.org") ].join("\n") end
Source
# File lib/man_lookup.rb, line 225 def format_reply(parsed, command:, section:, source:) title = format_header_title(command, section) lines = [IrcFormat.report_header(tag: "MAN", title: title, style: STYLE_TITLE)] lines << parsed[:name] unless parsed[:name].to_s.strip.empty? lines << "Usage: #{parsed[:synopsis]}" unless parsed[:synopsis].to_s.strip.empty? lines << parsed[:description] unless parsed[:description].to_s.strip.empty? unless parsed[:options].to_s.strip.empty? parsed[:options].to_s.lines.each do |option_line| cleaned = option_line.strip lines << cleaned unless cleaned.empty? end end lines << IrcFormat.report_footer("man", "section #{effective_section(section)}", source) lines.join("\n") end
Source
# File lib/man_lookup.rb, line 56 def invalid_query { command: nil, section: nil, invalid: true } end
Source
# File lib/man_lookup.rb, line 80 def join_paragraph(lines) clean_man_line(lines.join(" ")) end
Source
# File lib/man_lookup.rb, line 183 def local_miss?(output, status) value = output.to_s.strip return true if value.empty? return true if value.match?(/no (manual )?entry/i) return true if value.match?(/nothing appropriate/i) return true if !status.success? && parsed_empty?(parse_man_text(value)) parsed_empty?(parse_man_text(value)) end
Source
# File lib/man_lookup.rb, line 260 def lookup(text, run: nil, fetch: nil) runner = run || default_run parsed_query = parse_query(text) return usage_message if parsed_query[:command].nil? && !parsed_query[:invalid] return format_invalid_query if parsed_query[:invalid] command = parsed_query[:command] section = parsed_query[:section] http_fetch = fetch || default_fetch output, status = run_local(command, section, run: runner) unless local_miss?(output, status) body = parse_man_text(output) return format_reply(body, command: command, section: section, source: "local") unless parsed_empty?(body) end online = fetch_online(command, section, fetch: http_fetch) return format_reply(online, command: command, section: section, source: "man7.org") unless parsed_empty?(online) format_no_results(command, section: section) rescue StandardError format_no_results(parsed_query[:command] || text.to_s.strip, section: parsed_query[:section]) end
Source
# File lib/man_lookup.rb, line 71 def online_url(command, section) sect = effective_section(section) "#{ONLINE_BASE}/man#{sect}/#{command}.#{sect}.html" end
Source
# File lib/man_lookup.rb, line 162 def parse_man_html(html) doc = Nokogiri.HTML(html.to_s) sections = {} doc.css("h2").each do |heading| id = heading["id"] || heading.at_css("a[id]")&.[]("id") next unless id && SECTION_HEADERS.include?(id) pre = heading.xpath("following-sibling::pre[1]").first sections[id] = pre.text if pre end parse_man_text(sections.map { |name, body| "#{name}\n#{body}" }.join("\n\n")) end
Source
# File lib/man_lookup.rb, line 123 def parse_man_text(raw) sections = extract_sections(raw) return empty_parsed if sections.empty? { name: parse_name_section(sections["NAME"]), synopsis: truncate_text(join_paragraph(sections["SYNOPSIS"].to_s.lines), MAX_SYNOPSIS_LENGTH), description: truncate_text(first_paragraph(sections["DESCRIPTION"]), MAX_DESCRIPTION_LENGTH), options: parse_options_section(sections["OPTIONS"] || sections["ARGUMENTS"] || sections["DESCRIPTION"]) } end
Source
# File lib/man_lookup.rb, line 84 def parse_name_section(text) value = text.to_s.strip return nil if value.empty? if (match = value.match(/\A(.+?)\s+-\s+(.+)\z/m)) return clean_man_line(match[2]) end clean_man_line(value) end
Source
# File lib/man_lookup.rb, line 95 def parse_options_section(text) lines = text.to_s.lines.map { |line| clean_man_line(line) }.reject(&:empty?) return nil if lines.empty? snippets = [] current = nil lines.each do |line| if line.match?(/\A-[a-zA-Z-]/) snippets << current if current current = line elsif current current = "#{current} #{line}" end break if snippets.length >= MAX_OPTION_LINES end snippets << current if current && snippets.length < MAX_OPTION_LINES snippets&.first(MAX_OPTION_LINES)&.join("\n") end
Source
# File lib/man_lookup.rb, line 27 def parse_query(text) tokens = text.to_s.strip.split(/\s+/).reject(&:empty?) return { command: nil, section: nil, invalid: false } if tokens.empty? section = nil command = nil tokens.each do |token| if token.match?(SECTION_PATTERN) return invalid_query if section section = token.to_i elsif command_token?(token) return invalid_query if command command = token else return invalid_query end end return invalid_query if command.nil? { command: command, section: section, invalid: false } end
Source
# File lib/man_lookup.rb, line 177 def parsed_empty?(parsed) parsed[:name].to_s.strip.empty? && parsed[:synopsis].to_s.strip.empty? && parsed[:description].to_s.strip.empty? end
Source
# File lib/man_lookup.rb, line 193 def run_local(command, section, run:) stdout, stderr, status = run.call(build_argv(command, section)) output = [stdout, stderr].join("\n").strip [output, status] end
Source
# File lib/man_lookup.rb, line 119 def truncate_text(text, max_length) TextUtil.truncate(text, max_length) end
Source
# File lib/man_lookup.rb, line 23 def usage_message "Usage: !man <command> [section] โ Linux manual pages (local man, man7.org fallback)" end