feat: add support for built-in manual pages

This commit is contained in:
Marcus Holland-Moritz 2024-01-18 14:58:08 +01:00
parent 3c3de6ce9e
commit 6994f9691e
17 changed files with 840 additions and 5 deletions

View File

@ -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

View File

@ -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

View File

@ -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$<$<CONFIG:Debug>:Debug>")
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug>: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}
$<$<BOOL:${WITH_MAN_OPTION}>:DWARFS_BUILTIN_MANPAGE>
$<$<BOOL:${USE_JEMALLOC}>:DWARFS_USE_JEMALLOC>
$<$<BOOL:${LIBMAGIC_FOUND}>:DWARFS_HAVE_LIBMAGIC>
$<$<BOOL:${LIBLZ4_FOUND}>:DWARFS_HAVE_LIBLZ4>

View File

@ -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 <https://www.gnu.org/licenses/>.
#
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()

328
cmake/render_manpage.py Normal file
View File

@ -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<element, {1}> 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 <array>
#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<line, {ctx.line}> 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)

51
include/dwarfs/manpage.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstdint>
#include <span>
#include <string_view>
#include <fmt/color.h>
#include <fmt/format.h>
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<element const> elements;
};
using document = std::span<line const>;
document get_mkdwarfs_manpage();
document get_dwarfs_manpage();
document get_dwarfsck_manpage();
document get_dwarfsextract_manpage();
} // namespace dwarfs::manpage

30
include/dwarfs/pager.h Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
namespace dwarfs {
bool show_in_pager(std::string text);
} // namespace dwarfs

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include "dwarfs/manpage.h"
namespace dwarfs {
std::string render_manpage(manpage::document doc, size_t width, bool color);
} // namespace dwarfs

View File

@ -26,9 +26,14 @@
#include <boost/program_options.hpp>
#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

96
src/dwarfs/pager.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <string>
#include <string_view>
#include <vector>
#include <boost/asio/io_service.hpp>
#include <boost/process.hpp>
#include "dwarfs/pager.h"
namespace dwarfs {
namespace {
namespace bp = boost::process;
struct pager_def {
std::string name;
std::vector<std::string> args;
};
std::vector<pager_def> const pagers{
{"less", {"-R"}},
};
auto find_executable(std::string name) { return bp::search_path(name); }
std::pair<boost::filesystem::path, std::vector<std::string>> 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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <stdexcept>
#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

View File

@ -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<std::optional<bool>>(&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

View File

@ -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;

View File

@ -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")) {

View File

@ -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")) {

View File

@ -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")) {

64
test/manpage_test.cpp Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#include <map>
#include <string>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "dwarfs/render_manpage.h"
using namespace dwarfs;
namespace {
std::map<std::string, manpage::document> 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<std::tuple<std::string, bool>> {};
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()));