packages feed

fb-util-0.1.0.1: Util/OptParse.hs

{-
  Copyright (c) Meta Platforms, Inc. and affiliates.
  All rights reserved.

  This source code is licensed under the BSD-style license found in the
  LICENSE file in the root directory of this source tree.
-}

{-# LANGUAGE CPP #-}
module Util.OptParse
  ( -- * Options
    intOption
  , doubleOption
  , textOption
  , maybeStrOption
  , maybeTextOption
  , maybeIntOption
  , textCommaSplit
  , testCommaSplitAndStrip
  , stringCommaSplit
  , readCommaSplit
  , absFilePathOption
  , relativeFilePathOption
  , maybeAbsFilePathOption
  , maybeRelativeFilePathOption
  , jsonOption
  , showHelpText
    -- * Commands
  , commandParser
    -- * Pure Parser
  , runParserOnString
    -- * Dragons and stuff
  , partialParse
  ) where

import Data.Aeson (FromJSON(..), eitherDecodeStrict')
import Data.Bifunctor (second)
import Data.Char (isSpace)
import qualified Data.List.Split as List
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8)
import Options.Applicative
import Options.Applicative.Types

import Util.FilePath
import Util.String


intOption :: Mod OptionFields Int -> Parser Int
intOption = option auto

doubleOption :: Mod OptionFields Double -> Parser Double
doubleOption = option auto

textOption :: Mod OptionFields Text -> Parser Text
textOption = option (Text.pack <$> str)

maybeStrOption :: Mod OptionFields String -> Parser (Maybe String)
maybeStrOption = optional . option str

maybeTextOption :: Mod OptionFields Text -> Parser (Maybe Text)
maybeTextOption = optional . option (Text.pack <$> str)

maybeIntOption :: Mod OptionFields Int -> Parser (Maybe Int)
maybeIntOption = optional . option (read <$> str)

textCommaSplit :: ReadM [Text]
textCommaSplit = Text.splitOn "," . Text.pack <$> str

testCommaSplitAndStrip :: ReadM [Text]
testCommaSplitAndStrip = map Text.strip <$> textCommaSplit

stringCommaSplit :: ReadM [String]
stringCommaSplit = List.splitOn "," <$> str

readCommaSplit :: Read a => ReadM [a]
readCommaSplit = map (read . strip) <$> stringCommaSplit

jsonOption :: FromJSON a => Mod OptionFields a -> Parser a
jsonOption =
  option (eitherReader $ eitherDecodeStrict' . encodeUtf8 . Text.pack)

-- | Returns an absolute file path, while handling relative paths and ~
absFilePathOption :: DirEnv -> Mod OptionFields FilePath -> Parser FilePath
absFilePathOption e = option (absolutiseWith e <$> str)

-- | Returns a relative file path
relativeFilePathOption :: Mod OptionFields FilePath -> Parser FilePath
relativeFilePathOption = option str


-- | Returns an absolute file path, while handling relative paths and ~
maybeAbsFilePathOption
  :: DirEnv
  -> Mod OptionFields FilePath
  -> Parser (Maybe FilePath)
maybeAbsFilePathOption e = optional . option (absolutiseWith e <$> str)

-- | Returns a relative file path
maybeRelativeFilePathOption
  :: Mod OptionFields FilePath
  -> Parser (Maybe FilePath)
maybeRelativeFilePathOption = optional . option str

-- | Build a command subparser from a name, description, and parser.
--
-- Example: commandParser "foo" (progDesc "Foo does bar.") (... parser ...)
commandParser
  :: String     -- ^ command name (the 'cmd' in 'prog cmd --cmdflag1')
  -> InfoMod a  -- ^ command description in --help output
  -> Parser a   -- ^ parser of command's flags
  -> Parser a
commandParser cmd desc p =
  subparser $ metavar cmd <> command cmd (info (helper <*> p) desc)

-- | Run given parser on a string.
runParserOnString :: String -> Parser a -> String -> Either String a
runParserOnString cmdName p args =
  case parse of
    Success x -> Right x
    Failure f -> Left $ fst $ renderFailure f cmdName
    CompletionInvoked _ -> Left "Completion Invoked"
  where
    parse = execParserPure
      (prefs mempty) (info (p <**> helper) fullDesc) (quotedWords args)
    recurse (w,s) = w : quotedWords s
    -- Mimic shell's ability to group tokens with double quotes.
    quotedWords s =
      case dropWhile isSpace s of
        "" -> []
        ('"':cs) -> recurse . second tail $ break (=='"') cs
        s' -> recurse $ break isSpace s'

showHelpText :: ParseError
#if MIN_VERSION_optparse_applicative(0,16,0)
showHelpText = ShowHelpText Nothing
#else
showHelpText = ShowHelpText
#endif

-- -----------------------------------------------------------------------------
-- Things from Options.Applicative that have changed

-- | Attempts to parse across *all* arguments, returning ones that were
-- unrecognized. Fails if a required argument is missing.
partialParse :: ParserPrefs -> ParserInfo a -> [String] -> IO (a, [String])
partialParse pprefs pinfo args = handleParseResult res
  where
    pinfo' = pinfo
      { infoParser = (,) <$> infoParser pinfo <*> many (strArgument mempty)
      , infoPolicy = ForwardOptions
      }
    res = execParserPure pprefs pinfo' args