packages feed

scrod-0.2026.2.21: source/library/Scrod/Executable/Flag.hs

{-# LANGUAGE TemplateHaskellQuotes #-}

-- | CLI flag parsing via 'GetOpt'.
module Scrod.Executable.Flag where

import qualified Control.Monad.Catch as Exception
import qualified GHC.Stack as Stack
import qualified Scrod.Spec as Spec
import qualified System.Console.GetOpt as GetOpt

data Flag
  = Format String
  | GhcOption String
  | Help (Maybe String)
  | Literate (Maybe String)
  | Schema (Maybe String)
  | Signature (Maybe String)
  | Version (Maybe String)
  deriving (Eq, Ord, Show)

fromArguments :: (Stack.HasCallStack, Exception.MonadThrow m) => [String] -> m [Flag]
fromArguments arguments = do
  let (flgs, args, opts, errs) = GetOpt.getOpt' GetOpt.Permute optDescrs arguments
  mapM_ (Exception.throwM . userError . mappend "invalid option: ") errs
  mapM_ (Exception.throwM . userError . mappend "unknown option: ") opts
  mapM_ (Exception.throwM . userError . mappend "unexpected argument: ") args
  pure flgs

optDescrs :: [GetOpt.OptDescr Flag]
optDescrs =
  -- First help, then version, then everything else in alphabetical order.
  [ GetOpt.Option ['h'] ["help"] (GetOpt.OptArg Help "BOOL") "Shows the help.",
    GetOpt.Option [] ["version"] (GetOpt.OptArg Version "BOOL") "Shows the version.",
    GetOpt.Option [] ["format"] (GetOpt.ReqArg Format "FORMAT") "Sets the output format (json or html).",
    GetOpt.Option [] ["ghc-option"] (GetOpt.ReqArg GhcOption "OPTION") "Sets a GHC option (e.g. -XOverloadedStrings).",
    GetOpt.Option [] ["literate"] (GetOpt.OptArg Literate "BOOL") "Treats the input as Literate Haskell.",
    GetOpt.Option [] ["schema"] (GetOpt.OptArg Schema "BOOL") "Shows the JSON output schema.",
    GetOpt.Option [] ["signature"] (GetOpt.OptArg Signature "BOOL") "Treats the input as a Backpack signature."
  ]

spec :: (Applicative m, Monad n) => Spec.Spec m n -> n ()
spec s = do
  Spec.named s 'fromArguments $ do
    Spec.it s "works with no arguments" $ do
      Spec.assertEq s (fromArguments []) $ Just []

    Spec.it s "fails with an unknown option" $ do
      Spec.assertEq s (fromArguments ["-x"]) Nothing

    Spec.it s "fails with an unexpected argument" $ do
      Spec.assertEq s (fromArguments ["x"]) Nothing

    Spec.describe s "format" $ do
      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--format=json"]) $ Just [Format "json"]

      Spec.it s "works with html" $ do
        Spec.assertEq s (fromArguments ["--format=html"]) $ Just [Format "html"]

      Spec.it s "fails with no argument" $ do
        Spec.assertEq s (fromArguments ["--format"]) Nothing

    Spec.describe s "help" $ do
      Spec.it s "works with no argument" $ do
        Spec.assertEq s (fromArguments ["--help"]) $ Just [Help Nothing]

      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--help="]) $ Just [Help $ Just ""]

    Spec.describe s "literate" $ do
      Spec.it s "works with no argument" $ do
        Spec.assertEq s (fromArguments ["--literate"]) $ Just [Literate Nothing]

      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--literate="]) $ Just [Literate $ Just ""]

    Spec.describe s "schema" $ do
      Spec.it s "works with no argument" $ do
        Spec.assertEq s (fromArguments ["--schema"]) $ Just [Schema Nothing]

      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--schema="]) $ Just [Schema $ Just ""]

    Spec.describe s "signature" $ do
      Spec.it s "works with no argument" $ do
        Spec.assertEq s (fromArguments ["--signature"]) $ Just [Signature Nothing]

      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--signature="]) $ Just [Signature $ Just ""]

    Spec.describe s "version" $ do
      Spec.it s "works with no argument" $ do
        Spec.assertEq s (fromArguments ["--version"]) $ Just [Version Nothing]

      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--version="]) $ Just [Version $ Just ""]

    Spec.describe s "ghc-option" $ do
      Spec.it s "works with an argument" $ do
        Spec.assertEq s (fromArguments ["--ghc-option=-XOverloadedStrings"]) $ Just [GhcOption "-XOverloadedStrings"]

      Spec.it s "fails with no argument" $ do
        Spec.assertEq s (fromArguments ["--ghc-option"]) Nothing

      Spec.it s "works with multiple" $ do
        Spec.assertEq s (fromArguments ["--ghc-option=-XCPP", "--ghc-option=-XGADTs"]) $ Just [GhcOption "-XCPP", GhcOption "-XGADTs"]