Commit bf2edca9 authored by Mark Olesen's avatar Mark Olesen
Browse files

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
parent 7fc02ce1
......@@ -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
......
#!/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
# -----------------------------------------------------------------------------
......@@ -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
......
......@@ -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
......
......@@ -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");
......
......@@ -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;
......
/*---------------------------------------------------------------------------*\
========= |
\\ / 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);
}
}