From 5187aa13aa5cb9bed9eae1125e24218ab903a126 Mon Sep 17 00:00:00 2001 From: Mark Olesen <Mark.Olesen@esi-group.com> Date: Wed, 21 Nov 2018 19:55:28 +0100 Subject: [PATCH] ENH: improve output formatting for usage information - generalize output text wrapping, use for usage notes - add -help-man option for generating manpage content for any OpenFOAM application or solver. bin/tools/foamCreateManpage as helper --- bin/tools/foamCreateCompletionCache | 12 +- bin/tools/foamCreateManpage | 172 ++++++++++++++ etc/config.sh/bash_completion | 8 +- src/OpenFOAM/Make/files | 1 + src/OpenFOAM/global/argList/argList.C | 112 ++------- src/OpenFOAM/global/argList/argList.H | 3 + src/OpenFOAM/global/argList/argListHelp.C | 222 ++++++++++++++++++ .../primitives/strings/stringOps/stringOps.C | 110 ++++++++- .../primitives/strings/stringOps/stringOps.H | 19 ++ 9 files changed, 565 insertions(+), 94 deletions(-) create mode 100755 bin/tools/foamCreateManpage create mode 100644 src/OpenFOAM/global/argList/argListHelp.C diff --git a/bin/tools/foamCreateCompletionCache b/bin/tools/foamCreateCompletionCache index 4a8f56a144b..cf1bcdef507 100755 --- a/bin/tools/foamCreateCompletionCache +++ b/bin/tools/foamCreateCompletionCache @@ -140,7 +140,8 @@ HEADER # -opt1 descrip # -opt2 <arg> descrip # -help-full -# Terminate parsing on first appearance of -help-full +# Ignore -help-man (internal option). +# Terminate parsing on first appearance of -help-full. # - options with '=' (eg, -mode=ugo) are not handled very well at all. # - alternatives (eg, -a, -all) are not handled nicely either, # for these treat ',' like a space to catch the worst of them. @@ -148,12 +149,15 @@ extractOptions() { local appName="$1" local helpText=$($appName -help-full 2>/dev/null | \ - sed -ne 's/^ *//; /^$/d; /^[^-]/d; /^--/d;' \ + sed -ne 's/^ *//; /^$/d; /^[^-]/d; /^--/d; /^-help-man/d;' \ -e 'y/,/ /; s/=.*$/=/;' \ -e '/^-[^ ]* </{ s/^\(-[^ ]* <\).*$/\1/; p; d }' \ -e 's/^\(-[^ ]*\).*$/\1/; p; /^-help-full/q;' \ ) + # After this bit of sed, we have "-opt1" or "-opt2 <" + # for entries without or with arguments. + [ -n "$helpText" ] || { echo "Error calling $appName" 1>&2 return 1 @@ -163,8 +167,8 @@ extractOptions() local argOpts=($(awk '/</ {print $1}' <<< "$helpText")) # Array of options without args, but skip the following: - # -help-compat -help-full - local boolOpts=($(awk '!/</ && !/help-(compat|full)/ {print $1}' <<< "$helpText")) + # -help-compat -help-full etc + local boolOpts=($(awk '!/</ && !/help-/ {print $1}' <<< "$helpText")) appName="${appName##*/}" echo "$appName" 1>&2 diff --git a/bin/tools/foamCreateManpage b/bin/tools/foamCreateManpage new file mode 100755 index 00000000000..ed7ad21ff19 --- /dev/null +++ b/bin/tools/foamCreateManpage @@ -0,0 +1,172 @@ +#!/bin/sh +#------------------------------------------------------------------------------ +# ========= | +# \\ / F ield | OpenFOAM: The Open Source CFD Toolbox +# \\ / O peration | +# \\ / A nd | Copyright (C) 2018 OpenCFD Ltd. +# \\/ M anipulation | +#------------------------------------------------------------------------------ +# License +# This file is part of OpenFOAM, licensed under GNU General Public License +# <http://www.gnu.org/licenses/>. +# +# Script +# foamCreateManpage +# +# Description +# Query OpenFOAM applications with -help-man to generate manpage content. +# +#------------------------------------------------------------------------------ +defaultOutputDir="$WM_PROJECT_DIR/doc/man1" + +usage() { + exec 1>&2 + while [ "$#" -ge 1 ]; do echo "$1"; shift; done + cat<<USAGE + +Usage: ${0##*/} [OPTION] [appName .. [appNameN]] +options: + -dir dir Directory to process + -gzip Compressed output + -o DIR Write to alternative output directory + -version VER Specify an alternative version + -h | -help Print the usage + +Query OpenFOAM applications with -help-man for their manpage content +and redirect to corresponding directory location. +Default input: \$FOAM_APPBIN only. +Default output: $defaultOutputDir + +Uses the search directory if applications are specified. + +Copyright (C) 2018 OpenCFD Ltd. +USAGE + exit 1 +} + +# Report error and exit +die() +{ + exec 1>&2 + echo + echo "Error encountered:" + while [ "$#" -ge 1 ]; do echo " $1"; shift; done + echo + echo "See '${0##*/} -help' for usage" + echo + exit 1 +} + +#------------------------------------------------------------------------------- +searchDirs="$FOAM_APPBIN" +unset gzipFilter sedFilter outputDir + +while [ "$#" -gt 0 ] +do + case "$1" in + -h | -help*) + usage + ;; + -d | -dir) + [ "$#" -ge 2 ] || die "'$1' option requires an argument" + searchDirs="$2" + [ -d "$searchDirs" ] || die "directory not found '$searchDirs'" + shift + ;; + -gz | -gzip) + gzipFilter="gzip" + ;; + -v | -version) + [ "$#" -ge 2 ] || die "'$1' option requires an argument" + version="$2" + sedFilter='s/OpenFOAM-[^\"]*/OpenFOAM-'"$version/" + shift + ;; + -o | -output) + [ "$#" -ge 2 ] || die "'$1' option requires an argument" + outputDir="$2" + shift + ;; + -*) + die "unknown option: '$1'" + ;; + *) + break + ;; + esac + shift +done + +: ${outputDir:=$defaultOutputDir} + +# Verify that output is writeable +if [ -e "$outputDir" ] +then + [ -d "$outputDir" ] && [ -w "$outputDir" ] || \ + die "Cannot write to $outputDir" "Not a directory, or no permission?" +else + mkdir -p "$outputDir" || \ + die "Cannot create directory: $outputDir" +fi + +#------------------------------------------------------------------------------- + +# Pass through content, filter for version and/or gzip +# + +tmpFile="$outputDir/${0##*/}" +trap "rm -fv $tmpFile >/dev/null; exit 0" EXIT TERM INT + +process() +{ + local appName="$1" + local outFile="$outputDir/${appName##*/}.1" + + rm -f "$outFile"*; + + "$appName" -help-man 2>/dev/null >| $tmpFile; + + if grep -F -q "SYNOPSIS" "$tmpFile" 2>/dev/null + then + cat "$tmpFile" | \ + sed -e "${sedFilter:-p}" | "${gzipFilter:-cat}" \ + >| "$outFile${gzipFilter:+.gz}" + + echo "$outFile${gzipFilter:+.gz}" 1>&2 + else + echo "Problem with $appName" 1>&2 + fi +} + +#------------------------------------------------------------------------------ + +# Default to standard search directories +[ "$#" -gt 0 ] || set -- ${searchDirs} + +echo "Generating manpages from OpenFOAM applications" 1>&2 +echo 1>&2 + +for item +do + if [ -d "$item" ] + then + # Process directory for applications - sort with ignore-case + echo "[directory] $item" 1>&2 + choices="$(find $item -maxdepth 1 -executable -type f | sort -f 2>/dev/null)" + for appName in $choices + do + process $appName + done + elif command -v "$item" > /dev/null 2>&1 + then + process $item + else + echo "No such file or directory: $item" 1>&2 + fi +done + +echo 1>&2 +echo "Done" 1>&2 + + +# ----------------------------------------------------------------------------- diff --git a/etc/config.sh/bash_completion b/etc/config.sh/bash_completion index 0a89e3f643b..5fb8b874c9e 100644 --- a/etc/config.sh/bash_completion +++ b/etc/config.sh/bash_completion @@ -109,7 +109,7 @@ _of_complete_() local choices case ${prev} in - -help | -help-compat | -help-full | -doc | -doc-source) + -help | -help-compat | -help-full | -help-man | -doc | -doc-source) # These options are usage - we can stop now. COMPREPLY=() return 0 @@ -138,6 +138,7 @@ _of_complete_() # -opt1 descrip # -opt2 <arg> descrip # -help-full + # Ignore -help-man (internal option). # Terminate parsing on first appearance of -help-full # - options with '=' (eg, -mode=ugo) are not handled very well at all. # - alternatives (eg, -a, -all) are not handled nicely either, @@ -145,12 +146,15 @@ _of_complete_() if [ -z "$choices" ] then local helpText=$($appName -help-full 2>/dev/null | \ - sed -ne 's/^ *//; /^$/d; /^[^-]/d; /^--/d;' \ + sed -ne 's/^ *//; /^$/d; /^[^-]/d; /^--/d; /^-help-man/d;' \ -e 'y/,/ /; s/=.*$/=/;' \ -e '/^-[^ ]* </{ s/^\(-[^ ]* <\).*$/\1/; p; d }' \ -e 's/^\(-[^ ]*\).*$/\1/; p; /^-help-full/q;' \ ) + # After this bit of sed, we have "-opt1" or "-opt2 <" + # for entries without or with arguments. + if [ -z "$helpText" ] then echo "Error calling $appName" 1>&2 diff --git a/src/OpenFOAM/Make/files b/src/OpenFOAM/Make/files index 90ba8622095..a9466aa28d9 100644 --- a/src/OpenFOAM/Make/files +++ b/src/OpenFOAM/Make/files @@ -2,6 +2,7 @@ global/global.Cver /* global/constants/constants.C in global.Cver */ /* global/constants/dimensionedConstants.C in global.Cver */ global/argList/argList.C +global/argList/argListHelp.C global/clock/clock.C global/profiling/profiling.C global/profiling/profilingInformation.C diff --git a/src/OpenFOAM/global/argList/argList.C b/src/OpenFOAM/global/argList/argList.C index e6cd41aca35..245f5d67e5a 100644 --- a/src/OpenFOAM/global/argList/argList.C +++ b/src/OpenFOAM/global/argList/argList.C @@ -436,9 +436,7 @@ void Foam::argList::printOptionUsage const string& str ) { - const auto strLen = str.length(); - - if (!strLen) + if (str.empty()) { Info<< nl; return; @@ -456,84 +454,7 @@ void Foam::argList::printOptionUsage ++start; } - const std::string::size_type textWidth = (usageMax - usageMin); - - // Output with text wrapping - for (std::string::size_type pos = 0; pos < strLen; /*ni*/) - { - // Potential end point and next point - std::string::size_type end = pos + textWidth - 1; - std::string::size_type eol = str.find('\n', pos); - std::string::size_type next = string::npos; - - if (end >= strLen) - { - // No more wrapping needed - end = strLen; - - if (std::string::npos != eol && eol <= end) - { - end = eol; - next = str.find_first_not_of(" \t\n", end); // Next non-space - } - } - else if (std::string::npos != eol && eol <= end) - { - // Embedded '\n' char - end = eol; - next = str.find_first_not_of(" \t\n", end); // Next non-space - } - else if (isspace(str[end])) - { - // Ended on a space - can use this directly - next = str.find_first_not_of(" \t\n", end); // Next non-space - } - else if (isspace(str[end+1])) - { - // The next one is a space - so we are okay - ++end; // Otherwise the length is wrong - next = str.find_first_not_of(" \t\n", end); // Next non-space - } - else - { - // Line break will be mid-word - auto prev = str.find_last_of(" \t\n", end); // Prev word break - - if (std::string::npos != prev && prev > pos) - { - end = prev; - next = prev + 1; // Continue from here - } - } - - // The next position to continue from - if (std::string::npos == next) - { - next = end + 1; - } - - // Has a length - if (end > pos) - { - // Indent following lines. The first one was already done. - if (pos) - { - for (std::string::size_type i = 0; i < usageMin; ++i) - { - Info<<' '; - } - } - - while (pos < end) - { - Info<< str[pos]; - ++pos; - } - Info<< nl; - } - - pos = next; - } + stringOps::writeWrapped(Info, str, (usageMax-usageMin), usageMin); } @@ -960,6 +881,11 @@ void Foam::argList::parse printUsage(false); quickExit = true; } + else if (options_.found("help-man")) + { + printMan(); + quickExit = true; + } // Allow independent display of compatibility information if (options_.found("help-compat")) @@ -1584,13 +1510,21 @@ bool Foam::argList::unsetOption(const word& optName) void Foam::argList::printNotes() const { - // Output notes directly - no automatic text wrapping + // Output notes with automatic text wrapping if (!notes.empty()) { Info<< nl; - forAllConstIters(notes, iter) + + for (const std::string& note : notes) { - Info<< iter().c_str() << nl; + if (note.empty()) + { + Info<< nl; + } + else + { + stringOps::writeWrapped(Info, note, usageMax); + } } } } @@ -1610,10 +1544,10 @@ void Foam::argList::printUsage(bool full) const } label i = 0; - forAllConstIters(validArgs, iter) + for (const std::string& argName : validArgs) { if (i++) Info<< ' '; - Info<< '<' << iter().c_str() << '>'; + Info<< '<' << argName.c_str() << '>'; } if (!argsMandatory_) @@ -1677,6 +1611,12 @@ void Foam::argList::printUsage(bool full) const printOptionUsage(14, "Display compatibility options and exit"); } + if (full) + { + Info<< " -help-man"; + printOptionUsage(11, "Display full help (manpage format) and exit"); + } + Info<< " -help-full"; printOptionUsage(12, "Display full help and exit"); diff --git a/src/OpenFOAM/global/argList/argList.H b/src/OpenFOAM/global/argList/argList.H index 9979bdbac32..b287fc6d2d1 100644 --- a/src/OpenFOAM/global/argList/argList.H +++ b/src/OpenFOAM/global/argList/argList.H @@ -507,6 +507,9 @@ public: //- Print usage void printUsage(bool full=true) const; + //- Print usage as nroff-man format (Experimental) + void printMan() const; + //- Display documentation in browser // Optionally display the application source code void displayDoc(bool source=false) const; diff --git a/src/OpenFOAM/global/argList/argListHelp.C b/src/OpenFOAM/global/argList/argListHelp.C new file mode 100644 index 00000000000..04a99fa628a --- /dev/null +++ b/src/OpenFOAM/global/argList/argListHelp.C @@ -0,0 +1,222 @@ +/*---------------------------------------------------------------------------*\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | + \\ / A nd | Copyright (C) 2018 OpenCFD Ltd. + \\/ M anipulation | +------------------------------------------------------------------------------- +License + This file is part of OpenFOAM. + + OpenFOAM 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. + + OpenFOAM 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 OpenFOAM. If not, see <http://www.gnu.org/licenses/>. + +\*---------------------------------------------------------------------------*/ + +#include "argList.H" +#include "stringOps.H" + +// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // + +namespace Foam +{ + +// manpage Footer +static inline void printManFooter() +{ + Info<< ".SH \"SEE ALSO\"" << nl + << "Online documentation " + << "https://www.openfoam.com/documentation/" << nl + << ".SH COPYRIGHT" << nl + << "Copyright 2018 OpenCFD Ltd." << nl; +} + + +static void printManOption(const word& optName) +{ + Info<< ".TP" << nl << ".B \\-" << optName; + + // Option has arg? + const auto optIter = argList::validOptions.cfind(optName); + if (optIter().size()) + { + Info<< " <" << optIter().c_str() << '>'; + } + Info<< nl; + + // Option has usage information? + + const auto usageIter = argList::optionUsage.cfind(optName); + if (usageIter.found()) + { + stringOps::writeWrapped(Info, *usageIter, argList::usageMax, 0, true); + } + else + { + Info<< nl; + } +} + +} // End namespace Foam + + +// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * // + +void Foam::argList::printMan() const +{ + // .TH "<APPLICATION>" 1 "OpenFOAM-<version>" "source" "category" + + Info + << ".TH" << token::SPACE + // All uppercase and quoted + << stringOps::upper(executable_) << token::SPACE + << "\"1\"" << token::SPACE + << token::DQUOTE << "OpenFOAM-v" << OPENFOAM << token::DQUOTE + << token::SPACE + << token::DQUOTE << "www.openfoam.com" << token::DQUOTE + << token::SPACE + << token::DQUOTE << "OpenFOAM Commands Manual" << token::DQUOTE + << nl; + + + // .SH NAME + // <application> \- part of OpenFOAM (The Open Source CFD Toolbox). + Info + << ".SH \"NAME\"" << nl + << executable_ + << " \\- part of OpenFOAM (The Open Source CFD Toolbox)." + << nl; + + + // .SH SYNOPSIS + // .B command [OPTIONS] ... + + Info + << ".SH \"SYNOPSIS\"" << nl + << ".B " << executable_ << " [OPTIONS]"; + + if (validArgs.size()) + { + Info<< ' '; + + if (!argsMandatory_) + { + Info<< '['; + } + + label i = 0; + for (const std::string& argName : validArgs) + { + if (i++) Info<< ' '; + Info << '<' << argName.c_str() << '>'; + } + + if (!argsMandatory_) + { + Info<< ']'; + } + } + Info<< nl; + + + // .SH DESCRIPTION + { + Info + << ".SH \"DESCRIPTION\"" << nl; + + Info<< ".nf" << nl; + + if (notes.empty()) + { + Info<<"No description available\n"; + } + else + { + Info<< nl; + for (const std::string& note : notes) + { + if (note.empty()) + { + Info<< nl; + } + else + { + stringOps::writeWrapped(Info, note, usageMax, 0, true); + } + } + } + Info<< ".fi" << nl; + } + + + // .SH "OPTIONS" + Info + << ".SH \"OPTIONS\"" << nl; + + for (const word& optName : validOptions.sortedToc()) + { + // Normal options + if (!advancedOptions.found(optName)) + { + printManOption(optName); + } + } + + // Standard documentation/help options + + Info<< ".TP" << nl << ".B \\-" << "doc" << nl + <<"Display documentation in browser" << nl; + + Info<< ".TP" << nl << ".B \\-" << "doc-source" << nl + << "Display source code in browser" << nl; + + Info<< ".TP" << nl << ".B \\-" << "help" << nl + << "Display short help and exit" << nl; + + Info<< ".TP" << nl << ".B \\-" << "help-full" << nl + << "Display full help and exit" << nl; + + + // .SH "ADVANCED OPTIONS" + Info + << ".SH \"ADVANCED OPTIONS\"" << nl; + + for (const word& optName : validOptions.sortedToc()) + { + // Advanced options + if (advancedOptions.found(optName)) + { + printManOption(optName); + } + } + + // Compatibility information + if + ( + argList::validOptionsCompat.size() + + argList::ignoreOptionsCompat.size() + ) + { + Info<< ".TP" << nl << ".B \\-" << "help-compat" << nl + << "Display compatibility options and exit" << nl; + } + + Info<< ".TP" << nl << ".B \\-" << "help-man" << nl + << "Display full help (manpage format) and exit" << nl; + + // Footer + printManFooter(); +} + + +// ************************************************************************* // diff --git a/src/OpenFOAM/primitives/strings/stringOps/stringOps.C b/src/OpenFOAM/primitives/strings/stringOps/stringOps.C index 5ed3841e3fb..85dd6c0c580 100644 --- a/src/OpenFOAM/primitives/strings/stringOps/stringOps.C +++ b/src/OpenFOAM/primitives/strings/stringOps/stringOps.C @@ -25,9 +25,10 @@ License #include "stringOps.H" #include "typeInfo.H" -#include "OSspecific.H" #include "etcFiles.H" #include "StringStream.H" +#include "OSstream.H" +#include "OSspecific.H" #include <cctype> // * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * // @@ -80,7 +81,7 @@ static void expandLeadingTag(std::string& s, const char b, const char e) } else if (tag == "case") { - s = fileName(getEnv("FOAM_CASE"))/file; + s = fileName(Foam::getEnv("FOAM_CASE"))/file; } else if (tag == "constant" || tag == "system") { @@ -1090,4 +1091,109 @@ void Foam::stringOps::inplaceUpper(std::string& s) } +void Foam::stringOps::writeWrapped +( + OSstream& os, + const std::string& str, + const std::string::size_type width, + const std::string::size_type indent, + const bool escape +) +{ + const auto len = str.length(); + + std::string::size_type pos = 0; + + // Handle leading newlines + while (str[pos] == '\n' && pos < len) + { + os << '\n'; + ++pos; + } + + while (pos < len) + { + // Potential end point and next point + std::string::size_type end = pos + width - 1; + std::string::size_type eol = str.find('\n', pos); + std::string::size_type next = string::npos; + + if (end >= len) + { + // No more wrapping needed + end = len; + + if (std::string::npos != eol && eol <= end) + { + // Manual '\n' break, next follows it (default behaviour) + end = eol; + } + } + else if (std::string::npos != eol && eol <= end) + { + // Manual '\n' break, next follows it (default behaviour) + end = eol; + } + else if (isspace(str[end])) + { + // Ended on a space - can use this directly + next = str.find_first_not_of(" \t\n", end); // Next non-space + } + else if (isspace(str[end+1])) + { + // The next one is a space - so we are okay + ++end; // Otherwise the length is wrong + next = str.find_first_not_of(" \t\n", end); // Next non-space + } + else + { + // Line break will be mid-word + auto prev = str.find_last_of(" \t\n", end); // Prev word break + + if (std::string::npos != prev && prev > pos) + { + end = prev; + next = prev + 1; // Continue from here + } + } + + // The next position to continue from + if (std::string::npos == next) + { + next = end + 1; + } + + // Has a length + if (end > pos) + { + // Indent following lines. + // The first one was already done prior to calling this routine. + if (pos) + { + for (std::string::size_type i = 0; i < indent; ++i) + { + os <<' '; + } + } + + while (pos < end) + { + const char c = str[pos]; + + if (escape && c == '\\') + { + os << '\\'; + } + os << c; + + ++pos; + } + os << nl; + } + + pos = next; + } +} + + // ************************************************************************* // diff --git a/src/OpenFOAM/primitives/strings/stringOps/stringOps.H b/src/OpenFOAM/primitives/strings/stringOps/stringOps.H index 85414c90087..b31f9383223 100644 --- a/src/OpenFOAM/primitives/strings/stringOps/stringOps.H +++ b/src/OpenFOAM/primitives/strings/stringOps/stringOps.H @@ -49,6 +49,9 @@ SourceFiles namespace Foam { +// Forward declarations +class OSstream; + /*---------------------------------------------------------------------------*\ Namespace stringOps Declaration \*---------------------------------------------------------------------------*/ @@ -382,6 +385,22 @@ namespace stringOps const StringType& str ); + //- Output string with text wrapping. + // Always includes a trailing newline, unless the string itself is empty. + // + // \param os the output stream + // \param str the text to be output + // \param width the max-width before wrapping + // \param indent indentation for continued lines + // \param escape escape any backslashes on output + void writeWrapped + ( + OSstream& os, + const std::string& str, + const std::string::size_type width, + const std::string::size_type indent = 0, + const bool escape = false + ); } // End namespace stringOps -- GitLab