packages feed

core-program 0.3.2.0 → 0.3.4.0

raw patch · 4 files changed

+221/−54 lines, 4 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Core.Program.Execute: lookupArgument :: LongName -> Parameters -> Maybe String
- Core.Program.Execute: lookupEnvironmentValue :: LongName -> Parameters -> Maybe String
- Core.Program.Execute: lookupOptionFlag :: LongName -> Parameters -> Maybe Bool
- Core.Program.Execute: lookupOptionValue :: LongName -> Parameters -> Maybe String
+ Core.Program.Arguments: Remaining :: Description -> Options
+ Core.Program.Arguments: [remainingArgumentsFrom] :: Parameters -> [String]
+ Core.Program.Execute: queryArgument :: LongName -> Program τ Rope
+ Core.Program.Execute: queryEnvironmentValue :: LongName -> Program τ (Maybe Rope)
+ Core.Program.Execute: queryOptionFlag :: LongName -> Program τ Bool
+ Core.Program.Execute: queryOptionValue :: LongName -> Program τ (Maybe Rope)
+ Core.Program.Execute: queryRemaining :: Program τ [Rope]
- Core.Program.Arguments: Parameters :: Maybe LongName -> Map LongName ParameterValue -> Map LongName ParameterValue -> Parameters
+ Core.Program.Arguments: Parameters :: Maybe LongName -> Map LongName ParameterValue -> [String] -> Map LongName ParameterValue -> Parameters

Files

