Skip to content

Commit 888801b

Browse files
authored
Merge pull request #7579 from robx/cabal-arg-error
Handle option argument parse errors without 'error' (fixes #7573)
2 parents 451ef1c + d52d51a commit 888801b

File tree

7 files changed

+110
-31
lines changed

7 files changed

+110
-31
lines changed

Cabal-tests/Cabal-tests.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test-suite unit-tests
3131
UnitTests.Distribution.Compat.Time
3232
UnitTests.Distribution.Described
3333
UnitTests.Distribution.PkgconfigVersion
34+
UnitTests.Distribution.Simple.Command
3435
UnitTests.Distribution.Simple.Glob
3536
UnitTests.Distribution.Simple.Program.GHC
3637
UnitTests.Distribution.Simple.Program.Internal

Cabal-tests/tests/UnitTests.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Distribution.Compat.Time
1616
import qualified UnitTests.Distribution.Compat.CreatePipe
1717
import qualified UnitTests.Distribution.Compat.Time
1818
import qualified UnitTests.Distribution.Compat.Graph
19+
import qualified UnitTests.Distribution.Simple.Command
1920
import qualified UnitTests.Distribution.Simple.Glob
2021
import qualified UnitTests.Distribution.Simple.Program.GHC
2122
import qualified UnitTests.Distribution.Simple.Program.Internal
@@ -49,6 +50,8 @@ tests mtimeChangeCalibrated =
4950
(UnitTests.Distribution.Compat.Time.tests mtimeChange)
5051
, testGroup "Distribution.Compat.Graph"
5152
UnitTests.Distribution.Compat.Graph.tests
53+
, testGroup "Distribution.Simple.Command"
54+
UnitTests.Distribution.Simple.Command.tests
5255
, testGroup "Distribution.Simple.Glob"
5356
UnitTests.Distribution.Simple.Glob.tests
5457
, UnitTests.Distribution.Simple.Program.GHC.tests
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
module UnitTests.Distribution.Simple.Command
2+
( tests
3+
) where
4+
5+
import Distribution.Simple.Command
6+
import qualified Distribution.Simple.Flag as Flag
7+
import Distribution.Simple.Setup (optionVerbosity)
8+
import qualified Distribution.Verbosity as Verbosity
9+
import Test.Tasty
10+
import Test.Tasty.HUnit
11+
12+
argumentTests :: [TestTree]
13+
argumentTests =
14+
[ testCase "parses verbosity successfully" $ do
15+
let p = commandParseArgs cmdUI isGlobal ["-v2"]
16+
assertEqual "expected verbose" (Right verbose) $ evalParse p
17+
, testCase "handles argument parse error gracefully" $ do
18+
let p = commandParseArgs cmdUI isGlobal ["-v=2"]
19+
assertEqual "expected error" (Left "errors") $ evalParse p
20+
]
21+
where
22+
-- evaluate command parse result, to force possible exceptions in 'f'
23+
evalParse p = case p of
24+
CommandErrors _ -> Left "errors"
25+
CommandHelp _ -> Left "help"
26+
CommandList _ -> Left "list"
27+
CommandReadyToGo (f, _) -> Right $ f Flag.NoFlag
28+
verbose = Flag.Flag Verbosity.verbose
29+
isGlobal = True
30+
cmdUI = CommandUI
31+
{ commandName = "cmd"
32+
, commandSynopsis = "the command"
33+
, commandUsage = \name -> name ++ " cmd -v[N]"
34+
, commandDescription = Nothing
35+
, commandNotes = Nothing
36+
, commandDefaultFlags = Flag.NoFlag
37+
, commandOptions = const [ optField ]
38+
}
39+
optField = optionVerbosity id const
40+
41+
tests :: [TestTree]
42+
tests =
43+
[ testGroup "option argument tests" argumentTests
44+
]

Cabal/src/Distribution/GetOpt.hs

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
-- * Line wrapping in the 'usageInfo' output, plus a more compact
1818
-- rendering of short options, and slightly less padding.
1919
--
20-
-- If you want to take on the challenge of merging this with the GetOpt
21-
-- from the base package then go for it!
20+
-- * Parsing of option arguments is allowed to fail.
21+
--
22+
-- * 'ReturnInOrder' argument order is removed.
2223
--
2324
{-# LANGUAGE TupleSections #-}
2425
{-# LANGUAGE NamedFieldPuns #-}
@@ -36,8 +37,34 @@ module Distribution.GetOpt (
3637

3738
import Prelude ()
3839
import Distribution.Compat.Prelude
39-
import System.Console.GetOpt
40-
( ArgOrder(..), OptDescr(..), ArgDescr(..) )
40+
41+
-- | What to do with options following non-options
42+
data ArgOrder a
43+
= RequireOrder -- ^ no option processing after first non-option
44+
| Permute -- ^ freely intersperse options and non-options
45+
46+
data OptDescr a = -- description of a single options:
47+
Option [Char] -- list of short option characters
48+
[String] -- list of long option strings (without "--")
49+
(ArgDescr a) -- argument descriptor
50+
String -- explanation of option for user
51+
52+
instance Functor OptDescr where
53+
fmap f (Option a b argDescr c) = Option a b (fmap f argDescr) c
54+
55+
-- | Describes whether an option takes an argument or not, and if so
56+
-- how the argument is parsed to a value of type @a@.
57+
--
58+
-- Compared to System.Console.GetOpt, we allow for parse errors.
59+
data ArgDescr a
60+
= NoArg a -- ^ no argument expected
61+
| ReqArg (String -> Either String a) String -- ^ option requires argument
62+
| OptArg (Maybe String -> Either String a) String -- ^ optional argument
63+
64+
instance Functor ArgDescr where
65+
fmap f (NoArg a) = NoArg (f a)
66+
fmap f (ReqArg g s) = ReqArg (fmap f . g) s
67+
fmap f (OptArg g s) = OptArg (fmap f . g) s
4168

4269
data OptKind a -- kind of cmd line arg (internal use only):
4370
= Opt a -- an option
@@ -155,10 +182,8 @@ getOpt' ordering optDescr (arg:args) = procNextOpt opt ordering
155182
procNextOpt (UnreqOpt u) _ = (os,xs,u:us,es)
156183
procNextOpt (NonOpt x) RequireOrder = ([],x:rest,[],[])
157184
procNextOpt (NonOpt x) Permute = (os,x:xs,us,es)
158-
procNextOpt (NonOpt x) (ReturnInOrder f) = (f x :os, xs,us,es)
159185
procNextOpt EndOfOpts RequireOrder = ([],rest,[],[])
160186
procNextOpt EndOfOpts Permute = ([],rest,[],[])
161-
procNextOpt EndOfOpts (ReturnInOrder f) = (map f rest,[],[],[])
162187
procNextOpt (OptErr e) _ = (os,xs,us,e:es)
163188

164189
(opt,rest) = getNext arg args optDescr
@@ -181,15 +206,16 @@ longOpt ls rs optDescr = long ads arg rs
181206
options = if null exact then getWith isPrefixOf else exact
182207
ads = [ ad | Option _ _ ad _ <- options ]
183208
optStr = "--" ++ opt
209+
fromRes = fromParseResult optStr
184210

185211
long (_:_:_) _ rest = (errAmbig options optStr,rest)
186212
long [NoArg a ] [] rest = (Opt a,rest)
187213
long [NoArg _ ] ('=':_) rest = (errNoArg optStr,rest)
188214
long [ReqArg _ d] [] [] = (errReq d optStr,[])
189-
long [ReqArg f _] [] (r:rest) = (Opt (f r),rest)
190-
long [ReqArg f _] ('=':xs) rest = (Opt (f xs),rest)
191-
long [OptArg f _] [] rest = (Opt (f Nothing),rest)
192-
long [OptArg f _] ('=':xs) rest = (Opt (f (Just xs)),rest)
215+
long [ReqArg f _] [] (r:rest) = (fromRes (f r),rest)
216+
long [ReqArg f _] ('=':xs) rest = (fromRes (f xs),rest)
217+
long [OptArg f _] [] rest = (fromRes (f Nothing),rest)
218+
long [OptArg f _] ('=':xs) rest = (fromRes (f (Just xs)),rest)
193219
long _ _ rest = (UnreqOpt ("--"++ls),rest)
194220

195221
-- handle short option
@@ -198,15 +224,16 @@ shortOpt y ys rs optDescr = short ads ys rs
198224
where options = [ o | o@(Option ss _ _ _) <- optDescr, s <- ss, y == s ]
199225
ads = [ ad | Option _ _ ad _ <- options ]
200226
optStr = '-':[y]
227+
fromRes = fromParseResult optStr
201228

202229
short (_:_:_) _ rest = (errAmbig options optStr,rest)
203230
short (NoArg a :_) [] rest = (Opt a,rest)
204231
short (NoArg a :_) xs rest = (Opt a,('-':xs):rest)
205232
short (ReqArg _ d:_) [] [] = (errReq d optStr,[])
206-
short (ReqArg f _:_) [] (r:rest) = (Opt (f r),rest)
207-
short (ReqArg f _:_) xs rest = (Opt (f xs),rest)
208-
short (OptArg f _:_) [] rest = (Opt (f Nothing),rest)
209-
short (OptArg f _:_) xs rest = (Opt (f (Just xs)),rest)
233+
short (ReqArg f _:_) [] (r:rest) = (fromRes (f r),rest)
234+
short (ReqArg f _:_) xs rest = (fromRes (f xs),rest)
235+
short (OptArg f _:_) [] rest = (fromRes (f Nothing),rest)
236+
short (OptArg f _:_) xs rest = (fromRes (f (Just xs)),rest)
210237
short [] [] rest = (UnreqOpt optStr,rest)
211238
short [] xs rest = (UnreqOpt (optStr++xs),rest)
212239
-- This is different vs upstream = (UnreqOpt optStr,('-':xs):rest)
@@ -215,9 +242,14 @@ shortOpt y ys rs optDescr = short ads ys rs
215242
-- But why was no equivalent change required for longOpt? So could
216243
-- this change go upstream?
217244

245+
fromParseResult :: String -> Either String a -> OptKind a
246+
fromParseResult optStr res = case res of
247+
Right x -> Opt x
248+
Left err -> OptErr ("invalid argument to option `" ++ optStr ++ "': " ++ err ++ "\n")
249+
218250
-- miscellaneous error formatting
219251

220-
errAmbig :: [OptDescr a] -> String -> OptKind a
252+
errAmbig :: [OptDescr a] -> String -> OptKind b
221253
errAmbig ods optStr = OptErr (usageInfo header ods)
222254
where header = "option `" ++ optStr ++ "' is ambiguous; could be one of:"
223255

Cabal/src/Distribution/ReadE.hs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ module Distribution.ReadE (
1313
-- * ReadE
1414
ReadE(..), succeedReadE, failReadE,
1515
-- * Projections
16-
readEOrFail,
1716
parsecToReadE,
1817
) where
1918

@@ -38,9 +37,6 @@ succeedReadE f = ReadE (Right . f)
3837
failReadE :: ErrorMsg -> ReadE a
3938
failReadE = ReadE . const . Left
4039

41-
readEOrFail :: ReadE a -> String -> a
42-
readEOrFail r = either error id . runReadE r
43-
4440
parsecToReadE :: (String -> ErrorMsg) -> ParsecParser a -> ReadE a
4541
parsecToReadE err p = ReadE $ \txt ->
4642
case runParsecParser p "<parsecToReadE>" (fieldLineStreamFromString txt) of

Cabal/src/Distribution/Simple/Command.hs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -223,16 +223,15 @@ commandGetOpts :: ShowOrParseArgs -> CommandUI flags
223223
commandGetOpts showOrParse command =
224224
concatMap viewAsGetOpt (commandOptions command showOrParse)
225225

226-
viewAsGetOpt :: OptionField a -> [GetOpt.OptDescr (a->a)]
226+
viewAsGetOpt :: OptionField a -> [GetOpt.OptDescr (a -> a)]
227227
viewAsGetOpt (OptionField _n aa) = concatMap optDescrToGetOpt aa
228228
where
229229
optDescrToGetOpt (ReqArg d (cs,ss) arg_desc set _) =
230-
[GetOpt.Option cs ss (GetOpt.ReqArg set' arg_desc) d]
231-
where set' = readEOrFail set
230+
[GetOpt.Option cs ss (GetOpt.ReqArg (runReadE set) arg_desc) d]
232231
optDescrToGetOpt (OptArg d (cs,ss) arg_desc set def _) =
233232
[GetOpt.Option cs ss (GetOpt.OptArg set' arg_desc) d]
234-
where set' Nothing = def
235-
set' (Just txt) = readEOrFail set txt
233+
where set' Nothing = Right def
234+
set' (Just txt) = runReadE set txt
236235
optDescrToGetOpt (ChoiceOpt alts) =
237236
[GetOpt.Option sf lf (GetOpt.NoArg set) d | (d,(sf,lf),set,_) <- alts ]
238237
optDescrToGetOpt (BoolOpt d (sfT, lfT) ([], []) set _) =
@@ -391,13 +390,8 @@ addCommonFlags :: ShowOrParseArgs
391390
-> [GetOpt.OptDescr a]
392391
-> [GetOpt.OptDescr (Either CommonFlag a)]
393392
addCommonFlags showOrParseArgs options =
394-
map (fmapOptDesc Left) (commonFlags showOrParseArgs)
395-
++ map (fmapOptDesc Right) options
396-
where fmapOptDesc f (GetOpt.Option s l d m) =
397-
GetOpt.Option s l (fmapArgDesc f d) m
398-
fmapArgDesc f (GetOpt.NoArg a) = GetOpt.NoArg (f a)
399-
fmapArgDesc f (GetOpt.ReqArg s d) = GetOpt.ReqArg (f . s) d
400-
fmapArgDesc f (GetOpt.OptArg s d) = GetOpt.OptArg (f . s) d
393+
map (fmap Left) (commonFlags showOrParseArgs)
394+
++ map (fmap Right) options
401395

402396
-- | Parse a bunch of command line arguments
403397
--

changelog.d/option-argument-errors

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
synopsis: Handle option argument parse errors without 'error'
2+
packages: Cabal, cabal-install
3+
prs: #7579
4+
issues: #7573
5+
description: {
6+
- Errors parsing arguments such as `-v=3` no longer result in
7+
stack traces.
8+
- `Distribution.ReadE.readEOrFail` was removed.
9+
}

0 commit comments

Comments
 (0)