2021-09-10 19:37:42 +00:00
/*
* Copyright ( c ) 2021 , Ben Wiederhake < BenWiederhake . GitHub @ gmx . de >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
/*
* You may want to invoke the checker like this :
2023-05-19 23:36:52 +00:00
* $ ninja - C Build / lagom
* $ export SERENITY_SOURCE_DIR = / path / to / serenity
2023-03-18 15:23:35 +00:00
* $ find AK Base Documentation Kernel Meta Ports Tests Userland - type f - name ' * . md ' - print0 | xargs - 0 Build / lagom / bin / markdown - check README . md CONTRIBUTING . md
2021-09-10 19:37:42 +00:00
*/
# include <AK/Format.h>
# include <AK/HashMap.h>
# include <AK/HashTable.h>
# include <AK/LexicalPath.h>
2022-01-12 11:40:36 +00:00
# include <AK/RecursionDecision.h>
2022-02-25 12:44:52 +00:00
# include <AK/URL.h>
2021-09-10 19:37:42 +00:00
# include <AK/Vector.h>
2022-03-28 19:12:18 +00:00
# include <LibCore/ArgsParser.h>
2023-02-09 02:02:46 +00:00
# include <LibCore/File.h>
2023-03-21 15:35:30 +00:00
# include <LibFileSystem/FileSystem.h>
2022-03-28 18:59:25 +00:00
# include <LibMain/Main.h>
2023-07-02 11:20:23 +00:00
# include <LibManual/PageNode.h>
# include <LibManual/Path.h>
# include <LibManual/SectionNode.h>
2021-09-10 19:37:42 +00:00
# include <LibMarkdown/Document.h>
# include <LibMarkdown/Visitor.h>
2022-02-26 15:51:24 +00:00
# include <stdlib.h>
2021-09-10 19:37:42 +00:00
2023-05-19 23:35:18 +00:00
static bool is_missing_file_acceptable ( String const & filename )
2021-10-06 20:55:12 +00:00
{
const StringView acceptable_missing_files [ ] = {
2022-02-25 14:01:22 +00:00
// FIXME: Please write these manpages!
2022-07-11 17:32:29 +00:00
" /usr/share/man/man2/exec.md " sv ,
" /usr/share/man/man2/fcntl.md " sv ,
" /usr/share/man/man2/fork.md " sv ,
" /usr/share/man/man2/ioctl.md " sv ,
" /usr/share/man/man2/listen.md " sv ,
" /usr/share/man/man2/mmap.md " sv ,
" /usr/share/man/man2/mprotect.md " sv ,
" /usr/share/man/man2/open.md " sv ,
" /usr/share/man/man2/ptrace.md " sv ,
" /usr/share/man/man5/perfcore.md " sv ,
2021-10-20 19:52:34 +00:00
// These ones are okay:
2022-07-11 17:32:29 +00:00
" /home/anon/Tests/js-tests/test-common.js " sv ,
" /man1/index.html " sv ,
" /man2/index.html " sv ,
" /man3/index.html " sv ,
" /man4/index.html " sv ,
" /man5/index.html " sv ,
" /man6/index.html " sv ,
" /man7/index.html " sv ,
" /man8/index.html " sv ,
" index.html " sv ,
2021-10-06 20:55:12 +00:00
} ;
for ( auto const & suffix : acceptable_missing_files ) {
2023-05-19 23:35:18 +00:00
if ( filename . ends_with_bytes ( suffix ) )
2021-10-06 20:55:12 +00:00
return true ;
}
return false ;
}
2021-09-10 19:37:42 +00:00
struct FileLink {
2023-12-16 14:19:34 +00:00
ByteString file_path ; // May be empty, but not null
ByteString anchor ; // May be null ("foo.md", "bar.png"), may be empty ("baz.md#")
ByteString label ; // May be empty, but not null
2021-09-10 19:37:42 +00:00
} ;
class MarkdownLinkage final : Markdown : : Visitor {
public :
~ MarkdownLinkage ( ) = default ;
2023-07-25 17:52:06 +00:00
static MarkdownLinkage analyze ( Markdown : : Document const & , bool verbose ) ;
2021-09-10 19:37:42 +00:00
2023-12-16 14:19:34 +00:00
bool has_anchor ( ByteString const & anchor ) const { return m_anchors . contains ( anchor ) ; }
HashTable < ByteString > const & anchors ( ) const { return m_anchors ; }
2022-02-25 12:42:51 +00:00
bool has_invalid_link ( ) const { return m_has_invalid_link ; }
2021-09-10 19:37:42 +00:00
Vector < FileLink > const & file_links ( ) const { return m_file_links ; }
private :
2023-07-25 17:52:06 +00:00
MarkdownLinkage ( bool verbose )
: m_verbose ( verbose )
2022-02-26 15:51:24 +00:00
{
auto const * source_directory = getenv ( " SERENITY_SOURCE_DIR " ) ;
if ( source_directory ! = nullptr ) {
m_serenity_source_directory = source_directory ;
} else {
warnln ( " The environment variable SERENITY_SOURCE_DIR was not found. Link checking inside Serenity's filesystem will fail. " ) ;
}
}
2021-09-10 19:37:42 +00:00
virtual RecursionDecision visit ( Markdown : : Heading const & ) override ;
virtual RecursionDecision visit ( Markdown : : Text : : LinkNode const & ) override ;
2023-12-16 14:19:34 +00:00
HashTable < ByteString > m_anchors ;
2021-09-10 19:37:42 +00:00
Vector < FileLink > m_file_links ;
2022-02-25 12:42:51 +00:00
bool m_has_invalid_link { false } ;
2023-07-25 17:52:06 +00:00
bool m_verbose { false } ;
2022-02-26 15:51:24 +00:00
2023-12-16 14:19:34 +00:00
ByteString m_serenity_source_directory ;
2021-09-10 19:37:42 +00:00
} ;
2023-07-25 17:52:06 +00:00
MarkdownLinkage MarkdownLinkage : : analyze ( Markdown : : Document const & document , bool verbose )
2021-09-10 19:37:42 +00:00
{
2023-07-25 17:52:06 +00:00
MarkdownLinkage linkage ( verbose ) ;
2021-09-10 19:37:42 +00:00
document . walk ( linkage ) ;
return linkage ;
}
class StringCollector final : Markdown : : Visitor {
public :
StringCollector ( ) = default ;
virtual ~ StringCollector ( ) = default ;
2023-12-16 14:19:34 +00:00
ByteString build ( ) { return m_builder . to_byte_string ( ) ; }
2021-09-10 19:37:42 +00:00
2023-12-16 14:19:34 +00:00
static ByteString from ( Markdown : : Heading const & heading )
2021-09-10 19:37:42 +00:00
{
StringCollector collector ;
heading . walk ( collector ) ;
return collector . build ( ) ;
}
2023-12-16 14:19:34 +00:00
static ByteString from ( Markdown : : Text : : Node const & node )
2021-09-10 19:37:42 +00:00
{
StringCollector collector ;
node . walk ( collector ) ;
return collector . build ( ) ;
}
private :
2023-12-16 14:19:34 +00:00
virtual RecursionDecision visit ( ByteString const & text ) override
2021-09-10 19:37:42 +00:00
{
m_builder . append ( text ) ;
return RecursionDecision : : Recurse ;
}
StringBuilder m_builder ;
} ;
2023-12-16 14:19:34 +00:00
static ByteString slugify ( ByteString const & text )
2021-09-10 19:37:42 +00:00
{
// TODO: This feels like it belongs into LibWeb.
2023-12-16 14:19:34 +00:00
ByteString slug = text . to_lowercase ( ) ;
2021-09-10 19:37:42 +00:00
// Reverse-engineered through github, using:
// find AK/ Base/ Documentation/ Kernel/ Meta/ Ports/ Tests/ Userland/ -name '*.md' | xargs grep --color=always -Pin '^##+ .*[^a-z0-9 ?()`_:/!&|.$'"'"',<>"+-]' README.md
2022-07-11 17:32:29 +00:00
slug = slug . replace ( " " sv , " - " sv , ReplaceMode : : All )
. replace ( " ! " sv , " " sv , ReplaceMode : : All )
. replace ( " ? " sv , " " sv , ReplaceMode : : All )
. replace ( " ( " sv , " " sv , ReplaceMode : : All )
. replace ( " ) " sv , " " sv , ReplaceMode : : All )
. replace ( " : " sv , " " sv , ReplaceMode : : All )
. replace ( " / " sv , " - " sv , ReplaceMode : : All )
. replace ( " & " sv , " " sv , ReplaceMode : : All )
. replace ( " | " sv , " " sv , ReplaceMode : : All )
. replace ( " . " sv , " " sv , ReplaceMode : : All )
. replace ( " $ " sv , " " sv , ReplaceMode : : All )
. replace ( " ' " sv , " " sv , ReplaceMode : : All )
. replace ( " , " sv , " " sv , ReplaceMode : : All )
. replace ( " \" " sv , " " sv , ReplaceMode : : All )
. replace ( " + " sv , " " sv , ReplaceMode : : All )
. replace ( " \\ " sv , " " sv , ReplaceMode : : All )
. replace ( " < " sv , " " sv , ReplaceMode : : All )
. replace ( " > " sv , " " sv , ReplaceMode : : All ) ;
2021-09-10 19:37:42 +00:00
// What about "="?
return slug ;
}
RecursionDecision MarkdownLinkage : : visit ( Markdown : : Heading const & heading )
{
m_anchors . set ( slugify ( StringCollector : : from ( heading ) ) ) ;
return RecursionDecision : : Recurse ;
}
RecursionDecision MarkdownLinkage : : visit ( Markdown : : Text : : LinkNode const & link_node )
{
2023-12-16 14:19:34 +00:00
ByteString const & href = link_node . href ;
2023-10-10 11:30:58 +00:00
if ( href . is_empty ( ) ) {
2021-09-10 19:37:42 +00:00
// Nothing to do here.
return RecursionDecision : : Recurse ;
}
2023-10-10 11:30:58 +00:00
2022-02-25 12:44:52 +00:00
auto url = URL : : create_with_url_or_path ( href ) ;
if ( url . is_valid ( ) ) {
if ( url . scheme ( ) = = " https " | | url . scheme ( ) = = " http " ) {
2023-07-25 17:52:06 +00:00
if ( m_verbose )
outln ( " Not checking external link {} " , href ) ;
2022-02-25 12:44:52 +00:00
return RecursionDecision : : Recurse ;
}
if ( url . scheme ( ) = = " help " ) {
2023-08-08 02:26:17 +00:00
if ( url . host ( ) ! = " man " _string ) {
2022-02-25 12:47:41 +00:00
warnln ( " help:// URL without 'man': {} " , href ) ;
m_has_invalid_link = true ;
return RecursionDecision : : Recurse ;
}
2023-04-14 19:12:03 +00:00
if ( url . path_segment_count ( ) < 2 ) {
2022-02-25 12:47:41 +00:00
warnln ( " help://man URL is missing section or page: {} " , href ) ;
m_has_invalid_link = true ;
return RecursionDecision : : Recurse ;
}
2022-12-14 14:48:19 +00:00
// Remove leading '/' from the path.
2023-12-16 14:19:34 +00:00
auto file = ByteString : : formatted ( " {}/Base/usr/share/man/man{}.md " , m_serenity_source_directory , url . serialize_path ( ) . substring ( 1 ) ) ;
2022-02-25 12:47:41 +00:00
2023-12-16 14:19:34 +00:00
m_file_links . append ( { file , ByteString ( ) , StringCollector : : from ( * link_node . text ) } ) ;
2022-02-25 12:44:52 +00:00
return RecursionDecision : : Recurse ;
}
if ( url . scheme ( ) = = " file " ) {
2023-04-14 19:12:03 +00:00
auto file_path = url . serialize_path ( ) ;
if ( file_path . contains ( " man " sv ) & & file_path . ends_with ( " .md " sv ) ) {
2023-01-07 16:05:33 +00:00
warnln ( " Inter-manpage link without the help:// scheme: {} \n Please use help URLs of the form 'help://man/<section>/<subsection...>/<page>' " , href ) ;
m_has_invalid_link = true ;
return RecursionDecision : : Recurse ;
}
2022-02-26 15:51:24 +00:00
// TODO: Check more possible links other than icons.
2023-04-14 19:12:03 +00:00
if ( file_path . starts_with ( " /res/icons/ " sv ) ) {
2023-12-16 14:19:34 +00:00
auto file = ByteString : : formatted ( " {}/Base{} " , m_serenity_source_directory , file_path ) ;
m_file_links . append ( { file , ByteString ( ) , StringCollector : : from ( * link_node . text ) } ) ;
2023-04-14 19:12:03 +00:00
} else if ( file_path . starts_with ( " /bin " sv ) ) {
2023-01-07 16:15:19 +00:00
StringBuilder builder ;
link_node . text - > render_to_html ( builder ) ;
auto link_text = builder . string_view ( ) ;
if ( link_text ! = " Open " sv ) {
warnln ( " Binary link named '{}' is not allowed, binary links must be called 'Open'. Linked binary: {} " , link_text , href ) ;
m_has_invalid_link = true ;
}
2023-07-25 17:52:06 +00:00
} else if ( m_verbose ) {
2023-01-07 16:15:19 +00:00
outln ( " Not checking local link {} " , href ) ;
2022-02-25 12:44:52 +00:00
}
return RecursionDecision : : Recurse ;
}
2022-01-22 17:31:02 +00:00
}
2021-09-10 19:37:42 +00:00
2023-12-16 14:19:34 +00:00
ByteString label = StringCollector : : from ( * link_node . text ) ;
2021-09-10 19:37:42 +00:00
Optional < size_t > last_hash = href . find_last ( ' # ' ) ;
if ( last_hash . has_value ( ) ) {
m_file_links . append ( { href . substring ( 0 , last_hash . value ( ) ) , href . substring ( last_hash . value ( ) + 1 ) , label } ) ;
} else {
2023-12-16 14:19:34 +00:00
m_file_links . append ( { href , ByteString ( ) , label } ) ;
2021-09-10 19:37:42 +00:00
}
return RecursionDecision : : Recurse ;
}
2023-07-02 11:20:23 +00:00
static ErrorOr < String > generate_link_graph ( HashMap < NonnullRefPtr < Manual : : PageNode const > , Vector < NonnullRefPtr < Manual : : PageNode const > > > const & page_links )
{
auto const header = " digraph manpage_links { \n " sv ;
StringBuilder builder ;
TRY ( builder . try_append ( header ) ) ;
// Not displayed to the user.
HashMap < NonnullRefPtr < Manual : : PageNode const > , String > page_identifiers ;
for ( auto const & page : page_links . keys ( ) ) {
auto path = TRY ( page - > path ( ) ) ;
StringBuilder identifier_builder ;
// Only allow alphanumerics, replace everything else with underscores.
for ( auto const & character : path . code_points ( ) ) {
if ( AK : : is_ascii_alphanumeric ( character ) )
TRY ( identifier_builder . try_append_code_point ( character ) ) ;
else
TRY ( identifier_builder . try_append ( ' _ ' ) ) ;
}
auto const identifier = TRY ( identifier_builder . to_string ( ) ) ;
TRY ( builder . try_appendff ( " {} [label= \" {}({}) \" ]; \n " , identifier , TRY ( page - > name ( ) ) , page - > section_number ( ) ) ) ;
TRY ( page_identifiers . try_set ( page , identifier ) ) ;
}
for ( auto const & from_page_list : page_links ) {
auto const & from_page = from_page_list . key ;
for ( auto const & to_page : from_page_list . value ) {
auto const to_page_identifier = page_identifiers . get ( to_page ) ;
// Target page doesn't actually exist; it's probably an ignored page.
if ( ! to_page_identifier . has_value ( ) )
continue ;
TRY ( builder . try_appendff ( " {} -> {}; \n " , page_identifiers . get ( from_page ) . value ( ) , page_identifiers . get ( to_page ) . value ( ) ) ) ;
}
}
TRY ( builder . try_append ( " } \n " sv ) ) ;
return builder . to_string ( ) ;
}
2022-03-28 18:59:25 +00:00
ErrorOr < int > serenity_main ( Main : : Arguments arguments )
2021-09-10 19:37:42 +00:00
{
2022-03-28 19:12:18 +00:00
Core : : ArgsParser args_parser ;
Vector < StringView > file_paths ;
2023-07-02 11:20:23 +00:00
bool output_link_graph { false } ;
2023-07-25 17:52:06 +00:00
bool verbose_output { false } ;
2023-07-02 11:20:23 +00:00
StringView base_path = " / " sv ;
2022-03-28 19:12:18 +00:00
args_parser . add_positional_argument ( file_paths , " Path to markdown files to read and parse " , " paths " , Core : : ArgsParser : : Required : : Yes ) ;
2023-07-02 11:20:23 +00:00
args_parser . add_option ( base_path , " System base path (default: \" / \" ) " , " base " , ' b ' , " path " ) ;
args_parser . add_option ( output_link_graph , " Output a page link graph into \" manpage-links.gv \" . The recommended tool to process this graph is `fdp`. " , " link-graph " , ' g ' ) ;
2023-07-25 17:52:06 +00:00
args_parser . add_option ( verbose_output , " Print extra information about skipped links " , " verbose " , ' v ' ) ;
2022-03-28 19:12:18 +00:00
args_parser . parse ( arguments ) ;
2021-09-10 19:37:42 +00:00
2023-07-25 17:52:06 +00:00
if ( verbose_output )
outln ( " Reading and parsing Markdown files ... " ) ;
2024-01-15 16:23:24 +00:00
// FIXME: Use ByteString for file paths
2023-05-19 23:35:18 +00:00
HashMap < String , MarkdownLinkage > files ;
2022-03-28 19:12:18 +00:00
for ( auto path : file_paths ) {
2023-02-09 02:02:46 +00:00
auto file_or_error = Core : : File : : open ( path , Core : : File : : OpenMode : : Read ) ;
2021-09-10 19:37:42 +00:00
if ( file_or_error . is_error ( ) ) {
2022-09-14 15:18:44 +00:00
warnln ( " Failed to open {}: {} " , path , file_or_error . error ( ) ) ;
2021-09-10 19:37:42 +00:00
// Since this should never happen anyway, fail early.
2022-03-28 18:59:25 +00:00
return file_or_error . release_error ( ) ;
2021-09-10 19:37:42 +00:00
}
auto file = file_or_error . release_value ( ) ;
2022-09-14 15:18:44 +00:00
2022-12-11 16:49:00 +00:00
auto content_buffer_or_error = file - > read_until_eof ( ) ;
2022-09-14 15:18:44 +00:00
if ( content_buffer_or_error . is_error ( ) ) {
warnln ( " Failed to read {}: {} " , path , file_or_error . error ( ) ) ;
// Since this should never happen anyway, fail early.
return file_or_error . release_error ( ) ;
}
auto content_buffer = content_buffer_or_error . release_value ( ) ;
2021-09-10 19:37:42 +00:00
auto content = StringView ( content_buffer ) ;
auto document = Markdown : : Document : : parse ( content ) ;
if ( ! document ) {
warnln ( " Failed to parse {} due to an unspecified error. " , path ) ;
// Since this should never happen anyway, fail early.
return 1 ;
}
2024-01-15 16:23:24 +00:00
files . set ( TRY ( String : : from_byte_string ( TRY ( FileSystem : : real_path ( path ) ) ) ) , MarkdownLinkage : : analyze ( * document , verbose_output ) ) ;
2021-09-10 19:37:42 +00:00
}
2023-07-25 17:52:06 +00:00
if ( verbose_output )
outln ( " Checking links ... " ) ;
2021-09-10 19:37:42 +00:00
bool any_problems = false ;
for ( auto const & file_item : files ) {
2022-02-25 12:42:51 +00:00
if ( file_item . value . has_invalid_link ( ) ) {
outln ( " File '{}' has invalid links. " , file_item . key ) ;
any_problems = true ;
continue ;
}
2023-12-16 14:19:34 +00:00
auto file_lexical_path = LexicalPath ( file_item . key . to_byte_string ( ) ) ;
2021-09-10 19:37:42 +00:00
auto file_dir = file_lexical_path . dirname ( ) ;
for ( auto const & file_link : file_item . value . file_links ( ) ) {
2023-05-19 23:35:18 +00:00
String pointee_file ;
2021-09-10 19:37:42 +00:00
if ( file_link . file_path . is_empty ( ) ) {
pointee_file = file_item . key ;
} else {
2023-12-16 14:19:34 +00:00
pointee_file = TRY ( String : : from_byte_string ( LexicalPath : : absolute_path ( file_dir , file_link . file_path ) ) ) ;
2021-09-10 19:37:42 +00:00
}
2023-03-21 15:35:30 +00:00
if ( ! FileSystem : : exists ( pointee_file ) & & ! is_missing_file_acceptable ( pointee_file ) ) {
2021-09-10 19:37:42 +00:00
outln ( " File '{}' points to '{}' (label '{}'), but '{}' does not exist! " ,
file_item . key , file_link . file_path , file_link . label , pointee_file ) ;
any_problems = true ;
continue ;
}
if ( file_link . anchor . is_empty ( ) ) {
// No anchor to test for.
continue ;
}
auto pointee_linkage = files . find ( pointee_file ) ;
if ( pointee_linkage = = files . end ( ) ) {
outln ( " File '{}' points to file '{}', which exists, but was not scanned. Add it to the command-line arguments and re-run. " ,
file_item . key , pointee_file ) ;
any_problems = true ;
continue ;
}
if ( ! pointee_linkage - > value . has_anchor ( file_link . anchor ) ) {
outln ( " File '{}' points to '{}#{}' (label '{}'), but file '{}' does not have any heading that results in the anchor '{}'. " ,
file_item . key , file_link . file_path , file_link . anchor , file_link . label , pointee_file , file_link . anchor ) ;
out ( " The following anchors seem to be available: \n " ) ;
bool any_anchors = false ;
for ( auto const & anchor : pointee_linkage - > value . anchors ( ) ) {
if ( any_anchors )
out ( " , " ) ;
out ( " '{}' " , anchor ) ;
any_anchors = true ;
}
if ( ! any_anchors )
out ( " (none) " ) ;
outln ( ) ;
any_problems = true ;
}
}
}
2023-07-02 11:20:23 +00:00
if ( output_link_graph ) {
// First, collect all pages, and collect links between pages in a second step after all pages must have been collected.
HashMap < String , NonnullRefPtr < Manual : : PageNode const > > pages ;
for ( auto const & file : files ) {
auto base_relative_path = TRY ( String : : formatted ( " /{} " , LexicalPath : : relative_path ( file . key , base_path ) ) ) ;
auto page = Manual : : Node : : try_create_from_query ( { base_relative_path } ) ;
if ( page . is_error ( ) ) {
dbgln ( " Not including {} in the link graph since it's not a man page. " , file . key ) ;
continue ;
}
TRY ( pages . try_set ( file . key , page . value ( ) ) ) ;
for ( auto const & link : file . value . file_links ( ) ) {
auto base_relative_path = TRY ( String : : formatted ( " /{} " , LexicalPath : : relative_path ( link . file_path , base_path ) ) ) ;
auto maybe_target_page = Manual : : Node : : try_create_from_query ( { base_relative_path } ) ;
if ( maybe_target_page . is_error ( ) ) {
dbgln ( " Not including {} in the link graph since it's not a man page. " , link . file_path ) ;
continue ;
}
2023-12-16 14:19:34 +00:00
TRY ( pages . try_set ( TRY ( String : : from_byte_string ( link . file_path ) ) , maybe_target_page . value ( ) ) ) ;
2023-07-02 11:20:23 +00:00
}
}
HashMap < NonnullRefPtr < Manual : : PageNode const > , Vector < NonnullRefPtr < Manual : : PageNode const > > > page_links ;
for ( auto const & file : files ) {
auto page = pages . get ( file . key ) ;
if ( ! page . has_value ( ) )
continue ;
Vector < NonnullRefPtr < Manual : : PageNode const > > linked_pages ;
for ( auto const & link : file . value . file_links ( ) ) {
2023-12-16 14:19:34 +00:00
auto linked_page = pages . get ( TRY ( String : : from_byte_string ( link . file_path ) ) ) ;
2023-07-02 11:20:23 +00:00
if ( ! linked_page . has_value ( ) )
continue ;
TRY ( linked_pages . try_append ( * linked_page . value ( ) ) ) ;
}
TRY ( page_links . try_set ( * page . value ( ) , move ( linked_pages ) ) ) ;
}
auto const graph_text = TRY ( generate_link_graph ( page_links ) ) ;
auto const graph_file = TRY ( Core : : File : : open ( " manpage-links.gv " sv , Core : : File : : OpenMode : : Write | Core : : File : : OpenMode : : Truncate ) ) ;
TRY ( graph_file - > write_until_depleted ( graph_text . bytes ( ) ) ) ;
}
2021-09-10 19:37:42 +00:00
if ( any_problems ) {
outln ( " Done. Some errors were encountered, please check above log. " ) ;
return 1 ;
2023-07-25 17:52:06 +00:00
} else if ( verbose_output ) {
2021-09-10 19:37:42 +00:00
outln ( " Done. No problems detected. " ) ;
}
2023-07-25 17:52:06 +00:00
return 0 ;
2021-09-10 19:37:42 +00:00
}