3 -----------------------------------------------------------------------------
6 -- Module : Distribution.Client.Manpage
7 -- Copyright : (c) Maciek Makowski 2015
10 -- Maintainer : cabal-devel@haskell.org
11 -- Stability : provisional
12 -- Portability : portable
14 -- Functions for building the manual page.
15 module Distribution
.Client
.Manpage
16 ( -- * Manual page generation
24 import qualified Data
.List
.NonEmpty
as List1
25 import Distribution
.Client
.Compat
.Prelude
28 import Distribution
.Client
.Errors
29 import Distribution
.Client
.Init
.Utils
(trim
)
30 import Distribution
.Client
.ManpageFlags
31 import Distribution
.Client
.Setup
(globalCommand
)
32 import Distribution
.Compat
.Process
(proc
)
33 import Distribution
.Simple
.Command
34 import Distribution
.Simple
.Flag
(fromFlag
, fromFlagOrDefault
)
35 import Distribution
.Simple
.Utils
44 import System
.Environment
(lookupEnv
)
45 import System
.IO (hClose, hPutStr)
46 import qualified System
.Process
as Process
49 = -- | path, description
50 FileInfo
String String
52 -------------------------------------------------------------------------------
54 -------------------------------------------------------------------------------
56 -- | A list of files that should be documented in the manual page.
59 [ (FileInfo
"~/.config/cabal/config" "The defaults that can be overridden with command-line options.")
62 manpageCmd
:: String -> [CommandSpec a
] -> ManpageFlags
-> IO ()
63 manpageCmd pname commands flags
64 | fromFlagOrDefault
False (manpageRaw flags
) =
68 -- 2021-10-08, issue #7714
69 -- @cabal man --raw | man -l -@ does not work on macOS/BSD,
70 -- because BSD-man does not support option @-l@, rather would
71 -- accept directly a file argument, e.g. @man /dev/stdin@.
72 -- The following works both on macOS and Linux
73 -- (but not on Windows out-of-the-box):
75 -- cabal man --raw | nroff -man /dev/stdin | less
77 -- So let us simulate this!
79 -- Feed contents into @nroff -man /dev/stdin@
80 (formatted
, _errors
, ec1
) <-
84 ["-man", "/dev/stdin"]
85 Nothing
-- Inherit working directory
86 Nothing
-- Inherit environment
87 (Just
$ IODataText contents
)
90 unless (ec1
== ExitSuccess
) $ exitWith ec1
92 pagerAndArgs
<- fromMaybe "less -R" <$> lookupEnv
"PAGER"
93 -- 'less' is borked with color sequences otherwise, hence -R
94 (pager
, pagerArgs
) <- case words pagerAndArgs
of
95 [] -> dieWithException verbosity EmptyValuePagerEnvVariable
96 (p
: pa
) -> pure
(p
, pa
)
97 -- Pipe output of @nroff@ into @less@
98 (ec2
, _
) <- rawSystemProcAction
100 (proc pager pagerArgs
){Process
.std_in
= Process
.CreatePipe
}
102 let wIn
= fromCreatePipe mIn
103 hPutStr wIn formatted
108 contents
= manpage pname commands
109 verbosity
= fromFlag
$ manpageVerbosity flags
111 -- | Produces a manual page with @troff@ markup.
112 manpage
:: String -> [CommandSpec a
] -> String
113 manpage pname commands
=
115 [ ".TH " ++ map toUpper pname
++ " 1"
117 , pname
++ " \\- a system for building and packaging Haskell libraries and programs"
121 , ".RI < arguments |[ options ]>..."
128 ++ concatMap (commandSynopsisLines pname
) commands
129 ++ [ ".SH DESCRIPTION"
130 , "Cabal is the standard package system for Haskell software. It helps people to configure, "
131 , "build and install Haskell software and to distribute it easily to other users and developers."
133 , "The command line " ++ pname
++ " tool (also referred to as cabal-install) helps with "
134 , "installing existing packages and developing new packages. "
135 , "It can be used to work with local packages or to install packages from online package archives, "
136 , "including automatically installing dependencies. By default it is configured to use Hackage, "
137 , "which is Haskell's central package archive that contains thousands of libraries and applications "
138 , "in the Cabal package format."
143 ++ optionsLines
(globalCommand
[])
146 ++ concatMap (commandDetailsLines pname
) commands
149 ++ concatMap fileLines files
151 , "To browse the list of known issues or report a new one please see "
152 , "https://github.com/haskell/cabal/labels/cabal-install."
155 commandSynopsisLines
:: String -> CommandSpec action
-> [String]
156 commandSynopsisLines pname
(CommandSpec ui _ NormalCommand
) =
157 [ ".B " ++ pname
++ " " ++ (commandName ui
)
158 , "- " ++ commandSynopsis ui
161 commandSynopsisLines _
(CommandSpec _ _ HiddenCommand
) = []
163 commandDetailsLines
:: String -> CommandSpec action
-> [String]
164 commandDetailsLines pname
(CommandSpec ui _ NormalCommand
) =
165 [ ".B " ++ pname
++ " " ++ (commandName ui
)
167 , commandUsage ui pname
170 ++ optional removeLineBreaks commandDescription
171 ++ optional
id commandNotes
182 Just text
-> [f
$ text pname
, ""]
184 -- 2021-10-12, https://github.com/haskell/cabal/issues/7714#issuecomment-940842905
185 -- Line breaks just before e.g. 'new-build' cause weird @nroff@ warnings.
187 -- Remove line breaks but preserve paragraph breaks.
188 -- We group lines by empty/non-empty and then 'unwords'
189 -- blocks consisting of non-empty lines.
192 . concatMap unwordsNonEmpty
193 . List1
.groupWith
null
196 unwordsNonEmpty
:: List1
.NonEmpty
String -> [String]
197 unwordsNonEmpty ls1
= if null (List1
.head ls1
) then ls
else [unwords ls
]
199 ls
= List1
.toList ls1
200 commandDetailsLines _
(CommandSpec _ _ HiddenCommand
) = []
202 optionsLines
:: CommandUI flags
-> [String]
203 optionsLines command
= concatMap optionLines
(concatMap optionDescr
(commandOptions command ParseArgs
))
205 data ArgumentRequired
= Optional | Required
206 type OptionArg
= (ArgumentRequired
, ArgPlaceHolder
)
208 optionLines
:: OptDescr flags
-> [String]
209 optionLines
(ReqArg description
(optionChars
, optionStrings
) placeHolder _ _
) =
210 argOptionLines description optionChars optionStrings
(Required
, placeHolder
)
211 optionLines
(OptArg description
(optionChars
, optionStrings
) placeHolder _ _ _
) =
212 argOptionLines description optionChars optionStrings
(Optional
, placeHolder
)
213 optionLines
(BoolOpt description
(trueChars
, trueStrings
) (falseChars
, falseStrings
) _ _
) =
214 optionLinesIfPresent trueChars trueStrings
215 ++ optionLinesIfPresent falseChars falseStrings
216 ++ optionDescriptionLines description
217 optionLines
(ChoiceOpt options
) =
218 concatMap choiceLines options
220 choiceLines
(description
, (optionChars
, optionStrings
), _
, _
) =
221 [optionsLine optionChars optionStrings
]
222 ++ optionDescriptionLines description
224 argOptionLines
:: String -> [Char] -> [String] -> OptionArg
-> [String]
225 argOptionLines description optionChars optionStrings arg
=
226 [ optionsLine optionChars optionStrings
229 ++ optionDescriptionLines description
231 optionLinesIfPresent
:: [Char] -> [String] -> [String]
232 optionLinesIfPresent optionChars optionStrings
=
233 if null optionChars
&& null optionStrings
235 else [optionsLine optionChars optionStrings
, ".br"]
237 optionDescriptionLines
:: String -> [String]
238 optionDescriptionLines description
=
245 optionsLine
:: [Char] -> [String] -> String
246 optionsLine optionChars optionStrings
=
247 intercalate
", " (shortOptions optionChars
++ longOptions optionStrings
)
249 shortOptions
:: [Char] -> [String]
250 shortOptions
= map (\c
-> "\\-" ++ [c
])
252 longOptions
:: [String] -> [String]
253 longOptions
= map (\s
-> "\\-\\-" ++ s
)
255 optionArgLine
:: OptionArg
-> String
256 optionArgLine
(Required
, placeHolder
) = ".I " ++ placeHolder
257 optionArgLine
(Optional
, placeHolder
) = ".RI [ " ++ placeHolder
++ " ]"
259 fileLines
:: FileInfo
-> [String]
260 fileLines
(FileInfo path description
) =