mirror of
https://github.com/mhx/dwarfs.git
synced 2025-09-08 03:49:44 -04:00
feat: add support for built-in manual pages
This commit is contained in:
parent
3c3de6ce9e
commit
6994f9691e
@ -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
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
41
cmake/render_manpage.cmake
Normal file
41
cmake/render_manpage.cmake
Normal 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
328
cmake/render_manpage.py
Normal 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
51
include/dwarfs/manpage.h
Normal 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
30
include/dwarfs/pager.h
Normal 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
|
32
include/dwarfs/render_manpage.h
Normal file
32
include/dwarfs/render_manpage.h
Normal 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
|
@ -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
96
src/dwarfs/pager.cpp
Normal 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
|
87
src/dwarfs/render_manpage.cpp
Normal file
87
src/dwarfs/render_manpage.cpp
Normal 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
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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")) {
|
||||
|
@ -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")) {
|
||||
|
@ -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
64
test/manpage_test.cpp
Normal 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()));
|
Loading…
x
Reference in New Issue
Block a user