optparse-applicative-0.19.0.0: src/Options/Applicative/Help/Core.hs
{-# LANGUAGE CPP #-}
module Options.Applicative.Help.Core (
cmdDesc,
briefDesc,
missingDesc,
fullDesc,
globalDesc,
ParserHelp(..),
errorHelp,
headerHelp,
suggestionsHelp,
usageHelp,
descriptionHelp,
bodyHelp,
footerHelp,
globalsHelp,
parserHelp,
parserUsage,
parserGlobals
) where
import Control.Applicative
import Control.Monad (guard)
import Data.Foldable (any, foldl')
import Data.Function (on)
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty ((:|)))
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromMaybe, catMaybes)
import Prelude hiding (any)
import Options.Applicative.Common
import Options.Applicative.Types
import Options.Applicative.Help.Pretty
import Options.Applicative.Help.Chunk
-- | Style for rendering an option.
data OptDescStyle
= OptDescStyle
{ descSep :: Doc,
descHidden :: Bool,
descGlobal :: Bool
}
safelast :: [a] -> Maybe a
safelast = foldl' (const Just) Nothing
-- | Generate description for a single option.
optDesc :: ParserPrefs -> OptDescStyle -> ArgumentReachability -> Option a -> (OptGroup, Chunk Doc, Parenthetic)
optDesc pprefs style _reachability opt =
let names =
List.sort . optionNames . optMain $ opt
meta =
stringChunk $ optMetaVar opt
grp = propGroup $ optProps opt
descs =
map (pretty . showOption) names
descriptions =
listToChunk (List.intersperse (descSep style) descs)
desc
| prefHelpLongEquals pprefs && not (isEmpty meta) && any isLongName (safelast names) =
descriptions <> stringChunk "=" <> meta
| otherwise =
descriptions <<+>> meta
show_opt
| descGlobal style && not (propShowGlobal (optProps opt)) =
False
| optVisibility opt == Hidden =
descHidden style
| otherwise =
optVisibility opt == Visible
wrapping
| null names =
NeverRequired
| length names == 1 =
MaybeRequired
| otherwise =
AlwaysRequired
rendered
| not show_opt =
mempty
| otherwise =
desc
modified =
maybe id fmap (optDescMod opt) rendered
in (grp, modified, wrapping)
-- | Generate descriptions for commands.
cmdDesc :: ParserPrefs -> Parser a -> [(Maybe String, Chunk Doc)]
cmdDesc pprefs = mapParser desc
where
desc _ opt =
case optMain opt of
CmdReader gn cmds ->
(,) gn $
tabulate (prefTabulateFill pprefs)
[ (pretty nm, align (extractChunk (infoProgDesc cmd)))
| (nm, cmd) <- reverse cmds
]
_ -> mempty
-- | Generate a brief help text for a parser.
briefDesc :: ParserPrefs -> Parser a -> Chunk Doc
briefDesc = briefDesc' True
-- | Generate a brief help text for a parser, only including mandatory
-- options and arguments.
missingDesc :: ParserPrefs -> Parser a -> Chunk Doc
missingDesc = briefDesc' False
-- | Generate a brief help text for a parser, allowing the specification
-- of if optional arguments are show.
briefDesc' :: Bool -> ParserPrefs -> Parser a -> Chunk Doc
briefDesc' showOptional pprefs =
wrapOver NoDefault MaybeRequired
. foldTree pprefs style
. mFilterOptional
. treeMapParser optDesc'
where
mFilterOptional
| showOptional =
id
| otherwise =
filterOptional
style = OptDescStyle
{ descSep = pretty '|',
descHidden = False,
descGlobal = False
}
optDesc' reach opt =
let
(_, a, b) =
optDesc pprefs style reach opt
in
(a, b)
-- | Wrap a doc in parentheses or brackets if required.
wrapOver :: AltNodeType -> Parenthetic -> (Chunk Doc, Parenthetic) -> Chunk Doc
wrapOver altnode mustWrapBeyond (chunk, wrapping)
| altnode == MarkDefault =
fmap brackets chunk
| wrapping > mustWrapBeyond =
fmap parens chunk
| otherwise =
chunk
-- Fold a tree of option docs into a single doc with fully marked
-- optional areas and groups.
foldTree :: ParserPrefs -> OptDescStyle -> OptTree (Chunk Doc, Parenthetic) -> (Chunk Doc, Parenthetic)
foldTree _ _ (Leaf x) =
x
foldTree prefs s (MultNode xs) =
let go =
(<</>>) . wrapOver NoDefault MaybeRequired . foldTree prefs s
x =
foldr go mempty xs
wrapLevel =
multi_wrap xs
in (x, wrapLevel)
where
multi_wrap [_] = NeverRequired
multi_wrap _ = MaybeRequired
foldTree prefs s (AltNode b xs) =
(\x -> (x, NeverRequired))
. fmap groupOrNestLine
. wrapOver b MaybeRequired
. alt_node
. filter (not . isEmpty . fst)
. map (foldTree prefs s)
$ xs
where
alt_node :: [(Chunk Doc, Parenthetic)] -> (Chunk Doc, Parenthetic)
alt_node [n] = n
alt_node ns =
(\y -> (y, AlwaysRequired))
. foldr (chunked altSep . wrapOver NoDefault MaybeRequired) mempty
$ ns
foldTree prefs s (BindNode x) =
let rendered =
wrapOver NoDefault NeverRequired (foldTree prefs s x)
-- We always want to display the rendered option
-- if it exists, and only attach the suffix then.
withSuffix =
rendered >>= (\r -> pure r <> stringChunk (prefMultiSuffix prefs))
in (withSuffix, NeverRequired)
-- | Generate a full help text for a parser
fullDesc :: ParserPrefs -> Parser a -> Chunk Doc
fullDesc = optionsDesc False
-- | Generate a help text for the parser, showing
-- only what is relevant in the "Global options: section"
globalDesc :: ParserPrefs -> Parser a -> Chunk Doc
globalDesc = optionsDesc True
-- | Common generator for full descriptions and globals
optionsDesc :: Bool -> ParserPrefs -> Parser a -> Chunk Doc
optionsDesc global pprefs p =
vsepChunks
. formatTitle'
. fmap tabulateGroup
. groupByTitle
$ docs
where
docs :: [Maybe (OptGroup, (Doc, Doc))]
docs = mapParser doc p
groupByTitle :: [Maybe (OptGroup, (Doc, Doc))] -> [[(OptGroup, (Doc, Doc))]]
groupByTitle xs = groupFstAll . catMaybes $ xs
-- NOTE: [Nested group alignment]
--
-- For nested groups, we want to produce output like:
--
-- Group 1
-- --opt-1 INT Option 1
--
-- - Group 2
-- --opt-2 INT Option 2
--
-- - Group 3
-- - opt-3 INT Option 3
--
-- That is, we have the following constraints:
--
-- 1. Nested groups are prefixed with a hyphen '- ', where the hyphen
-- starts on the same column as the parent group.
--
-- 2. We still want the listed options to be indented twice under the
-- group name, so this means nested options need to be indented
-- again by the standard amount (2), due to the hyphen.
--
-- 3. Help text should be __globally__ aligned.
tabulateGroup :: [(OptGroup, (Doc, Doc))] -> (OptGroup, Chunk Doc)
tabulateGroup l@((title,_):_) =
(title, tabulate (prefTabulateFill pprefs) (getGroup <$> l))
where
-- Handle NOTE: [Nested group alignment] 3. here i.e. indent the
-- right Doc (help text) according to its indention level and
-- global maxGroupLevel. Notice there is an inverse relationship here,
-- as the further the entire group is indented, the less we need to
-- indent the help text.
getGroup :: (OptGroup, (Doc, Doc)) -> (Doc, Doc)
getGroup o@(_, (x, y)) =
let helpIndent = calcOptHelpIndent o
in (x, indent helpIndent y)
-- Indents the option help text, taking the option's group level and
-- maximum group level into account.
calcOptHelpIndent :: (OptGroup, a) -> Int
calcOptHelpIndent g =
let groupLvl = optGroupToLevel g
in lvlIndent * (maxGroupLevel - groupLvl)
tabulateGroup [] = (OptGroup [], mempty)
-- Fold so we can update the (printedGroups :: [String]) arg as we
-- iterate. End with a reverse since we use foldl'.
formatTitle' :: [(OptGroup, Chunk Doc)] -> [Chunk Doc]
formatTitle' = reverse . snd . foldl' formatTitle ([], [])
formatTitle :: ([String], [Chunk Doc]) -> (OptGroup, Chunk Doc) -> ([String], [Chunk Doc])
formatTitle (printedGroups, acc) o@(OptGroup groups, opts) =
case parentGroups of
-- No nested groups: No special logic.
[] -> (groupTitle : printedGroups, ((\d -> pretty groupTitle .$. d) <$> opts) : acc)
-- We have at least one parent group title P for current group G: P has
-- already been printed iff it is attached to another (non-grouped)
-- option. In other words, P has __not__ been printed if its only
-- member is another group.
--
-- The parameter (printedGroups :: [String]) holds all groups that
-- have already been printed.
parents@(_ : _) ->
let groupLvl = optGroupToLevel o
-- indent opts an extra lvlIndent to account for hyphen
indentOpts = indent lvlIndent
-- new printedGroups is all previous + this and parents.
printedGroups' = groupTitle : parents ++ printedGroups
parentsWithIndent = zip [0 .. ] parents
-- docs for unprinted parent title groups
parentDocs = pure $ mkParentDocs printedGroups parentsWithIndent
-- docs for the current group
thisDocs =
(\d -> lvlIndentNSub1 groupLvl $ (hyphenate groupTitle) .$. indentOpts d)
<$> opts
allDocs = parentDocs <> thisDocs
in (printedGroups', allDocs : acc)
where
-- Separate parentGroups and _this_ group, in case we need to also
-- print parent groups.
(parentGroups, groupTitle) = case unsnoc groups of
Nothing -> ([], defTitle)
Just (parentGrps, grp) -> (parentGrps, grp)
defTitle =
if global
then "Global options:"
else "Available options:"
maxGroupLevel :: Int
maxGroupLevel = findMaxGroupLevel docs
-- Finds the maxium OptGroup level.
findMaxGroupLevel :: [Maybe (OptGroup, (Doc, Doc))] -> Int
findMaxGroupLevel = foldl' (\acc -> max acc . optGroupToLevel) 0 . catMaybes
optGroupToLevel :: (OptGroup, a) -> Int
-- 0 (defTitle) and 1 (custom group name) are handled identically
-- w.r.t indenation (not indented). Hence the subtraction here.
optGroupToLevel (OptGroup [], _) = 0
optGroupToLevel (OptGroup xs@(_ : _), _) = length xs - 1
doc :: ArgumentReachability -> Option a -> Maybe (OptGroup, (Doc, Doc))
doc info opt = do
guard . not . isEmpty $ n
guard . not . isEmpty $ h
return (grp, (extractChunk n, align . extractChunk $ h <</>> hdef))
where
(grp, n, _) = optDesc pprefs style info opt
h = optHelp opt
hdef = Chunk . fmap show_def . optShowDefault $ opt
show_def s = parens (pretty "default:" <+> pretty s)
style = OptDescStyle
{ descSep = pretty ',',
descHidden = True,
descGlobal = global
}
--
-- Prints all parent titles that have not already been printed
-- (i.e. in printedGroups).
mkParentDocs :: [String] -> [(Int, String)] -> Doc
mkParentDocs printedGroups =
foldr g mempty
where
g :: (Int, String) -> Doc -> Doc
g (i, s) acc
| s `List.elem` printedGroups = acc
| i == 0 = pretty s .$. acc
| otherwise = lvlIndentNSub1 i $ hyphenate s .$. acc
hyphenate s = pretty ("- " <> s)
lvlIndentNSub1 :: Int -> Doc -> Doc
lvlIndentNSub1 n = indent (lvlIndent * (n - 1))
lvlIndent :: Int
lvlIndent = 2
errorHelp :: Chunk Doc -> ParserHelp
errorHelp chunk = mempty { helpError = chunk }
headerHelp :: Chunk Doc -> ParserHelp
headerHelp chunk = mempty { helpHeader = chunk }
suggestionsHelp :: Chunk Doc -> ParserHelp
suggestionsHelp chunk = mempty { helpSuggestions = chunk }
globalsHelp :: Chunk Doc -> ParserHelp
globalsHelp chunk = mempty { helpGlobals = chunk }
usageHelp :: Chunk Doc -> ParserHelp
usageHelp chunk = mempty { helpUsage = chunk }
descriptionHelp :: Chunk Doc -> ParserHelp
descriptionHelp chunk = mempty { helpDescription = chunk }
bodyHelp :: Chunk Doc -> ParserHelp
bodyHelp chunk = mempty { helpBody = chunk }
footerHelp :: Chunk Doc -> ParserHelp
footerHelp chunk = mempty { helpFooter = chunk }
-- | Generate the help text for a program.
parserHelp :: ParserPrefs -> Parser a -> ParserHelp
parserHelp pprefs p =
bodyHelp . vsepChunks $
fullDesc pprefs p
: (group_title <$> cs)
where
def = "Available commands:"
cs = groupFstAll $ cmdDesc pprefs p
group_title a@((n, _) : _) =
with_title (fromMaybe def n) $
vcatChunks (snd <$> a)
group_title _ = mempty
with_title :: String -> Chunk Doc -> Chunk Doc
with_title title = fmap (pretty title .$.)
parserGlobals :: ParserPrefs -> Parser a -> ParserHelp
parserGlobals pprefs p =
globalsHelp $ globalDesc pprefs p
-- | Generate option summary.
parserUsage :: ParserPrefs -> Parser a -> String -> Doc
parserUsage pprefs p progn =
group $
hsep
[ pretty "Usage:",
pretty progn,
hangAtIfOver 9 (prefBriefHangPoint pprefs) (extractChunk (briefDesc pprefs p))
]
-- | Peek at the structure of the rendered tree within.
--
-- For example, if a child is an option with multiple
-- alternatives, such as -a or -b, we need to know this
-- when wrapping it. For example, whether it's optional:
-- we don't want to have [(-a|-b)], rather [-a|-b] or
-- (-a|-b).
data Parenthetic
= NeverRequired
-- ^ Parenthesis are not required.
| MaybeRequired
-- ^ Parenthesis should be used if this group can be repeated
| AlwaysRequired
-- ^ Parenthesis should always be used.
deriving (Eq, Ord, Show)
-- | Groups on the first element of the tuple. This differs from the simple
-- @groupBy ((==) `on` fst)@ in that non-adjacent groups are __also__ grouped
-- together. For example:
--
-- @
-- groupFst = groupBy ((==) `on` fst)
--
-- let xs = [(1, "a"), (1, "b"), (3, "c"), (2, "d"), (3, "e"), (2, "f")]
--
-- groupFst xs === [[(1,"a"),(1,"b")],[(3,"c")],[(2,"d")],[(3,"e")],[(2,"f")]]
-- groupFstAll xs === [[(1,"a"),(1,"b")],[(3,"c"),(3,"e")],[(2,"d"),(2,"f")]]
-- @
--
-- Notice that the original order is preserved i.e. we do not first sort on
-- the first element.
--
-- @since 0.19.0.0
groupFstAll :: Ord a => [(a, b)] -> [[(a, b)]]
groupFstAll =
-- In order to group all (adjacent + non-adjacent) Eq elements together, we
-- sort the list so that the Eq elements are in fact adjacent, _then_ group.
-- We don't want to destroy the original order, however, so we add a
-- temporary index that maintains this original order. The full logic is:
--
-- 1. Add index i that preserves original order.
-- 2. Sort on tuple's fst.
-- 3. Group by fst.
-- 4. Sort by i, restoring original order.
-- 5. Drop index i.
fmap (NE.toList . dropIdx)
. List.sortOn toIdx
. NE.groupBy ((==) `on` fst')
. List.sortOn fst'
. zipWithIndex
where
dropIdx :: NonEmpty (Int, (a, b)) -> NonEmpty (a, b)
dropIdx = fmap snd
toIdx :: NonEmpty (Int, (a, b)) -> Int
toIdx ((x, _) :| _) = x
-- Like fst, ignores our added index
fst' :: (Int, (a, b)) -> a
fst' (_, (x, _)) = x
zipWithIndex :: [(a, b)] -> [(Int, (a, b))]
zipWithIndex = zip [1 ..]
-- | From base-4.19.0.0.
unsnoc :: [a] -> Maybe ([a], a)
unsnoc = foldr (\x -> Just . maybe ([], x) (\(~(a, b)) -> (x : a, b))) Nothing