core-program.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           core-program-version:        0.3.2.0+version:        0.3.4.0 synopsis:       Opinionated Haskell Interoperability description:    A library to help build command-line programs, both tools and                 longer-running daemons.
lib/Core/Program/Arguments.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-}@@ -323,6 +324,18 @@ which ends with a full stop. For options that take values, use /upper case/ when specifying the label to be used in help output. +'Remaining' is special; it indicates that you are expecting a variable number+of additional, non-mandatory arguments. This is used for programs which take a+list of files to process, for example. It'll show up in the help with the+description you supply alongside.++@+        [ ...+        , 'Remaining' "The files you wish to delete permanently."+        , ...+        ]+@+ 'Variable' declares an /environment variable/ that, if present, will be read by the program and stored in its runtime context. By convention these are /upper case/. If the identifier is two or more words they are joined@@ -338,6 +351,7 @@ data Options     = Option LongName (Maybe ShortName) ParameterValue Description     | Argument LongName Description+    | Remaining Description     | Variable LongName Description  appendOption :: Options -> Config -> Config@@ -406,6 +420,7 @@ data Parameters = Parameters     { commandNameFrom :: Maybe LongName     , parameterValuesFrom :: Map LongName ParameterValue+    , remainingArgumentsFrom :: [String]     , environmentValuesFrom :: Map LongName ParameterValue     }     deriving (Show, Eq)@@ -415,6 +430,7 @@     Parameters         { commandNameFrom = Nothing         , parameterValuesFrom = emptyMap+        , remainingArgumentsFrom = []         , environmentValuesFrom = emptyMap         } @@ -535,22 +551,24 @@ -} parseCommandLine :: Config -> [String] -> Either InvalidCommandLine Parameters parseCommandLine config argv = case config of-    Blank -> return (Parameters Nothing emptyMap emptyMap)+    Blank -> return (Parameters Nothing emptyMap [] emptyMap)     Simple options -> do-        params <- extractor Nothing options argv-        return (Parameters Nothing params emptyMap)+        (params, remainder) <- extractor Nothing options argv+        checkRemainder options remainder+        return (Parameters Nothing params remainder emptyMap)     Complex commands ->         let globalOptions = extractGlobalOptions commands             modes = extractValidModes commands          in do                 (possibles, argv') <- splitCommandLine1 argv-                params1 <- extractor Nothing globalOptions possibles-                (first, remainingArgs) <- splitCommandLine2 argv'+                (params1, _) <- extractor Nothing globalOptions possibles+                (first, moreArgs) <- splitCommandLine2 argv'                 (mode, localOptions) <- parseIndicatedCommand modes first-                params2 <- extractor (Just mode) localOptions remainingArgs-                return (Parameters (Just mode) ((<>) params1 params2) emptyMap)+                (params2, remainder) <- extractor (Just mode) localOptions moreArgs+                checkRemainder localOptions remainder+                return (Parameters (Just mode) ((<>) params1 params2) remainder emptyMap)   where-    extractor :: Maybe LongName -> [Options] -> [String] -> Either InvalidCommandLine (Map LongName ParameterValue)+    extractor :: Maybe LongName -> [Options] -> [String] -> Either InvalidCommandLine ((Map LongName ParameterValue), [String])     extractor mode options args =         let (possibles, arguments) = List.partition isOption args             valids = extractValidNames options@@ -558,9 +576,29 @@             needed = extractRequiredArguments options          in do                 list1 <- parsePossibleOptions mode valids shorts possibles-                list2 <- parseRequiredArguments needed arguments-                return ((<>) (intoMap list1) (intoMap list2))+                (list2, arguments') <- parseRequiredArguments needed arguments+                pure (((<>) (intoMap list1) (intoMap list2)), arguments') +    checkRemainder :: [Options] -> [String] -> Either InvalidCommandLine ()+    checkRemainder options remainder =+        if List.null remainder+            then Right ()+            else+                if hasRemaining options+                    then Right ()+                    else Left (UnexpectedArguments remainder)++-- is one of the options Remaining?+hasRemaining :: [Options] -> Bool+hasRemaining options =+    List.foldl'+        ( \acc option -> case option of+            Remaining _ -> True+            _ -> acc+        )+        False+        options+ isOption :: String -> Bool isOption arg = case arg of     ('-' : _) -> True@@ -603,21 +641,20 @@ parseRequiredArguments ::     [LongName] ->     [String] ->-    Either InvalidCommandLine [(LongName, ParameterValue)]+    Either InvalidCommandLine ([(LongName, ParameterValue)], [String]) parseRequiredArguments needed argv = iter needed argv   where-    iter :: [LongName] -> [String] -> Either InvalidCommandLine [(LongName, ParameterValue)]--    iter [] [] = Right []+    iter :: [LongName] -> [String] -> Either InvalidCommandLine ([(LongName, ParameterValue)], [String])+    iter [] [] = Right ([], [])     -- more arguments supplied than expected-    iter [] args = Left (UnexpectedArguments args)+    iter [] args = Right ([], args)     -- more arguments required, not satisfied     iter (name : _) [] = Left (MissingArgument name)     iter (name : names) (arg : args) =         let deeper = iter names args          in case deeper of                 Left e -> Left e-                Right list -> Right ((name, Value arg) : list)+                Right (list, remainder) -> Right (((name, Value arg) : list), remainder)  parseIndicatedCommand ::     Map LongName [Options] ->@@ -751,6 +788,7 @@                             [ pretty programName                             , optionsSummary o                             , argumentsSummary a+                            , remainingSummary a                             ]                         )                     )@@ -796,6 +834,7 @@                                     , commandSummary modes                                     , localSummary oL                                     , argumentsSummary aL+                                    , remainingSummary aL                                     ]                                 )                             )@@ -835,6 +874,9 @@      argumentsHeading as = if length as > 0 then hardline <> "Required arguments:" <> hardline else emptyDoc +    remainingSummary :: [Options] -> Doc ann+    remainingSummary as = if hasRemaining as then  " ..." else emptyDoc+     -- there is a corner case of complex config with no commands     commandSummary modes = if length modes > 0 then softline <> commandName else emptyDoc     commandHeading modes = if length modes > 0 then hardline <> "Available commands:" <> hardline else emptyDoc@@ -842,6 +884,7 @@     f :: Options -> ([Options], [Options]) -> ([Options], [Options])     f o@(Option _ _ _ _) (opts, args) = (o : opts, args)     f a@(Argument _ _) (opts, args) = (opts, a : args)+    f a@(Remaining _) (opts, args) = (opts, a : args)     f (Variable _ _) (opts, args) = (opts, args)      formatParameters :: [Options] -> Doc ann@@ -873,6 +916,9 @@         let l = pretty longname             d = fromRope description          in fillBreak 16 ("  " <> l <> " ") <+> align (reflow d) <> hardline <> acc+    g (Remaining description) acc =+        let d = fromRope description+         in fillBreak 16 ("  " <>  "... ") <+> align (reflow d) <> hardline <> acc     g (Variable longname description) acc =         let l = pretty longname             d = fromRope description
lib/Core/Program/Execute.hs view
@@ -61,10 +61,11 @@      -- * Accessing program context     getCommandLine,-    lookupOptionFlag,-    lookupOptionValue,-    lookupArgument,-    lookupEnvironmentValue,+    queryOptionFlag,+    queryOptionValue,+    queryArgument,+    queryRemaining,+    queryEnvironmentValue,     getProgramName,     setProgramName,     getVerbosityLevel,@@ -94,8 +95,12 @@     unProgram,     unThread,     invalid,-    Boom(..),+    Boom (..),     loopForever,+    lookupOptionFlag,+    lookupOptionValue,+    lookupArgument,+    lookupEnvironmentValue, ) where  import Chrono.TimeStamp (getCurrentTimeNanoseconds)@@ -113,6 +118,7 @@     wait,  ) import Control.Concurrent.MVar (+    MVar,     modifyMVar_,     newMVar,     putMVar,@@ -150,7 +156,7 @@ import GHC.Conc (getNumProcessors, numCapabilities, setNumCapabilities) import GHC.IO.Encoding (setLocaleEncoding, utf8) import System.Directory (-    findExecutable+    findExecutable,  ) import System.Exit (ExitCode (..)) import qualified System.Posix.Process as Posix (exitImmediately)@@ -280,7 +286,7 @@     -- set up debug logger     l <-         Async.async $ do-            processTelemetryMessages forwarder out tel+            processTelemetryMessages forwarder level out tel      -- run actual program, ensuring to grab any otherwise uncaught exceptions.     code <-@@ -311,9 +317,12 @@         ( do             atomically $ do                 writeTQueue tel Nothing-                writeTQueue out Nothing              Async.wait l++            atomically $ do+                writeTQueue out Nothing+             Async.wait o         )         ( do@@ -356,17 +365,16 @@ -- the technique used   by **io-streams** to just pass along a stream of Maybes, -- with Nothing signalling end-of-stream is exactly good enough for our needs. ---processTelemetryMessages :: Forwarder -> TQueue (Maybe Rope) -> TQueue (Maybe Datum) -> IO ()-processTelemetryMessages processor out tel = do+processTelemetryMessages :: Forwarder -> MVar Verbosity -> TQueue (Maybe Rope) -> TQueue (Maybe Datum) -> IO ()+processTelemetryMessages processor v out tel = do     Safe.catch-        (loopForever action out tel)+        (loopForever action v out tel)         (collapseHandler "telemetry processing collapsed")   where     action = telemetryHandlerFrom processor -loopForever :: ([a] -> IO ()) -> TQueue (Maybe Rope) -> TQueue (Maybe a) -> IO ()-loopForever action out queue = do-    start <- getCurrentTimeNanoseconds+loopForever :: ([a] -> IO ()) -> MVar Verbosity -> TQueue (Maybe Rope) -> TQueue (Maybe a) -> IO ()+loopForever action v out queue = do     -- block waiting for an item     possibleItems <- atomically $ do         cycleOverQueue []@@ -376,20 +384,16 @@         Nothing -> pure ()         -- handle it and loop         Just items -> do+            start <- getCurrentTimeNanoseconds             catch-                (action (reverse items))+                ( do+                    action (reverse items)+                    reportStatus start (length items)+                )                 ( \(e :: SomeException) -> do-                    now <- getCurrentTimeNanoseconds-                    let result =-                            formatLogMessage-                                start-                                now-                                SeverityWarn-                                ("sending telemetry failed (Exception: " <> intoRope (show e) <> "); Restarting exporter.")-                    atomically $ do-                        writeTQueue out (Just result)+                    reportProblem start e                 )-            loopForever action out queue+            loopForever action v out queue   where     cycleOverQueue items =         case items of@@ -420,6 +424,35 @@                             Just item -> do                                 cycleOverQueue (item : items) +    reportStatus start num = do+        level <- readMVar v+        when (isDebug level) $ do+            now <- getCurrentTimeNanoseconds+            let desc = case num of+                    1 -> "1 event"+                    _ -> intoRope (show num) <> " events"+                message =+                    formatLogMessage+                        start+                        now+                        SeverityInternal+                        ("telemetry: sent " <> desc)+            atomically $ do+                writeTQueue out (Just message)++    reportProblem start e = do+        level <- readMVar v+        when (isEvent level) $ do+            now <- getCurrentTimeNanoseconds+            let message =+                    formatLogMessage+                        start+                        now+                        SeverityWarn+                        ("sending telemetry failed (Exception: " <> intoRope (show e) <> "); Restarting exporter.")+            atomically $ do+                writeTQueue out (Just message)+ {- | Safely exit the program with the supplied exit code. Current output and debug queues will be flushed, and then the process will terminate.@@ -752,8 +785,7 @@  This is available should you need to differentiate between a @Value@ and an @Empty@ 'ParameterValue', but for many cases as a convenience you can use the-'lookupOptionFlag', 'lookupOptionValue', and 'lookupArgument' functions below-(which are just wrappers around a code block like the example shown here).+'queryOptionFlag', 'queryOptionValue', and 'queryArgument' functions below. -} getCommandLine :: Program τ (Parameters) getCommandLine = do@@ -762,11 +794,24 @@  {- | Arguments are mandatory, so by the time your program is running a value-has already been identified. This returns the value for that parameter.+has already been identified. This retreives the value for that parameter.++@+program = do+    file <- 'queryArgument' \"filename\"+    ...+@ -}+queryArgument :: LongName -> Program τ Rope+queryArgument name = do+    context <- ask+    let params = commandLineFrom context+    case lookupKeyValue name (parameterValuesFrom params) of+        Nothing -> error "Attempted lookup of unconfigured argument"+        Just argument -> case argument of+            Empty -> error "Invalid State"+            Value value -> pure (intoRope value) --- this is Maybe because you can inadvertently ask for an unconfigured name--- this could be fixed with a much stronger Config type, potentially. lookupArgument :: LongName -> Parameters -> Maybe String lookupArgument name params =     case lookupKeyValue name (parameterValuesFrom params) of@@ -774,13 +819,48 @@         Just argument -> case argument of             Empty -> error "Invalid State"             Value value -> Just value+{-# DEPRECATED lookupArgument "Use queryArgument instead" #-}  {- |+In other applications, you want to gather up the remaining arguments on the+command-line. You need to have specified 'Remaining' in the configuration.++@+program = do+    files \<- 'queryRemaining'+    ...+@+-}+queryRemaining :: Program τ [Rope]+queryRemaining = do+    context <- ask+    let params = commandLineFrom context+    let remaining = remainingArgumentsFrom params+    pure (fmap intoRope remaining)++{- | Look to see if the user supplied a valued option and if so, what its value-was.+was. Use of the @LambdaCase@ extension might make accessing the parameter a+bit eaiser:++@+program = do+    count \<- 'queryOptionValue' \"count\" '>>=' \\case+        'Nothing' -> 'pure' 0+        'Just' value -> 'pure' value+    ...+@ -}+queryOptionValue :: LongName -> Program τ (Maybe Rope)+queryOptionValue name = do+    context <- ask+    let params = commandLineFrom context+    case lookupKeyValue name (parameterValuesFrom params) of+        Nothing -> pure Nothing+        Just argument -> case argument of+            Empty -> pure (Just emptyRope)+            Value value -> pure (Just (intoRope value)) --- Should this be more severe if it encounters Empty? lookupOptionValue :: LongName -> Parameters -> Maybe String lookupOptionValue name params =     case lookupKeyValue name (parameterValuesFrom params) of@@ -788,24 +868,47 @@         Just argument -> case argument of             Empty -> Nothing             Value value -> Just value+{-# DEPRECATED lookupOptionValue "Use queryOptionValue instead" #-}  {- |-Returns @Just True@ if the option is present, and @Nothing@ if it is not.+Returns @True@ if the option is present, and @False@ if it is not.++@+program = do+    overwrite \<- 'queryOptionValue' \"overwrite\"+    ...+@ -}+queryOptionFlag :: LongName -> Program τ Bool+queryOptionFlag name = do+    context <- ask+    let params = commandLineFrom context+    case lookupKeyValue name (parameterValuesFrom params) of+        Nothing -> pure False+        Just _ -> pure True --- The type is boolean to support a possible future extension of negated--- arguments. lookupOptionFlag :: LongName -> Parameters -> Maybe Bool lookupOptionFlag name params =     case lookupKeyValue name (parameterValuesFrom params) of         Nothing -> Nothing         Just argument -> case argument of             _ -> Just True -- nom, nom+{-# DEPRECATED lookupOptionFlag "Use queryOptionFlag instead" #-}  {- | Look to see if the user supplied the named environment variable and if so, return what its value was. -}+queryEnvironmentValue :: LongName -> Program τ (Maybe Rope)+queryEnvironmentValue name = do+    context <- ask+    let params = commandLineFrom context+    case lookupKeyValue name (environmentValuesFrom params) of+        Nothing -> error "Attempted lookup of unconfigured environment variable"+        Just param -> case param of+            Empty -> pure Nothing+            Value str -> pure (Just (intoRope str))+ lookupEnvironmentValue :: LongName -> Parameters -> Maybe String lookupEnvironmentValue name params =     case lookupKeyValue name (environmentValuesFrom params) of@@ -813,6 +916,7 @@         Just param -> case param of             Empty -> Nothing             Value str -> Just str+{-# DEPRECATED lookupEnvironmentValue "Use queryEnvironment instead" #-}  {- | Illegal internal state resulting from what should be unreachable code or
lib/Core/Program/Logging.hs view
@@ -140,6 +140,11 @@     debug,     debugS,     debugR,++    -- internal+    internal,+    isEvent,+    isDebug, ) where  import Chrono.TimeStamp (TimeStamp (..), getCurrentTimeNanoseconds)@@ -167,6 +172,7 @@     | SeverityWarn     | SeverityInfo     | SeverityDebug+    | SeverityInternal  putMessage :: Context τ -> Message -> IO () putMessage context (Message now level text possiblelValue) = do@@ -204,12 +210,13 @@         -- I hate doing math in Haskell         !elapsed = fromRational (toRational (now' - start') / 1e9) :: Fixed E3 -        !color = case severity of+        !colour = case severity of             SeverityNone -> emptyRope             SeverityCritical -> intoEscapes pureRed             SeverityWarn -> intoEscapes pureYellow             SeverityInfo -> intoEscapes dullWhite             SeverityDebug -> intoEscapes pureGrey+            SeverityInternal -> intoEscapes dullBlue          !reset = intoEscapes resetColour      in mconcat@@ -218,7 +225,7 @@             , " ("             , padWithZeros 6 (show elapsed)             , ") "-            , color+            , colour             , message             , reset             ]@@ -448,3 +455,13 @@             let value = render columns thing             !value' <- evaluate value             putMessage context (Message now SeverityDebug label (Just value'))++internal :: Rope -> Rope -> Program τ ()+internal label value = do+    context <- ask+    liftIO $ do+        level <- readMVar (verbosityLevelFrom context)+        when (isDebug level) $ do+            now <- getCurrentTimeNanoseconds+            !value' <- evaluate value+            putMessage context (Message now SeverityInternal (label <> value') Nothing)