Skip to content

Commit e5b148b

Browse files
committed
Fix haskell#7714: use nroff -man | less as backend for cabal man
Directly piping into `man -l -` does not work as BSD-`man` does not understand option `-l`. More standardized are the building blocks `nroff` and `less`. `cabal man` now should behave as pipeline ``` cabal man --raw | nroff -man /dev/stdin | less ``` Also fixed output of `cabal man --raw` so that it does not produce warnings. - `.R` removed. Was warning: ``` `R' is a string (producing the registered sign), not a macro. ``` - No quoted 'new-FOO' should appear at beginning of line. Was warning: ``` warning: macro `new-FOO'' not defined (probably missing space after `ne') ``` Added to `cabal-testsuite/PackageTests/Man/cabal.test.hs` a check that the `stderr` output of `nroff -man /dev/stdin` is empty (no warnings). Remaining problem: Unfortunately, after quitting `less` with `q` the following error is displayed: ``` fd:NNN: commitBuffer: resource vanished (Broken pipe) ``` Not sure how to fix this (my attempts failed).
1 parent 96ea35d commit e5b148b

File tree

3 files changed

+81
-24
lines changed

3 files changed

+81
-24
lines changed

cabal-install/main/Main.hs

+1-1
Original file line numberDiff line numberDiff line change
@@ -972,7 +972,7 @@ manpageAction :: [CommandSpec action] -> ManpageFlags -> [String] -> Action
972972
manpageAction commands flags extraArgs _ = do
973973
let verbosity = fromFlag (manpageVerbosity flags)
974974
unless (null extraArgs) $
975-
die' verbosity $ "'manpage' doesn't take any extra arguments: " ++ unwords extraArgs
975+
die' verbosity $ "'man' doesn't take any extra arguments: " ++ unwords extraArgs
976976
pname <- getProgName
977977
let cabalCmd = if takeExtension pname == ".exe"
978978
then dropExtension pname

cabal-install/src/Distribution/Client/Manpage.hs

+64-23
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ module Distribution.Client.Manpage
2222

2323
import Distribution.Client.Compat.Prelude
2424
import Prelude ()
25+
import qualified Data.List.NonEmpty as List1
2526

27+
import Distribution.Client.Init.Utils (trim)
28+
-- TODO #7744: move 'trim' to a more canonical place
2629
import Distribution.Client.ManpageFlags
2730
import Distribution.Client.Setup (globalCommand)
28-
import Distribution.Compat.Process (createProcess)
2931
import Distribution.Simple.Command
3032
import Distribution.Simple.Flag (fromFlagOrDefault)
33+
import Distribution.Simple.Utils
34+
( IOData(..), IODataMode(..), createProcessWithEnv, rawSystemStdInOut )
35+
import qualified Distribution.Verbosity as Verbosity
3136
import System.IO (hClose, hPutStr)
3237

3338
import qualified System.Process as Process
@@ -50,23 +55,43 @@ manpageCmd pname commands flags
5055
= putStrLn contents
5156
| otherwise
5257
= do
53-
let cmd = "man"
54-
args = ["-l", "-"]
55-
56-
(mb_in, _, _, ph) <- createProcess (Process.proc cmd args)
57-
{ Process.std_in = Process.CreatePipe
58-
, Process.std_out = Process.Inherit
59-
, Process.std_err = Process.Inherit
60-
}
61-
62-
-- put contents
63-
for_ mb_in $ \hin -> do
64-
hPutStr hin contents
65-
hClose hin
66-
67-
-- wait for process to exit, propagate exit code
68-
ec <- Process.waitForProcess ph
69-
exitWith ec
58+
-- 2021-10-08, issue #7714
59+
-- @cabal man --raw | man -l -@ does not work on macOS/BSD,
60+
-- because BSD-man does not support option @-l@, rather would
61+
-- accept directly a file argument, e.g. @man /dev/stdin@.
62+
-- The following works both on macOS and Linux
63+
-- (but not on Windows out-of-the-box):
64+
--
65+
-- cabal man --raw | nroff -man /dev/stdin | less
66+
--
67+
-- So let us simulate this!
68+
69+
-- Feed contents into @nroff -man /dev/stdin@
70+
(formatted, _errors, ec1) <- rawSystemStdInOut
71+
Verbosity.normal
72+
"nroff"
73+
[ "-man", "/dev/stdin" ]
74+
Nothing -- Inherit working directory
75+
Nothing -- Inherit environment
76+
(Just $ IODataText contents)
77+
IODataModeText
78+
79+
unless (ec1 == ExitSuccess) $ exitWith ec1
80+
81+
-- Pipe output of @nroff@ into @less@
82+
(Just inLess, _, _, procLess) <- createProcessWithEnv
83+
Verbosity.normal
84+
"less"
85+
[]
86+
Nothing -- Inherit working directory
87+
Nothing -- Inherit environment
88+
Process.CreatePipe -- in
89+
Process.Inherit -- out
90+
Process.Inherit -- err
91+
92+
hPutStr inLess formatted
93+
hClose inLess
94+
exitWith =<< Process.waitForProcess procLess
7095
where
7196
contents :: String
7297
contents = manpage pname commands
@@ -117,7 +142,7 @@ manpage pname commands = unlines $
117142
commandSynopsisLines :: String -> CommandSpec action -> [String]
118143
commandSynopsisLines pname (CommandSpec ui _ NormalCommand) =
119144
[ ".B " ++ pname ++ " " ++ (commandName ui)
120-
, ".R - " ++ commandSynopsis ui
145+
, "- " ++ commandSynopsis ui
121146
, ".br"
122147
]
123148
commandSynopsisLines _ (CommandSpec _ _ HiddenCommand) = []
@@ -129,8 +154,8 @@ commandDetailsLines pname (CommandSpec ui _ NormalCommand) =
129154
, commandUsage ui pname
130155
, ""
131156
] ++
132-
optional commandDescription ++
133-
optional commandNotes ++
157+
optional removeLineBreaks commandDescription ++
158+
optional id commandNotes ++
134159
[ "Flags:"
135160
, ".RS"
136161
] ++
@@ -139,10 +164,26 @@ commandDetailsLines pname (CommandSpec ui _ NormalCommand) =
139164
, ""
140165
]
141166
where
142-
optional field =
167+
optional f field =
143168
case field ui of
144-
Just text -> [text pname, ""]
169+
Just text -> [ f $ text pname, "" ]
145170
Nothing -> []
171+
-- 2021-10-12, https://github.com/haskell/cabal/issues/7714#issuecomment-940842905
172+
-- Line breaks just before e.g. 'new-build' cause weird @nroff@ warnings.
173+
-- Thus:
174+
-- Remove line breaks but preserve paragraph breaks.
175+
-- We group lines by empty/non-empty and then 'unwords'
176+
-- blocks consisting of non-empty lines.
177+
removeLineBreaks
178+
= unlines
179+
. concatMap unwordsNonEmpty
180+
. List1.groupWith null
181+
. map trim
182+
. lines
183+
unwordsNonEmpty :: List1.NonEmpty String -> [String]
184+
unwordsNonEmpty ls1 = if null (List1.head ls1) then ls else [unwords ls]
185+
where ls = List1.toList ls1
186+
146187
commandDetailsLines _ (CommandSpec _ _ HiddenCommand) = []
147188

148189
optionsLines :: CommandUI flags -> [String]
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1+
import System.Process
12
import Test.Cabal.Prelude
3+
4+
25
main = cabalTest $ do
36
r <- cabal' "man" ["--raw"]
47
assertOutputContains ".B cabal install" r
58
assertOutputDoesNotContain ".B cabal manpage" r
9+
10+
-- Check that output of `cabal man --raw` can be passed through `nroff -man`
11+
-- without producing any warnings (which are printed to stderr).
12+
--
13+
-- NB: runM is not suitable as it mixes stdout and stderr
14+
-- r2 <- runM "nroff" ["-man", "/dev/stdin"] $ Just $ resultOutput r
15+
(ec, _output, errors) <- liftIO $
16+
readProcessWithExitCode "nroff" ["-man", "/dev/stdin"] $ resultOutput r
17+
unless (null errors) $
18+
assertFailure $ unlines
19+
[ "Error: unexpected warnings produced by `nroff -man`:"
20+
, errors
21+
]

0 commit comments

Comments
 (0)