From 6994f9691e8ff1e7ea486cba0c96f47365e01b3e Mon Sep 17 00:00:00 2001 From: Marcus Holland-Moritz Date: Thu, 18 Jan 2024 14:58:08 +0100 Subject: [PATCH] feat: add support for built-in manual pages --- .docker/Dockerfile | 1 + .docker/build-linux.sh | 2 +- CMakeLists.txt | 30 ++- cmake/render_manpage.cmake | 41 ++++ cmake/render_manpage.py | 328 ++++++++++++++++++++++++++++++++ include/dwarfs/manpage.h | 51 +++++ include/dwarfs/pager.h | 30 +++ include/dwarfs/render_manpage.h | 32 ++++ include/dwarfs/tool.h | 9 + src/dwarfs/pager.cpp | 96 ++++++++++ src/dwarfs/render_manpage.cpp | 87 +++++++++ src/dwarfs/tool.cpp | 21 ++ src/dwarfs_main.cpp | 32 ++++ src/dwarfsck_main.cpp | 7 + src/dwarfsextract_main.cpp | 7 + src/mkdwarfs_main.cpp | 7 + test/manpage_test.cpp | 64 +++++++ 17 files changed, 840 insertions(+), 5 deletions(-) create mode 100644 cmake/render_manpage.cmake create mode 100644 cmake/render_manpage.py create mode 100644 include/dwarfs/manpage.h create mode 100644 include/dwarfs/pager.h create mode 100644 include/dwarfs/render_manpage.h create mode 100644 src/dwarfs/pager.cpp create mode 100644 src/dwarfs/render_manpage.cpp create mode 100644 test/manpage_test.cpp diff --git a/.docker/Dockerfile b/.docker/Dockerfile index c7a15e26..0317762c 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -54,6 +54,7 @@ RUN apt install -y \ libgoogle-glog-dev \ libutfcpp-dev \ libflac++-dev \ + python3-mistletoe \ bash-completion COPY install-static-libs.sh /usr/local/bin/install-static-libs.sh RUN bash /usr/local/bin/install-static-libs.sh diff --git a/.docker/build-linux.sh b/.docker/build-linux.sh index 3b941d4c..596a2110 100644 --- a/.docker/build-linux.sh +++ b/.docker/build-linux.sh @@ -80,7 +80,7 @@ if [[ "-$BUILD_TYPE-" == *-nojemalloc-* ]]; then fi if [[ "-$BUILD_TYPE-" == *-noperfmon-* ]]; then - CMAKE_ARGS="${CMAKE_ARGS} -DENABLE_PERFMON=0" + CMAKE_ARGS="${CMAKE_ARGS} -DENABLE_PERFMON=0 -DWITH_MAN_OPTION=0" fi if [[ "-$BUILD_TYPE-" == *-static-* ]]; then diff --git a/CMakeLists.txt b/CMakeLists.txt index eb7fdd42..39d7388e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ include(CheckCXXSourceCompiles) option(WITH_TESTS "build with tests" OFF) option(WITH_BENCHMARKS "build with benchmarks" OFF) option(WITH_FUZZ "build with fuzzing binaries" OFF) +option(WITH_MAN_OPTION "build with --man option" ON) option(ENABLE_PERFMON "enable performance monitor in all tools" ON) option(ENABLE_FLAC "build with FLAC support" ON) if(WIN32) @@ -116,6 +117,7 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") # Apply /MT or /MTd (multithread, static version of the run-time library) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:Embedded>") + add_compile_definitions(_WIN32_WINNT=0x0601 WINVER=0x0601) endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") @@ -381,9 +383,7 @@ if(NOT endif() endif() -list( - APPEND - LIBDWARFS_SRC +list(APPEND LIBDWARFS_SRC src/dwarfs/block_cache.cpp src/dwarfs/block_compressor.cpp src/dwarfs/block_compressor_parser.cpp @@ -442,7 +442,24 @@ list( src/dwarfs/terminal.cpp src/dwarfs/util.cpp src/dwarfs/wcwidth.c - src/dwarfs/worker_group.cpp) + src/dwarfs/worker_group.cpp +) + +if(WITH_MAN_OPTION) + include(${CMAKE_SOURCE_DIR}/cmake/render_manpage.cmake) + + list(APPEND LIBDWARFS_SRC + src/dwarfs/pager.cpp + src/dwarfs/render_manpage.cpp + ) + + foreach(man mkdwarfs dwarfs dwarfsck dwarfsextract) + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/src/dwarfs") + add_manpage_source(doc/${man}.md NAME ${man} + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/src/dwarfs/${man}_manpage.cpp) + list(APPEND LIBDWARFS_SRC ${CMAKE_CURRENT_BINARY_DIR}/src/dwarfs/${man}_manpage.cpp) + endforeach() +endif() # Just an example for setting per file compile options # set_source_files_properties(src/dwarfs/segmenter.cpp PROPERTIES COMPILE_FLAGS -march=tigerlake) @@ -640,6 +657,10 @@ if(WITH_TESTS) utils_test ) + if(WITH_MAN_OPTION) + list(APPEND DWARFS_TESTS manpage_test) + endif() + if(FLAC_FOUND) list(APPEND DWARFS_TESTS flac_compressor_test) endif() @@ -777,6 +798,7 @@ foreach(tgt dwarfs dwarfs_compression dwarfs_categorizer ${tgt} PRIVATE DWARFS_HAVE_LIBZSTD DWARFS_STATIC_BUILD=${STATIC_BUILD_DO_NOT_USE} + $<$:DWARFS_BUILTIN_MANPAGE> $<$:DWARFS_USE_JEMALLOC> $<$:DWARFS_HAVE_LIBMAGIC> $<$:DWARFS_HAVE_LIBLZ4> diff --git a/cmake/render_manpage.cmake b/cmake/render_manpage.cmake new file mode 100644 index 00000000..5014a500 --- /dev/null +++ b/cmake/render_manpage.cmake @@ -0,0 +1,41 @@ +# +# Copyright (c) Marcus Holland-Moritz +# +# This file is part of dwarfs. +# +# dwarfs is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# dwarfs is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# dwarfs. If not, see . +# + +cmake_minimum_required(VERSION 3.25.0) + +function(add_manpage_source markdown) + set(_options) + set(_oneValueArgs NAME OUTPUT) + set(_multiValueArgs) + cmake_parse_arguments(_MANPAGE "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + + find_program(_PYTHON_EXE python python3) + if(NOT _PYTHON_EXE) + find_package(Python3 REQUIRED) + set(_PYTHON_EXE "${Python3_EXECUTABLE}") + endif() + + set(_MANPAGE_GENERATOR "${CMAKE_SOURCE_DIR}/cmake/render_manpage.py") + + add_custom_command( + OUTPUT "${_MANPAGE_OUTPUT}" + COMMAND "${_PYTHON_EXE}" "${_MANPAGE_GENERATOR}" + "${_MANPAGE_NAME}" "${CMAKE_CURRENT_SOURCE_DIR}/${markdown}" "${_MANPAGE_OUTPUT}" + DEPENDS "${markdown}" "${_MANPAGE_GENERATOR}" + ) +endfunction() diff --git a/cmake/render_manpage.py b/cmake/render_manpage.py new file mode 100644 index 00000000..17198ada --- /dev/null +++ b/cmake/render_manpage.py @@ -0,0 +1,328 @@ +import mistletoe +import sys + + +class RenderContext: + def __init__(self): + self.line = 0 + self.level = 0 + self.indent = 4 + self.section = None + + def get_indent(self, add=0): + return self.indent * (max(0, self.level + add)) + + +class Element: + def __init__(self, tags, content=None, comment=None): + self.tags = tags + self.content = content + self.comment = comment + + def __repr__(self): + return f"Element({self.tags}, {self.content}, {self.comment})" + + def tags_to_style(self): + style = [] + if "b" in self.tags: + style.append("fmt::emphasis::bold") + if "i" in self.tags: + style.append("fmt::emphasis::italic") + if "head" in self.tags: + style.append( + "fmt::fg(fmt::terminal_color::bright_green) | fmt::emphasis::bold" + ) + if "code" in self.tags: + style.append( + "fmt::fg(fmt::terminal_color::bright_blue) | fmt::emphasis::bold" + ) + if "block" in self.tags: + style.append("fmt::emphasis::faint") + if len(style) == 0: + return "{}" + return " | ".join(style) + + def render(self, context): + return f'{{{self.tags_to_style()} /* {self.tags} */, R"({self.content})"}} /* {self.comment} */' + + +def apply(elements, *tags): + return [Element({*tags, *e.tags}, e.content, e.comment) for e in elements] + + +class Line: + def __init__(self, elements=None, indent_first=0, indent=None, comment=None): + self.elements = [] if elements is None else elements + self.indent_first = indent_first + self.indent = indent_first if indent is None else indent + self.comment = comment + + def split(self): + rv = [] + cur = [] + indent_first = self.indent_first + for e in self.elements: + if e.comment == "line break": + rv.append(Line(cur, indent_first, self.indent, comment=self.comment)) + indent_first = self.indent + cur = [] + else: + cur.append(e) + rv.append(Line(cur, indent_first, self.indent, comment=self.comment)) + return rv + + def join(self): + rv = [] + for e in self.elements: + if e.comment == "line break": + rv.append(Element(set(), " ", "whitespace")) + else: + rv.append(e) + return Line(rv, self.indent_first, self.indent, comment=self.comment) + + def render(self, context): + template = ( + "constexpr uint32_t const line{0}_indent_first{{{2}}};\n" + "constexpr uint32_t const line{0}_indent_next{{{3}}};\n" + "constexpr std::array const line{0}_elements{{{{\n" + "{4}" + "}}}};\n\n" + ) + rv = "" + if self.comment is not None: + rv += "// " + self.comment + "\n" + rv += template.format( + context.line, + len(self.elements), + self.indent_first, + self.indent, + "".join([f" {e.render(context)},\n" for e in self.elements]), + ) + context.line += 1 + return rv + + +class Paragraph: + def __init__(self, elements, comment=None): + self.elements = elements + self.comment = comment + + def render(self, context, indent_first=None, indent=None): + if indent_first is None: + indent_first = context.get_indent() + if indent is None: + indent = indent_first + line = Line(self.elements, indent_first, indent, comment=self.comment) + if context.section is not None and context.section == "SYNOPSIS": + lines = line.split() + else: + lines = [line.join()] + return "".join([l.render(context) for l in lines]) + Line().render(context) + + +class Heading: + def __init__(self, elements, level, section=None, comment=None): + self.elements = elements + self.level = level + self.section = section + self.comment = comment + + def __repr__(self): + return f"Heading({self.level}, {self.section}, {self.comment})" + + def render(self, context): + context.level = 2 + indent = context.get_indent(-2 if self.level <= 2 else -1) + context.section = self.section + rv = Line(apply(self.elements, "head"), indent, comment=self.comment).render( + context + ) + if self.level == 1: + rv += Line().render(context) + return rv + + +class ListItem: + def __init__(self, paragraphs, comment=None): + self.paragraphs = paragraphs + self.comment = comment + + def __repr__(self): + return f"ListItem({self.paragraphs}, {self.comment})" + + def render(self, context): + rv = "" + for i, p in enumerate(self.paragraphs): + handled = False + if i == 0: + content_len = 0 + for i in range(len(p.elements) - 1): + cur = p.elements[i] + nxt = p.elements[i + 1] + content_len += len(cur.content) + if cur.content.endswith(":") and nxt.comment == "line break": + cur.content = cur.content[:-1] + content_len -= 1 + if content_len < (2 * context.indent - 1): + cur.content += " " * (2 * context.indent - content_len) + p.elements = p.elements[: i + 1] + p.elements[i + 2 :] + rv += p.render( + context, + indent_first=context.get_indent(), + indent=context.get_indent(2), + ) + else: + rv += Line( + p.elements[:i + 1], context.get_indent(), comment=p.comment + ).render(context) + p.elements = p.elements[i + 2 :] + rv += p.render(context, indent_first=context.get_indent(2)) + handled = True + break + if not handled: + rv += p.render(context) + return rv + + +class BlockCode: + def __init__(self, element): + self.element = element + + def render(self, context): + lines = self.element.content.split("\n") + rv = "" + for line in lines: + rv += Line( + [Element({"block"}, line)], + context.get_indent(1), + comment=self.element.comment, + ).render(context) + return rv + + +class ManpageRenderer(mistletoe.base_renderer.BaseRenderer): + def __init__(self, document_name, *extras, **kwargs): + self.__document_name = document_name + super().__init__(*extras, **kwargs) + + def render_inner(self, token, tags=None): + rv = [] + for child in token.children: + c = self.render(child) + if isinstance(c, list): + rv.extend(c) + else: + rv.append(c) + if tags is not None: + for child in rv: + child.tags.update(tags) + return rv + + @staticmethod + def render_thematic_break(token): + raise NotImplementedError + + @staticmethod + def render_line_break(token): + return Element(set(), "", "line break") + + def render_inline_code(self, token): + assert len(token.children) == 1 + return Element({"code"}, token.children[0].content, "inline code") + + def render_raw_text(self, token, escape=True): + return Element(set(), token.content, "raw text") + + def render_strikethrough(self, token): + raise NotImplementedError + + def render_escape_sequence(self, token): + return self.render_inner(token) + + def render_strong(self, token): + return self.render_inner(token, {"b"}) + + def render_emphasis(self, token): + return self.render_inner(token, {"i"}) + + def render_image(self, token): + raise NotImplementedError + + def render_heading(self, token): + inner = self.render_inner(token, {"h{}".format(token.level)}) + section = None + if len(inner) == 1: + assert isinstance(inner[0], Element) + section = inner[0].content + return Heading(inner, token.level, section) + + def render_paragraph(self, token): + inner = self.render_inner(token) + return Paragraph(inner, "paragraph") + + def render_block_code(self, token): + inner = self.render_inner(token) + assert len(inner) == 1 + assert isinstance(inner[0], Element) + return BlockCode(inner[0]) + + def render_list(self, token): + return [self.render(child) for child in token.children] + + def render_list_item(self, token): + inner = self.render_inner(token) + assert isinstance(inner, list) + assert isinstance(inner[0], Paragraph) + return ListItem(inner, "list item") + + def render_table(self, token): + raise NotImplementedError + + def render_table_row(self, token): + raise NotImplementedError + + def render_math(self, token): + raise NotImplementedError + + def render_table_cell(self, token): + raise NotImplementedError + + def render_document(self, token): + rv = """#include +#include "dwarfs/manpage.h" + +namespace dwarfs::manpage { + +namespace { +""" + ctx = RenderContext() + for child in token.children: + r = self.render(child) + rv += f"#if 0\n{r}\n#endif\n" + if isinstance(r, list): + for e in r: + rv += e.render(ctx) + else: + rv += r.render(ctx) + rv += f"constexpr std::array const document_array{{{{\n" + for i in range(ctx.line): + rv += f" {{line{i}_indent_first, line{i}_indent_next, line{i}_elements}},\n" + rv += f"""}}}}; +}} // namespace + +document get_{self.__document_name}_manpage() {{ return document_array; }} + +}} // namespace dwarfs::manpage +""" + return rv + + +doc_name = sys.argv[1] +input_file = sys.argv[2] +output_file = sys.argv[3] + +with open(input_file, "r") as fin: + with ManpageRenderer(doc_name) as renderer: + doc = renderer.render(mistletoe.Document(fin)) + with open(output_file, "w") as fout: + fout.write(doc) diff --git a/include/dwarfs/manpage.h b/include/dwarfs/manpage.h new file mode 100644 index 00000000..908dce2f --- /dev/null +++ b/include/dwarfs/manpage.h @@ -0,0 +1,51 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#pragma once + +#include +#include +#include + +#include +#include + +namespace dwarfs::manpage { + +struct element { + fmt::text_style style; + std::string_view text; +}; + +struct line { + uint32_t indent_first; + uint32_t indent_next; + std::span elements; +}; + +using document = std::span; + +document get_mkdwarfs_manpage(); +document get_dwarfs_manpage(); +document get_dwarfsck_manpage(); +document get_dwarfsextract_manpage(); + +} // namespace dwarfs::manpage diff --git a/include/dwarfs/pager.h b/include/dwarfs/pager.h new file mode 100644 index 00000000..44f5e604 --- /dev/null +++ b/include/dwarfs/pager.h @@ -0,0 +1,30 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#pragma once + +#include + +namespace dwarfs { + +bool show_in_pager(std::string text); + +} // namespace dwarfs diff --git a/include/dwarfs/render_manpage.h b/include/dwarfs/render_manpage.h new file mode 100644 index 00000000..848cd226 --- /dev/null +++ b/include/dwarfs/render_manpage.h @@ -0,0 +1,32 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#pragma once + +#include + +#include "dwarfs/manpage.h" + +namespace dwarfs { + +std::string render_manpage(manpage::document doc, size_t width, bool color); + +} // namespace dwarfs diff --git a/include/dwarfs/tool.h b/include/dwarfs/tool.h index e6ff8c8c..3f471333 100644 --- a/include/dwarfs/tool.h +++ b/include/dwarfs/tool.h @@ -26,9 +26,14 @@ #include +#ifdef DWARFS_BUILTIN_MANPAGE +#include "dwarfs/manpage.h" +#endif + namespace dwarfs { struct logger_options; +struct iolayer; std::string tool_header(std::string_view tool_name, std::string_view extra_info = ""); @@ -36,4 +41,8 @@ tool_header(std::string_view tool_name, std::string_view extra_info = ""); void add_common_options(boost::program_options::options_description& opts, logger_options& logopts); +#ifdef DWARFS_BUILTIN_MANPAGE +void show_manpage(manpage::document doc, iolayer const& iol); +#endif + } // namespace dwarfs diff --git a/src/dwarfs/pager.cpp b/src/dwarfs/pager.cpp new file mode 100644 index 00000000..a7bb0c85 --- /dev/null +++ b/src/dwarfs/pager.cpp @@ -0,0 +1,96 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#include +#include +#include + +#include +#include + +#include "dwarfs/pager.h" + +namespace dwarfs { + +namespace { + +namespace bp = boost::process; + +struct pager_def { + std::string name; + std::vector args; +}; + +std::vector const pagers{ + {"less", {"-R"}}, +}; + +auto find_executable(std::string name) { return bp::search_path(name); } + +std::pair> find_pager() { + if (auto pager_env = std::getenv("PAGER")) { + std::string_view sv(pager_env); + if (sv.starts_with('"') && sv.ends_with('"')) { + sv.remove_prefix(1); + sv.remove_suffix(1); + } + if (sv == "cat") { + return {}; + } + boost::filesystem::path p{std::string(sv)}; + if (boost::filesystem::exists(p)) { + return {p.string(), {}}; + } + if (auto exe = find_executable(pager_env); !exe.empty()) { + return {exe, {}}; + } + } + + for (auto const& p : pagers) { + if (auto exe = find_executable(std::string(p.name)); !exe.empty()) { + return {exe, p.args}; + } + } + + return {}; +} + +} // namespace + +bool show_in_pager(std::string text) { + auto [pager_exe, pager_args] = find_pager(); + + if (pager_exe.empty()) { + return false; + } + + boost::asio::io_service ios; + bp::child proc(pager_exe, bp::args(pager_args), + bp::std_in = + boost::asio::const_buffer(text.data(), text.size()), + bp::std_out > stdout, ios); + ios.run(); + proc.wait(); + + return true; +} + +} // namespace dwarfs diff --git a/src/dwarfs/render_manpage.cpp b/src/dwarfs/render_manpage.cpp new file mode 100644 index 00000000..5ec469b1 --- /dev/null +++ b/src/dwarfs/render_manpage.cpp @@ -0,0 +1,87 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#include + +#include "dwarfs/render_manpage.h" + +namespace dwarfs { + +std::string render_manpage(manpage::document const doc, size_t const width, + bool const color) { + static constexpr std::string_view punct = ".,:;!?"; + static constexpr size_t right_margin = 4; + size_t const effective_width = width - right_margin; + std::string out; + auto out_it = std::back_inserter(out); + + for (auto const& l : doc) { + uint32_t indent = l.indent_first; + uint32_t column = indent; + + fmt::format_to(out_it, "{}", std::string(indent, ' ')); + + for (size_t i = 0; i < l.elements.size(); ++i) { + auto e = l.elements[i]; + auto* next = (i + 1 < l.elements.size()) ? &l.elements[i + 1] : nullptr; + auto t = e.text; + auto style = color ? e.style : fmt::text_style{}; + + while (column + t.size() > effective_width) { + auto wp = t.rfind(' ', effective_width - column); + + if (wp == std::string_view::npos && column == indent) { + wp = effective_width - column; + } + + if (wp != std::string_view::npos) { + fmt::format_to(out_it, style, "{}", t.substr(0, wp)); + column += wp; + t = t.substr(wp + 1); + } + + indent = l.indent_next; + fmt::format_to(out_it, "\n{}", std::string(indent, ' ')); + column = indent; + } + + if (column + t.size() > effective_width) { + throw std::logic_error("line too long"); + } + + if (column + t.size() == effective_width && next && + next->text.size() == 1 && + punct.find(next->text[0]) != std::string_view::npos) { + indent = l.indent_next; + fmt::format_to(out_it, "\n{}", std::string(indent, ' ')); + column = indent; + } + + fmt::format_to(out_it, style, "{}", t); + column += t.size(); + } + fmt::format_to(out_it, "\n"); + } + + return out; +} + +} // namespace dwarfs diff --git a/src/dwarfs/tool.cpp b/src/dwarfs/tool.cpp index 20c15ebc..cb1546bd 100644 --- a/src/dwarfs/tool.cpp +++ b/src/dwarfs/tool.cpp @@ -27,6 +27,13 @@ #include "dwarfs/tool.h" #include "dwarfs/version.h" +#ifdef DWARFS_BUILTIN_MANPAGE +#include "dwarfs/iolayer.h" +#include "dwarfs/pager.h" +#include "dwarfs/render_manpage.h" +#include "dwarfs/terminal.h" +#endif + namespace po = boost::program_options; namespace boost { @@ -71,10 +78,24 @@ void add_common_options(po::options_description& opts, ("log-with-context", po::value>(&logopts.with_context)->zero_tokens(), "enable context logging regardless of level") +#ifdef DWARFS_BUILTIN_MANPAGE + ("man", + "show manual page and exit") +#endif ("help,h", "output help message and exit") ; // clang-format on } +#ifdef DWARFS_BUILTIN_MANPAGE +void show_manpage(manpage::document doc, iolayer const& iol) { + auto const fancy = iol.term->is_fancy(iol.out); + auto content = render_manpage(doc, iol.term->width(), fancy); + if (!show_in_pager(content)) { + iol.out << content; + } +} +#endif + } // namespace dwarfs diff --git a/src/dwarfs_main.cpp b/src/dwarfs_main.cpp index 6a08d826..fcb1af9a 100644 --- a/src/dwarfs_main.cpp +++ b/src/dwarfs_main.cpp @@ -145,6 +145,9 @@ struct dwarfs_userdata { dwarfs_userdata& operator=(dwarfs_userdata const&) = delete; bool is_help{false}; +#ifdef DWARFS_BUILTIN_MANPAGE + bool is_man{false}; +#endif options opts; stream_logger lgr; filesystem_v2 fs; @@ -966,6 +969,9 @@ void usage(std::ostream& os, std::filesystem::path const& progname) { << " -o tidy_max_age=TIME tidy blocks after this time (10m)\n" #if DWARFS_PERFMON_ENABLED << " -o perfmon=name[,...] enable performance monitor\n" +#endif +#ifdef DWARFS_BUILTIN_MANPAGE + << " --man show manual page and exit\n" #endif << "\n"; @@ -1009,6 +1015,13 @@ int option_hdl(void* data, char const* arg, int key, userdata.is_help = true; return -1; } + +#ifdef DWARFS_BUILTIN_MANPAGE + if (::strncmp(arg, "--man", 5) == 0) { + userdata.is_man = true; + return -1; + } +#endif break; default: @@ -1252,6 +1265,12 @@ int dwarfs_main(int argc, sys_char** argv, iolayer const& iol) { struct fuse_cmdline_opts fuse_opts; if (fuse_parse_cmdline(&args, &fuse_opts) == -1 || !fuse_opts.mountpoint) { +#ifdef DWARFS_BUILTIN_MANPAGE + if (userdata.is_man) { + show_manpage(manpage::get_dwarfs_manpage(), iol); + return 0; + } +#endif usage(iol.out, opts.progname); return userdata.is_help ? 0 : 1; } @@ -1266,6 +1285,12 @@ int dwarfs_main(int argc, sys_char** argv, iolayer const& iol) { int mt, fg; if (fuse_parse_cmdline(&args, &mountpoint, &mt, &fg) == -1 || !mountpoint) { +#ifdef DWARFS_BUILTIN_MANPAGE + if (userdata.is_man) { + show_manpage(manpage::get_dwarfs_manpage(), iol); + return 0; + } +#endif usage(iol.out, opts.progname); return userdata.is_help ? 0 : 1; } @@ -1337,6 +1362,13 @@ int dwarfs_main(int argc, sys_char** argv, iolayer const& iol) { return 1; } +#ifdef DWARFS_BUILTIN_MANPAGE + if (userdata.is_man) { + show_manpage(manpage::get_dwarfs_manpage(), iol); + return 0; + } +#endif + if (!opts.seen_mountpoint) { usage(iol.out, opts.progname); return 1; diff --git a/src/dwarfsck_main.cpp b/src/dwarfsck_main.cpp index b718e231..6596454a 100644 --- a/src/dwarfsck_main.cpp +++ b/src/dwarfsck_main.cpp @@ -115,6 +115,13 @@ int dwarfsck_main(int argc, sys_char** argv, iolayer const& iol) { return 1; } +#ifdef DWARFS_BUILTIN_MANPAGE + if (vm.count("man")) { + show_manpage(manpage::get_dwarfsck_manpage(), iol); + return 0; + } +#endif + auto constexpr usage = "Usage: dwarfsck [OPTIONS...]\n"; if (vm.count("help") or !vm.count("input")) { diff --git a/src/dwarfsextract_main.cpp b/src/dwarfsextract_main.cpp index b199a9de..38bb7bdd 100644 --- a/src/dwarfsextract_main.cpp +++ b/src/dwarfsextract_main.cpp @@ -105,6 +105,13 @@ int dwarfsextract_main(int argc, sys_char** argv, iolayer const& iol) { return 1; } +#ifdef DWARFS_BUILTIN_MANPAGE + if (vm.count("man")) { + show_manpage(manpage::get_dwarfsextract_manpage(), iol); + return 0; + } +#endif + auto constexpr usage = "Usage: dwarfsextract [OPTIONS...]\n"; if (vm.count("help") or !vm.count("input")) { diff --git a/src/mkdwarfs_main.cpp b/src/mkdwarfs_main.cpp index d725648f..f316a490 100644 --- a/src/mkdwarfs_main.cpp +++ b/src/mkdwarfs_main.cpp @@ -660,6 +660,13 @@ int mkdwarfs_main(int argc, sys_char** argv, iolayer const& iol) { return 1; } +#ifdef DWARFS_BUILTIN_MANPAGE + if (vm.count("man")) { + show_manpage(manpage::get_mkdwarfs_manpage(), iol); + return 0; + } +#endif + auto constexpr usage = "Usage: mkdwarfs [OPTIONS...]\n"; if (vm.count("long-help")) { diff --git a/test/manpage_test.cpp b/test/manpage_test.cpp new file mode 100644 index 00000000..17a31c3b --- /dev/null +++ b/test/manpage_test.cpp @@ -0,0 +1,64 @@ +/* vim:set ts=2 sw=2 sts=2 et: */ +/** + * \author Marcus Holland-Moritz (github@mhxnet.de) + * \copyright Copyright (c) Marcus Holland-Moritz + * + * This file is part of dwarfs. + * + * dwarfs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * dwarfs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with dwarfs. If not, see . + */ + +#include +#include + +#include +#include + +#include "dwarfs/render_manpage.h" + +using namespace dwarfs; + +namespace { + +std::map const docs = { + {"mkdwarfs", manpage::get_mkdwarfs_manpage()}, + {"dwarfs", manpage::get_dwarfs_manpage()}, + {"dwarfsck", manpage::get_dwarfsck_manpage()}, + {"dwarfsextract", manpage::get_dwarfsextract_manpage()}, +}; + +} + +class manpage_render_test + : public ::testing::TestWithParam> {}; + +TEST_P(manpage_render_test, basic) { + auto [name, color] = GetParam(); + auto doc = docs.at(name); + for (size_t width = 20; width <= 200; width += 1) { + auto out = render_manpage(doc, width, color); + EXPECT_GT(out.size(), 1000); + EXPECT_THAT(out, ::testing::HasSubstr(name)); + EXPECT_THAT(out, ::testing::HasSubstr("SYNOPSIS")); + EXPECT_THAT(out, ::testing::HasSubstr("DESCRIPTION")); + EXPECT_THAT(out, ::testing::HasSubstr("AUTHOR")); + EXPECT_THAT(out, ::testing::HasSubstr("COPYRIGHT")); + } +} + +INSTANTIATE_TEST_SUITE_P( + dwarfs, manpage_render_test, + ::testing::Combine(::testing::Values("mkdwarfs", "dwarfs", "dwarfsck", + "dwarfsextract"), + ::testing::Bool()));