Reputation: 4520
I have a basic command add
that takes 2 kind of arguments: a word or a tag. A tag is just a word starting by +
. A word is just a String
. It can contain at least one argument (I use some
for this).
data Arg = Add AddOpts
data AddOpts = AddOpts
{ desc :: String,
tags :: [String]
}
deriving (Show)
addCommand :: Mod CommandFields Arg
addCommand = command "add" (info parser infoMod)
where
infoMod = progDesc "Add a new task"
parser = Add <$> parseDescAndTags <$> partition isTag <$> some (argument str (metavar "DESC"))
parseDescAndTags (_, []) = FAIL HERE
parseDescAndTags (tags, desc) = AddOpts (unwords desc) (map tail tags)
I want to add another rule: the add
command should receive at least one word (but 0 or more tags). For this, I need to check after the first parsing the word list. If it's empty, I would like to fail as if the add
commands received no argument, but I can't figure out how to do.
Upvotes: 1
Views: 218
Reputation: 55059
parseDescAndTags
is currently a pure function, so there’s no way for it to cause parsing to fail. Just to get this out of the way, I should also note that in this code:
Add <$> parseDescAndTags <$> partition isTag <$> some (argument str (metavar "DESC"))
The operator <$>
is declared infixl 4
, so it’s left-associative, and your expression is therefore equivalent to:
((Add <$> parseDescAndTags) <$> partition isTag) <$> some (argument str (metavar "DESC"))
You happen to be using <$>
in the “function reader” functor, (->) a
, which is equivalent to composition (.)
:
Add . parseDescAndTags . partition isTag <$> some (argument str (metavar "DESC"))
If you want to use ReadM
, you need to use functions such as eitherReader
to construct a ReadM
action. But the problem is that you would need to use it as the first argument to argument
instead of the str
reader, and that’s the wrong place for it, since some
is on the outside and you want to fail parsing based on the accumulated results of the whole option.
Unfortunately that kind of context-sensitive parsing is not what optparse-applicative
is designed for; it doesn’t offer a Monad
instance for parsers.
Currently, your parser allows tags and descriptions to be interleaved, like this (supposing isTag = (== ".") . take 1
for illustration):
add some .tag1 description .tag2 text
Producing "some description text"
for the description and [".tag1", ".tag2"]
as the tags. Is that what you want, or can you use a simpler format instead, like requiring all tags at the end?
add some description text .tag1 .tag2
If so, the result is simple: parse at least one non-tag with some
, then any number of tags with many
:
addCommand :: Mod CommandFields Arg
addCommand = command "add" (info parser infoMod)
where
infoMod = progDesc "Add a new task"
parser = Add <$> addOpts
addOpts = AddOpts
<$> (unwords <$> some (argument nonTag (metavar "DESC")))
<*> many (argument tag (metavar "TAG"))
nonTag = eitherReader
$ \ str -> if isTag str
then Left ("unexpected tag: '" <> str <> "'")
else Right str
tag = eitherReader
$ \ str -> if isTag str
then Right $ drop 1 str
else Left ("not a tag: '" <> str <> "'")
As an alternative, you can parse command-line options with optparse-applicative
, but do any more complex validation on your options records after running the parser. Then if you want to print the help text manually, you can use:
printHelp :: ParserPrefs -> ParserInfo a -> IO a
printHelp parserPrefs parserInfo = handleParseResult $ Failure
$ parserFailure parserPrefs parserInfo ShowHelpText mempty
Upvotes: 1