packages feed

mangrove-cli-0.1.0.0: src/Mangrove.hs

{-# LANGUAGE DataKinds          #-}
{-# LANGUAGE FlexibleContexts   #-}
{-# LANGUAGE GADTs              #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators      #-}

{-|
Module      : Mangrove
Copyright   : (c) Quytelda Kahja, 2026
License     : BSD-3-Clause

This module contains an API (types and functions) for running argument parsers.
-}
module Mangrove
  ( -- * Standard Interface
    parseArguments

    -- * Types
  , ProgramInfo(..)
  , ParseTree
  , Scheme
  , Result(..)
  , SupportsHelp
  , StreamState
  , HelpHandler
  , HelpContinuation(..)

    -- * Pure Interface
    -- ** Helpful Parsers
  , runHelpfulParser
  , runHelpfulParser'
  , runHelpfulParser_

    -- ** Silent Parsers
  , runSilentParser
  , runSilentParser'

    -- ** General Parsers (CPS)
  , runArgumentParser
  , runArgumentParser'
  ) where

import           Data.Text           (Text)
import qualified Data.Text           as T
import qualified Data.Text.IO        as TIO
import           System.Environment
import           System.Exit
import           System.IO

import           Mangrove.Parser
import           Mangrove.Resolve
import           Mangrove.Text

-- | Program metadata for displaying help output.
data ProgramInfo = ProgramInfo
  { programName :: !Text -- ^ The program name
  , programDesc :: !Text -- ^ A description of the program
  } deriving (Show)

-- | The results of a parsing operation.
--
-- Only parsing schemes that support generating help output will yield
-- 'Help' values.
data Result s r where
  -- | A successful parsing operation yields a list of leftover
  -- arguments and a result value.
  Success :: ![Text] -> !r -> Result s r
  -- | A failed parsing operation yields an error message.
  Failure :: !Text -> Result s r
  -- | A request for help yields human-readable help output (for
  -- parsers that support it).
  Help :: SupportsHelp s => !Text -> Result s r

deriving instance Show r => Show (Result s r)
deriving instance Eq r => Eq (Result s r)

-- | Create a default initial 'StreamState' from a list of arguments.
argsToState :: [Text] -> StreamState s
argsToState args = StreamState args [] False

-- | Attempt to parse a value of type @r@ from a list of arguments,
-- where the parser @ParseTree s r@ doesn't support help output.
runSilentParser
  :: (Scheme s, HelpSupport s ~ 'Silent)
  => ParseTree s r -- ^ Argument parser
  -> [Text] -- ^ Input arguments
  -> Result s r
runSilentParser tree = runSilentParser' tree . argsToState

-- | A more general form of 'runSilentParser' that accepts a custom
-- 'StreamState' as the starting state.
runSilentParser'
  :: (Scheme s, HelpSupport s ~ 'Silent)
  => ParseTree s r -- ^ Argument parser
  -> StreamState s -- ^ Initial stream state
  -> Result s r
runSilentParser' tree state =
  runArgumentParser' tree state Success Failure NoHelp

-- | Attempt to parse a value of type @r@ from a list of arguments,
-- where the parser @ParseTree s r@ supports help output.
runHelpfulParser
  :: SupportsHelp s
  => ProgramInfo -- ^ Program metadata
  -> ParseTree s r -- ^ Argument parser
  -> [Text] -- ^ Input arguments
  -> Result s r
runHelpfulParser info tree = runHelpfulParser' info tree . argsToState

-- | A more general form of 'runHelpfulParser' that accepts a custom
-- 'StreamState' as the starting state.
runHelpfulParser'
  :: SupportsHelp s
  => ProgramInfo -- ^ Program metadata
  -> ParseTree s r -- ^ Argument parser
  -> StreamState s -- ^ Initial stream state
  -> Result s r
runHelpfulParser' info tree state =
  runArgumentParser' tree state Success Failure (OnHelp _onHelpRequest)
  where
    _onHelpRequest state' =
      Help $ makeHelpInfo tree (streamContext state') (programName info) (programDesc info)

-- | A variant of 'runHelpfulParser' that treats help requests as
-- failures.
runHelpfulParser_
  :: SupportsHelp s
  => ParseTree s r -- ^ Argument parser
  -> [Text] -- ^ Input arguments
  -> Result s r
runHelpfulParser_ tree args =
  runArgumentParser' tree (argsToState args) Success Failure (OnHelp _onHelpRequest)
  where
    _onHelpRequest state' = Failure $
      formatError (streamContext state') "help requested"

-- | Parse the command line arguments passed to the program, then
-- invoke the program's entrypoint with the results of the parsing. If
-- parsing fails, we instead display an error to stderr and exit.
-- Alternatively, if help was requested, we abandon parsing and print
-- the relevant help output to stdout, then exit without indicating an
-- error.
parseArguments
  :: SupportsHelp s
  => ProgramInfo -- ^ Program metadata
  -> ParseTree s r -- ^ Argument parser
  -> (r -> IO a) -- ^ Program Entrypoint
  -> IO a
parseArguments info tree action = do
  args <- map T.pack <$> getArgs
  case runHelpfulParser info tree args of
    Success [] result -> action result
    Success (token:_) _ -> do
      hPutBuilder stderr $ "unexpected " <> render token <> "\n"
      exitFailure
    Failure err -> do
      TIO.hPutStrLn stderr err
      exitFailure
    Help output -> do
      TIO.putStr output
      exitSuccess

-- | Satiate a 'ParseTree' with all the input it can consume, then
-- attempt to evaluate it.
runArgumentParser
  :: Scheme s
  => ParseTree s r -- ^ Argument parser
  -> [Text] -- ^ Input arguments
  -> ([Text] -> r -> a) -- ^ Success handler
  -> (Text -> a) -- ^ Failure handler
  -> HelpHandler s a -- ^ Help request handler
  -> a
runArgumentParser tree args =
  runArgumentParser' tree StreamState
  { streamContent = args
  , streamContext = []
  , streamEscaped = False
  }

-- | A more general form of 'runArgumentParser' that accepts a custom
-- 'StreamState' as the starting state.
runArgumentParser'
  :: Scheme s
  => ParseTree s r -- ^ Argument parser
  -> StreamState s -- ^ Initial stream state
  -> ([Text] -> r -> a) -- ^ Success handler
  -> (Text -> a) -- ^ Failure handler
  -> HelpHandler s a -- ^ Help request handler
  -> a
runArgumentParser' tree state cok cerr hhelp =
  runStreamParser (satiate tree) handler state
  where
    _onFailure state' = cerr . formatError (streamContext state')
    _onSuccess state' tree' =
      case (streamContent state', resolve tree') of
        (leftovers, Value result) -> cok leftovers result
        ([], EmptyError)          -> _onFailure state' "empty"
        ([], ExpectedError es)    -> _onFailure state' $ renderExpectedError es
        (token:_, _)              -> _onFailure state' $ "unexpected " <> render token
    handler = StreamHandler
      { onSuccess = _onSuccess
      , onFailure = _onFailure
      , onEmpty = flip _onFailure "empty"
      , onHelpRequest = hhelp
      }