pcapriotti / pcapriotti/optparse-applicative

Support exactly-one mutually exclusive options

Open
#513 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Haskell
Stars
959
Forks
123
Avg merge
3d 19h
Merged PRs (30d)
1

Description

Related: #111

I want to allow the user to specify exactly one of --modified-files, --staged-files, or --all-files with a nice error. If I follow the current recommendation in #111, I get the following behavior:

oneOf
  [ switch (long "modified-files")
  , switch (long "staged-files")
  , switch (long "all-files")
  ]

oneOf = foldr (<|>) empty
$ example
Missing: (--modified-files | --staged-files | --all-files)

$ example --modified-files
Works

$ example --modified-files --staged-files
Unknown flag: --staged-files

This has the correct semantics, and the error message when no options are passed is pretty good. The only issue is that passing multiple flags shows an unhelpful error message, when clearly a good error message is possible.

My current workaround is:

oneOf :: [Opt.Parser a] -> Opt.Parser a
oneOf parsers = validate <$> traverse Opt.optional parsers
 where
  validate results =
    case catMaybes results of
      [a] -> a
      _ -> error $ "Expected exactly one of: " <> (Text.intercalate ", " . map Text.pack) optNames

  optNames = concatMap getOptNames parsers
  getOptNames :: Opt.Parser x -> [String]
  getOptNames = \case
    Opt.Internal.NilP _ -> []
    Opt.Internal.OptP opt -> [getOptName opt]
    Opt.Internal.MultP p1 p2 -> getOptNames p1 <> getOptNames p2
    Opt.Internal.AltP p1 p2 -> getOptNames p1 <> getOptNames p2
    Opt.Internal.BindP p _ -> getOptNames p
  getOptName opt =
    case Opt.Internal.optMain opt of
      Opt.Internal.OptReader names _ _ | Just name <- getName names -> name
      Opt.Internal.FlagReader names _ | Just name <- getName names -> name
      _ -> Opt.Internal.propMetaVar $ Opt.Internal.optProps opt
  getName names
    | n : _ <- [n | Opt.Internal.OptLong n <- names] = Just ("--" <> n)
    | c : _ <- [c | Opt.Internal.OptShort c <- names] = Just ['-', c]
    | otherwise = Nothing

but it's super hacky and only fails when the result is used in main, instead of during the Parser evaluation. It would be great to have something officially supported here.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the current oneOf recommendation from #111 and the parser combinators shown in this issue. Investigate where multiple alternatives become an unknown-flag error, then define support for exactly-one parsing so multiple selections produce a clear error during parsing rather than in main; done means the no-option, one-option, and multiple-option cases have useful messages.

Written by the indexing model from the issue text.

Assessment

Tech stack
haskell
Domain
cli
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.