packages feed

opt-env-conf 0.11.1.0 → 0.12.1.0

raw patch · 13 files changed

+567/−291 lines, 13 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- OptEnvConf.Run: internalParser :: Parser a -> Parser (Internal a)
- OptEnvConf.Run: runParser :: Version -> String -> Parser a -> IO a
- OptEnvConf.Run: runSettingsParser :: HasParser a => Version -> String -> IO a
+ OptEnvConf.Check: instance GHC.Base.Functor OptEnvConf.Check.CheckResult
+ OptEnvConf.Check: instance GHC.Generics.Generic (OptEnvConf.Check.CheckResult a)
+ OptEnvConf.Check: instance GHC.Generics.Generic OptEnvConf.Check.MissingCapability
+ OptEnvConf.Check: instance GHC.Show.Show OptEnvConf.Check.MissingCapability
+ OptEnvConf.Check: instance GHC.Show.Show a => GHC.Show.Show (OptEnvConf.Check.CheckResult a)
+ OptEnvConf.Check: runSettingsCheck :: Capabilities -> Parser a -> Args -> EnvMap -> Maybe Object -> IO void
+ OptEnvConf.Check: runSettingsCheckOn :: Capabilities -> TerminalCapabilities -> Parser a -> Args -> EnvMap -> Maybe Object -> IO (CheckResult a)
+ OptEnvConf.Error: ParseErrorMissingCapability :: !Capability -> ParseErrorMessage
+ OptEnvConf.Main: internalParser :: Parser a -> Parser (Internal a)
+ OptEnvConf.Main: runParser :: Version -> String -> Parser a -> IO a
+ OptEnvConf.Main: runSettingsParser :: HasParser a => Version -> String -> IO a
+ OptEnvConf.Output: capabilitiesChunks :: Set Capability -> [Chunk]
+ OptEnvConf.Output: capabilityChunk :: Capability -> Chunk
+ OptEnvConf.Parser: Capability :: Text -> Capability
+ OptEnvConf.Parser: [unCapability] :: Capability -> Text
+ OptEnvConf.Parser: instance Data.String.IsString OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: instance Data.Validity.Validity OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: instance GHC.Classes.Eq OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: instance GHC.Classes.Ord OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: instance GHC.Generics.Generic OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: instance GHC.Show.Show OptEnvConf.Parser.Capability
+ OptEnvConf.Parser: newtype Capability
+ OptEnvConf.Parser: readSecretCapability :: String
+ OptEnvConf.Parser: requireCapability :: HasCallStack => String -> Parser a -> Parser a
+ OptEnvConf.Run: Capabilities :: Set Capability -> Capabilities
+ OptEnvConf.Run: [unCapabilities] :: Capabilities -> Set Capability
+ OptEnvConf.Run: allCapabilities :: Capabilities
+ OptEnvConf.Run: disableCapability :: Capability -> Capabilities -> Capabilities
+ OptEnvConf.Run: enableCapability :: Capability -> Capabilities -> Capabilities
+ OptEnvConf.Run: instance Control.Monad.IO.Class.MonadIO OptEnvConf.Run.PP
+ OptEnvConf.Run: instance Control.Monad.Reader.Class.MonadReader OptEnvConf.Run.PPEnv OptEnvConf.Run.PP
+ OptEnvConf.Run: instance Control.Monad.State.Class.MonadState OptEnvConf.Run.PPState OptEnvConf.Run.PP
+ OptEnvConf.Run: instance Control.Selective.Selective OptEnvConf.Run.PP
+ OptEnvConf.Run: instance Data.Validity.Validity OptEnvConf.Run.Capabilities
+ OptEnvConf.Run: instance GHC.Base.Applicative OptEnvConf.Run.PP
+ OptEnvConf.Run: instance GHC.Base.Functor OptEnvConf.Run.PP
+ OptEnvConf.Run: instance GHC.Base.Monad OptEnvConf.Run.PP
+ OptEnvConf.Run: instance GHC.Generics.Generic OptEnvConf.Run.Capabilities
+ OptEnvConf.Run: instance GHC.Show.Show OptEnvConf.Run.Capabilities
+ OptEnvConf.Run: newtype Capabilities
- OptEnvConf.Parser: [ParserCheck] :: forall a1 a. !Maybe SrcLoc -> !Bool -> !a1 -> IO (Either String a) -> !Parser a1 -> Parser a
+ OptEnvConf.Parser: [ParserCheck] :: forall a1 a. !Maybe SrcLoc -> !Bool -> !Set Capability -> !a1 -> IO (Either String a) -> !Parser a1 -> Parser a
- OptEnvConf.Run: runParserOn :: Maybe TerminalCapabilities -> Parser a -> Args -> EnvMap -> Maybe Object -> IO (Either (NonEmpty ParseError) a)
+ OptEnvConf.Run: runParserOn :: Capabilities -> Maybe TerminalCapabilities -> Parser a -> Args -> EnvMap -> Maybe Object -> IO (Either (NonEmpty ParseError) a)

Files

CHANGELOG.md view
@@ -1,5 +1,14 @@ # Changelog +## [0.12.0.0] - 2025-11-25++### Added++* Support for capabilities during settings parsing, and disabling them in the+  settings check.++This is technically a breaking change, but if you don't use any `opt-env-conf`+internals, nothing should break for you.  ## [0.11.1.0] - 2025-10-23 
opt-env-conf.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           opt-env-conf-version:        0.11.1.0+version:        0.12.1.0 synopsis:       Settings parsing for Haskell: command-line arguments, environment variables, and configuration values. homepage:       https://github.com/NorfairKing/opt-env-conf#readme bug-reports:    https://github.com/NorfairKing/opt-env-conf/issues@@ -27,12 +27,14 @@       OptEnvConf       OptEnvConf.Args       OptEnvConf.Casing+      OptEnvConf.Check       OptEnvConf.Completer       OptEnvConf.Completion       OptEnvConf.Doc       OptEnvConf.EnvMap       OptEnvConf.Error       OptEnvConf.Lint+      OptEnvConf.Main       OptEnvConf.Nix       OptEnvConf.NonDet       OptEnvConf.Output
src/OptEnvConf.hs view
@@ -150,6 +150,7 @@ import OptEnvConf.Casing import OptEnvConf.Completer import OptEnvConf.Doc+import OptEnvConf.Main import OptEnvConf.Nix import OptEnvConf.Parser import OptEnvConf.Reader
+ src/OptEnvConf/Check.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module OptEnvConf.Check+  ( runSettingsCheck,+    runSettingsCheckOn,+  )+where++import qualified Data.Aeson as JSON+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Maybe+import GHC.Generics (Generic)+import GHC.Stack (SrcLoc)+import OptEnvConf.Args as Args+import OptEnvConf.EnvMap (EnvMap (..))+import OptEnvConf.Error+import OptEnvConf.Parser+import OptEnvConf.Run+import OptEnvConf.Terminal (getTerminalCapabilitiesFromHandle)+import System.Exit+import System.IO (stderr, stdout)+import Text.Colour++runSettingsCheck :: Capabilities -> Parser a -> Args -> EnvMap -> Maybe JSON.Object -> IO void+runSettingsCheck capabilities p args envVars mConfig = do+  stderrTc <- getTerminalCapabilitiesFromHandle stderr+  errOrSets <- runSettingsCheckOn capabilities stderrTc p args envVars mConfig+  case errOrSets of+    CheckFailed errs -> do+      hPutChunksLocaleWith stderrTc stderr $ renderErrors errs+      exitFailure+    CheckIncapable missingCaps -> do+      tc <- getTerminalCapabilitiesFromHandle stdout+      hPutChunksLocaleWith tc stdout ["Could not complete parsing settings because of missing capabilities, but no errors were found so far."]+      hPutChunksLocaleWith stderrTc stderr $ renderMissingCapabilities missingCaps+      exitSuccess+    CheckSucceeded _ -> do+      tc <- getTerminalCapabilitiesFromHandle stdout+      hPutChunksLocaleWith tc stdout ["Settings parsed successfully."]+      exitSuccess++renderMissingCapabilities :: NonEmpty MissingCapability -> [Chunk]+renderMissingCapabilities = renderErrors . NE.map capabilityErorr+  where+    capabilityErorr (MissingCapability mLoc cap) =+      ParseError mLoc $ ParseErrorMissingCapability cap++data CheckResult a+  = -- | Check succeeded+    CheckSucceeded a+  | -- | Check could not be completed because of missing capability+    CheckIncapable (NonEmpty MissingCapability)+  | -- | Check failed with parse errors+    CheckFailed (NonEmpty ParseError)+  deriving (Show, Generic, Functor)++data MissingCapability+  = MissingCapability+      -- Where the capability was needed+      !(Maybe SrcLoc)+      -- Where the capability was needed+      !Capability+  deriving (Show, Generic)++runSettingsCheckOn ::+  Capabilities ->+  -- DebugMode, always on+  TerminalCapabilities ->+  Parser a ->+  Args ->+  EnvMap ->+  Maybe JSON.Object ->+  IO (CheckResult a)+runSettingsCheckOn capabilities debugMode p args envVars mConfig = do+  errOrSets <- runParserOn capabilities (Just debugMode) p args envVars mConfig+  pure $ case errOrSets of+    Right a -> CheckSucceeded a+    Left errs ->+      let missingCaps =+            mapMaybe+              ( \case+                  ParseError mLoc (ParseErrorMissingCapability cap) -> Just (MissingCapability mLoc cap)+                  _ -> Nothing+              )+              (NE.toList errs)+       in case NE.nonEmpty missingCaps of+            Just ne -> CheckIncapable ne+            Nothing -> CheckFailed errs
src/OptEnvConf/Completion.hs view
@@ -284,7 +284,7 @@           Nothing -> pure Nothing           Just os -> fmap (os ++) <$> go p       ParserAllOrNothing _ p -> go p-      ParserCheck _ _ _ p -> go p+      ParserCheck _ _ _ _ p -> go p       ParserWithConfig _ p1 p2 ->         -- The config-file auto-completion is probably less important, we put it second.         andCompletions p2 p1
src/OptEnvConf/Doc.hs view
@@ -172,7 +172,7 @@       ParserMany _ p -> AnyDocsOr [go p, AnyDocsSingle Nothing]       ParserSome mLoc p -> AnyDocsAnd [go p, go (ParserMany mLoc p)] -- TODO: is this right?       ParserAllOrNothing _ p -> go p-      ParserCheck _ _ _ p -> go p+      ParserCheck _ _ _ _ p -> go p       ParserCommands _ mDefault cs -> AnyDocsCommands mDefault $ map commandParserDocs cs       ParserWithConfig _ p1 p2 -> AnyDocsAnd [go p1, go p2] -- TODO: is this right? Maybe we want to document that it's not a pure parser?       ParserSetting _ set -> AnyDocsSingle $ settingSetDoc set
src/OptEnvConf/Error.hs view
@@ -12,6 +12,7 @@ import GHC.Stack (SrcLoc) import OptEnvConf.Doc import OptEnvConf.Output+import OptEnvConf.Parser import OptEnvConf.Setting import Text.Colour @@ -39,6 +40,7 @@   | ParseErrorUnrecognisedCommand !String ![CommandDoc ()]   | ParseErrorAllOrNothing !(Map SettingHash SrcLoc)   | ParseErrorUnrecognised !(NonEmpty String)+  | ParseErrorMissingCapability !Capability   deriving (Show)  -- | Whether the other side of an 'Alt' should be tried if we find this error.@@ -64,6 +66,7 @@   ParseErrorUnrecognisedCommand _ _ -> False   ParseErrorAllOrNothing _ -> False   ParseErrorUnrecognised _ -> False+  ParseErrorMissingCapability _ -> False  eraseErrorSrcLocs :: (Functor f) => f ParseError -> f ParseError eraseErrorSrcLocs = fmap eraseErrorSrcLoc@@ -131,7 +134,9 @@           ]             ++ map (pure . srcLocChunk) (M.elems locs)         ParseErrorUnrecognised leftovers ->-          ["Unrecognised args: " : unwordsChunks (map (pure . chunk . T.pack) (NE.toList leftovers))],+          ["Unrecognised args: " : unwordsChunks (map (pure . chunk . T.pack) (NE.toList leftovers))]+        ParseErrorMissingCapability cap ->+          ["Missing capability: " : [chunk $ T.pack $ show cap]],       maybe [] (pure . ("see " :) . pure . srcLocChunk) parseErrorSrcLoc     ] 
src/OptEnvConf/Lint.hs view
@@ -271,7 +271,7 @@             validationTFailure LintErrorManyInfinite         pure c       ParserAllOrNothing _ p -> go p-      ParserCheck _ _ _ p -> go p+      ParserCheck _ _ _ _ p -> go p       ParserCommands mLoc mDefault cs -> do         if null cs           then validationTFailure $ LintError mLoc LintErrorNoCommands
+ src/OptEnvConf/Main.hs view
@@ -0,0 +1,305 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}++module OptEnvConf.Main+  ( runSettingsParser,+    runParser,+    internalParser,+  )+where++import qualified Data.Text as T+import Data.Version+import OptEnvConf.Args as Args+import OptEnvConf.Check+import OptEnvConf.Completion+import OptEnvConf.Doc+import qualified OptEnvConf.EnvMap as EnvMap+import OptEnvConf.Error+import OptEnvConf.Lint+import OptEnvConf.Nix+import OptEnvConf.Parser+import OptEnvConf.Reader+import OptEnvConf.Run+import OptEnvConf.Setting+import OptEnvConf.Terminal (getTerminalCapabilitiesFromHandle)+import Path+import System.Environment (getArgs, getEnvironment, getProgName)+import System.Exit+import System.IO+import Text.Colour++-- | Run 'runParser' on your @Settings@' type's 'settingsParser'.+--+-- __This is most likely the function you want to be using.__+runSettingsParser ::+  (HasParser a) =>+  -- | Program version, get this from Paths_your_package_name+  Version ->+  -- | Program description+  String ->+  IO a+runSettingsParser version progDesc =+  runParser version progDesc settingsParser++-- | Run a parser+--+-- This function with exit on:+--+--     * Parse failure: show a nice error message.+--     * @-h|--help@: Show help text+--     * @--version@: Show version information+--     * @--render-man-page@: Render a man page+--     * @--bash-completion-script@: Render a bash completion script+--     * @--zsh-completion-script@: Render a zsh completion script+--     * @--fish-completion-script@: Render a fish completion script+--     * @query-opt-env-conf-completion@: Perform a completion query+--+-- This gets the arguments and environment variables from the current process.+runParser ::+  -- | Program version, get this from Paths_your_package_name+  Version ->+  -- | Program description+  String ->+  Parser a ->+  IO a+runParser version progDesc p = do+  completeEnv <- getEnvironment+  let envVars = EnvMap.parse completeEnv++  case lintParser p of+    Just errs -> do+      tc <- getTerminalCapabilitiesFromHandle stderr+      hPutChunksLocaleWith tc stderr $ renderLintErrors errs+      exitFailure+    Nothing -> do+      let docs = parserDocs p++      allArgs <- getArgs+      let (debugMode, args) = consumeDebugMode allArgs++      mDebugMode <-+        if debugMode+          then Just <$> getTerminalCapabilitiesFromHandle stderr+          else pure Nothing++      let (helpMode, args') = consumeHelpMode args++      if helpMode+        then do+          progname <- getProgName+          errOrDocs <- runHelpParser mDebugMode (Args.parseArgs args') p+          case errOrDocs of+            Left errs -> do+              stderrTc <- getTerminalCapabilitiesFromHandle stderr+              hPutChunksLocaleWith stderrTc stderr $ renderErrors errs+              exitFailure+            Right mCommandDoc -> do+              tc <- getTerminalCapabilitiesFromHandle stdout+              hPutChunksLocaleWith tc stdout $ case mCommandDoc of+                Nothing -> renderHelpPage progname version progDesc docs+                Just (path, cDoc) -> renderCommandHelpPage progname path cDoc+              exitSuccess+        else do+          let (capabilities, args'') = consumeCapabilities args'+          let (checkMode, args''') = consumeCheckMode args''++          let readyArgs = Args.parseArgs args'''++          let mConfig = Nothing -- We start with no config loaded.+          if checkMode+            then runSettingsCheck capabilities p readyArgs envVars mConfig+            else do+              let p' = internalParser p+              errOrResult <-+                runParserOn+                  capabilities+                  mDebugMode+                  p'+                  readyArgs+                  envVars+                  mConfig+              case errOrResult of+                Left errs -> do+                  tc <- getTerminalCapabilitiesFromHandle stderr+                  hPutChunksLocaleWith tc stderr $ renderErrors errs+                  exitFailure+                Right i -> case i of+                  ShowVersion -> do+                    progname <- getProgName+                    tc <- getTerminalCapabilitiesFromHandle stdout+                    hPutChunksLocaleWith tc stdout $ renderVersionPage progname version+                    exitSuccess+                  RenderMan -> do+                    progname <- getProgName+                    tc <- getTerminalCapabilitiesFromHandle stdout+                    hPutChunksLocaleWith tc stdout $ renderManPage progname version progDesc docs+                    exitSuccess+                  RenderDocumentation -> do+                    progname <- getProgName+                    tc <- getTerminalCapabilitiesFromHandle stdout+                    hPutChunksLocaleWith tc stdout $ renderReferenceDocumentation progname docs+                    exitSuccess+                  RenderNixosOptions -> do+                    putStrLn $ T.unpack $ renderParserNixOptions p'+                    exitSuccess+                  BashCompletionScript progPath -> do+                    progname <- getProgName+                    generateBashCompletionScript progPath progname+                    exitSuccess+                  ZshCompletionScript progPath -> do+                    progname <- getProgName+                    generateZshCompletionScript progPath progname+                    exitSuccess+                  FishCompletionScript progPath -> do+                    progname <- getProgName+                    generateFishCompletionScript progPath progname+                    exitSuccess+                  CompletionQuery enriched index ws -> do+                    runCompletionQuery p' enriched index ws+                    exitSuccess+                  ParsedNormally a -> pure a++-- We use [String] instead of [Args] because we want to remove these args, and act on them, before any real arg parsing happens.+consumeExactArg :: String -> [String] -> (Bool, [String])+consumeExactArg arg = go+  where+    go = \case+      [] -> (False, [])+      (x : xs)+        | x == arg -> (True, xs)+        | otherwise ->+            let (found, rest) = go xs+             in (found, x : rest)++consumeDebugMode :: [String] -> (Bool, [String])+consumeDebugMode = consumeExactArg "--debug-optparse"++-- Note: Here we only consume exact -h as an arg, not -h as part of another arg like -hu+consumeHelpMode :: [String] -> (Bool, [String])+consumeHelpMode as =+  let (found, as') = consumeExactArg "--help" as+   in if found+        then (True, as')+        else consumeExactArg "-h" as'++consumeCheckMode :: [String] -> (Bool, [String])+consumeCheckMode = consumeExactArg "--run-settings-check"++consumeCapabilities :: [String] -> (Capabilities, [String])+consumeCapabilities = go allCapabilities+  where+    go :: Capabilities -> [String] -> (Capabilities, [String])+    go caps = \case+      [] -> (caps, [])+      (x : xs) ->+        let t = T.pack x+         in case T.stripPrefix "--settings-capabilities-disable-" t of+              Just capName -> go (disableCapability (Capability capName) caps) xs+              Nothing -> case T.stripPrefix "--settings-capabilities-enable-" t of+                Just capName -> go (enableCapability (Capability capName) caps) xs+                Nothing ->+                  let (finalCaps, rest) = go caps xs+                   in (finalCaps, x : rest)++-- Internal structure to help us do what the framework+-- is supposed to.+data Internal a+  = ShowVersion+  | RenderMan+  | RenderDocumentation+  | RenderNixosOptions+  | BashCompletionScript (Path Abs File)+  | ZshCompletionScript (Path Abs File)+  | FishCompletionScript (Path Abs File)+  | CompletionQuery+      -- Enriched+      !Bool+      -- Index+      !Int+      -- Args+      ![String]+  | ParsedNormally !a++internalParser :: Parser a -> Parser (Internal a)+internalParser p =+  choice+    [ setting+        [ switch ShowVersion,+          long "version",+          hidden+        ],+      setting+        [ switch RenderMan,+          long "render-man-page",+          hidden,+          help "Render a manpage"+        ],+      setting+        [ switch RenderDocumentation,+          long "render-reference-documentation",+          hidden,+          help "Render reference documentation"+        ],+      setting+        [ switch RenderNixosOptions,+          long "render-nix-options",+          hidden,+          help "Render Nix options"+        ],+      BashCompletionScript+        <$> setting+          [ option,+            reader $ maybeReader parseAbsFile,+            long "bash-completion-script",+            hidden,+            help "Render the bash completion script"+          ],+      ZshCompletionScript+        <$> setting+          [ option,+            reader $ maybeReader parseAbsFile,+            long "zsh-completion-script",+            hidden,+            help "Render the zsh completion script"+          ],+      ZshCompletionScript+        <$> setting+          [ option,+            reader $ maybeReader parseAbsFile,+            long "fish-completion-script",+            hidden,+            help "Render the fish completion script"+          ],+      setting+        [ help "Query completion",+          switch CompletionQuery,+          -- Long string that no normal user would ever use.+          long "query-opt-env-conf-completion",+          hidden+        ]+        <*> setting+          [ switch True,+            long "completion-enriched",+            value False,+            hidden,+            help "Whether to enable enriched completion"+          ]+        <*> setting+          [ option,+            reader auto,+            long "completion-index",+            hidden,+            help "The index between the arguments where completion was invoked."+          ]+        <*> many+          ( setting+              [ option,+                reader str,+                long "completion-word",+                hidden,+                help "The words (arguments) that have already been typed"+              ]+          ),+      ParsedNormally <$> p+    ]
src/OptEnvConf/Nix.hs view
@@ -39,7 +39,7 @@       ParserMany _ p -> go p       ParserSome _ p -> go p       ParserAllOrNothing _ p -> go p-      ParserCheck _ _ _ p -> go p+      ParserCheck _ _ _ _ p -> go p       ParserCommands _ _ cs -> M.unionsWith combineOption $ map goCommand cs       ParserWithConfig _ p1 p2 ->         -- I'm not sure if we need the first as well because you wouldn't use a
src/OptEnvConf/Output.hs view
@@ -6,6 +6,8 @@ import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as NE import Data.Maybe+import Data.Set (Set)+import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import Data.Version@@ -79,6 +81,14 @@  syntaxChunk :: String -> Chunk syntaxChunk = fore blue . chunk . T.pack++capabilitiesChunks :: Set Capability -> [Chunk]+capabilitiesChunks caps = case Set.toList caps of+  [] -> []+  cs -> intersperse ", " (map capabilityChunk cs)++capabilityChunk :: Capability -> Chunk+capabilityChunk = fore green . chunk . unCapability  mSrcLocChunk :: Maybe SrcLoc -> Chunk mSrcLocChunk = maybe "without srcLoc" srcLocChunk
src/OptEnvConf/Parser.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-}@@ -25,6 +28,7 @@     checkMapIOForgivable,     checkMapMaybeForgivable,     allOrNothing,+    requireCapability,     commands,     command,     defaultCommand,@@ -56,9 +60,11 @@     readSecretTextFile,     secretTextFileSetting,     secretTextFileOrBareSetting,+    readSecretCapability,      -- * Parser implementation     Parser (..),+    Capability (..),     HasParser (..),     Command (..),     CommandsBuilder (..),@@ -93,10 +99,14 @@ import Data.Map (Map) import qualified Data.Map as M import Data.Maybe+import Data.Set (Set)+import qualified Data.Set as Set import Data.String import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.IO as T+import Data.Validity+import GHC.Generics (Generic) import GHC.Stack (HasCallStack, SrcLoc, callStack, getCallStack, withFrozenCallStack) import OptEnvConf.Args (Dashed (..), prefixDashed) import OptEnvConf.Casing@@ -191,6 +201,9 @@     !(Maybe SrcLoc) ->     -- | Forgivable     !Bool ->+    -- | Necessary capabilities+    !(Set Capability) ->+    -- | Check function     !(a -> IO (Either String b)) ->     !(Parser a) ->     Parser b@@ -213,6 +226,20 @@     !(Setting a) ->     Parser a +newtype Capability = Capability {unCapability :: Text}+  deriving stock (Generic)+  deriving newtype (Show, Eq, Ord, IsString)++instance Validity Capability++-- | The annotation for any setting reading secrets.+--+-- We add these so that we can disable them in settings checks, to avoid+-- failing settings checks when secrets are read at runtime instead of+-- build-time.+readSecretCapability :: String+readSecretCapability = "read-secret"+ instance Functor Parser where   -- We case-match to produce shallower parser structures.   fmap f = \case@@ -221,11 +248,11 @@     ParserSelect pe pf -> ParserSelect (fmap (fmap f) pe) (fmap (fmap f) pf)     ParserEmpty mLoc -> ParserEmpty mLoc     ParserAlt p1 p2 -> ParserAlt (fmap f p1) (fmap f p2)-    ParserCheck mLoc forgivable g p -> ParserCheck mLoc forgivable (fmap (fmap f) . g) p+    ParserCheck mLoc forgivable caps g p -> ParserCheck mLoc forgivable caps (fmap (fmap f) . g) p     ParserCommands mLoc mDefault cs -> ParserCommands mLoc mDefault $ map (fmap f) cs     ParserWithConfig mLoc pc pa -> ParserWithConfig mLoc pc (fmap f pa)-    -- TODO: make setting a functor and fmap here-    p -> ParserCheck Nothing True (pure . Right . f) p+    -- If we ever make Setting a functor, then we need to fmap here+    p -> ParserCheck Nothing True Set.empty (pure . Right . f) p  instance Applicative Parser where   pure = ParserPure@@ -250,7 +277,7 @@           ParserMany _ p -> isEmpty p           ParserSome _ p -> isEmpty p           ParserAllOrNothing _ p -> isEmpty p-          ParserCheck _ _ _ p -> isEmpty p+          ParserCheck _ _ _ _ p -> isEmpty p           ParserCommands _ _ cs -> null cs           ParserWithConfig _ pc ps -> isEmpty pc && isEmpty ps           ParserSetting _ _ -> False@@ -331,12 +358,14 @@             . showsPrec 11 mLoc             . showString " "             . go 11 p-      ParserCheck mLoc forgivable _ p ->+      ParserCheck mLoc forgivable caps _ p ->         showParen (d > 10) $           showString "Check "             . showsPrec 11 mLoc             . showString " "             . showsPrec 11 forgivable+            . showString " "+            . showsPrec 11 caps             . showString " _ "             . go 11 p       ParserCommands mLoc mDefault cs ->@@ -599,11 +628,11 @@  -- | Check a 'Parser' after the fact, purely. checkMapEither :: (HasCallStack) => (a -> Either String b) -> Parser a -> Parser b-checkMapEither func p = withFrozenCallStack $ checkMapIO (pure . func) p+checkMapEither f = withFrozenCallStack $ checkMapIO (pure . f)  -- | Check a 'Parser' after the fact, allowing IO. checkMapIO :: (HasCallStack) => (a -> IO (Either String b)) -> Parser a -> Parser b-checkMapIO = ParserCheck mLoc False+checkMapIO = ParserCheck mLoc False Set.empty   where     mLoc = snd <$> listToMaybe (getCallStack callStack) @@ -634,6 +663,26 @@   where     mLoc = snd <$> listToMaybe (getCallStack callStack) +-- | Require the given capability for running the given parser.+--+-- This lets us annotate a parser with a power that it needs to be executed so+-- that we can run a settings check without that power to still check the rest+-- of the parser.+--+-- This only makes sense when used with a 'checkMap', 'checkEither', 'mapIO',+-- 'runIO', or similar function because the capability requirement is attached+-- to the Check node in the parser tree.+-- When used without such a node, this will attach a no-op Check node to the+-- parser tree and as such still try to do all the parsing below but not above.+requireCapability :: (HasCallStack) => String -> Parser a -> Parser a+requireCapability capName = \case+  ParserCheck mLoc' forgivable caps f p ->+    ParserCheck mLoc' forgivable (Set.insert cap caps) f p+  p -> ParserCheck mLoc False (Set.singleton cap) (pure . Right) p+  where+    cap = Capability (T.pack capName)+    mLoc = snd <$> listToMaybe (getCallStack callStack)+ -- | Like 'checkMapMaybe', but allow trying the other side of any alternative if the result is Nothing. checkMapMaybeForgivable :: (HasCallStack) => (a -> Maybe b) -> Parser a -> Parser b checkMapMaybeForgivable func p =@@ -647,12 +696,11 @@  -- | Like 'checkMapEither', but allow trying the other side of any alternative if the result is Nothing. checkMapEitherForgivable :: (HasCallStack) => (a -> Either String b) -> Parser a -> Parser b-checkMapEitherForgivable func p = withFrozenCallStack $ checkMapIOForgivable (pure . func) p+checkMapEitherForgivable f = withFrozenCallStack $ checkMapIOForgivable (pure . f)  -- | Like 'checkMapIO', but allow trying the other side of any alternative if the result is Nothing.--- TODO add a SRCLoc here checkMapIOForgivable :: (HasCallStack) => (a -> IO (Either String b)) -> Parser a -> Parser b-checkMapIOForgivable = ParserCheck mLoc True+checkMapIOForgivable = ParserCheck mLoc True Set.empty   where     mLoc = snd <$> listToMaybe (getCallStack callStack) @@ -964,7 +1012,11 @@  -- | Load a secret from a text file, with 'readSecretTextFile' secretTextFileSetting :: (HasCallStack) => [Builder FilePath] -> Parser Text-secretTextFileSetting bs = withFrozenCallStack $ mapIO readSecretTextFile $ filePathSetting bs+secretTextFileSetting bs =+  withFrozenCallStack $+    requireCapability readSecretCapability $+      mapIO readSecretTextFile $+        filePathSetting bs  -- | Load a secret from a text file, with 'readSecretTextFile', or specify it -- directly@@ -990,7 +1042,13 @@     fileSetting p f = do       let s = completeBuilder $ mconcat [mapMaybeBuilder f b, reader str, metavar "FILE_PATH"]       guard $ p s-      pure $ mapIO (resolveFile' >=> readSecretTextFile) $ ParserSetting mLoc s+      pure $+        requireCapability readSecretCapability $+          -- These two mapIOs are not combined because the capability should+          -- only apply to the reading, not the resolving.+          mapIO readSecretTextFile $+            mapIO resolveFile' $+              ParserSetting mLoc s      bareOption = bareSetting settingTryOption $ \case       BuildTryArgument -> Nothing@@ -1117,7 +1175,7 @@       ParserMany _ p -> ParserMany Nothing (go p)       ParserSome _ p -> ParserSome Nothing (go p)       ParserAllOrNothing _ p -> ParserAllOrNothing Nothing (go p)-      ParserCheck _ forgivable f p -> ParserCheck Nothing forgivable f (go p)+      ParserCheck _ forgivable caps f p -> ParserCheck Nothing forgivable caps f (go p)       ParserCommands _ mDefault cs -> ParserCommands Nothing mDefault $ map commandEraseSrcLocs cs       ParserWithConfig _ p1 p2 -> ParserWithConfig Nothing (go p1) (go p2)       ParserSetting _ s -> ParserSetting Nothing s@@ -1154,7 +1212,7 @@       ParserMany mLoc p -> ParserMany mLoc <$> go p       ParserSome mLoc p -> ParserSome mLoc <$> go p       ParserAllOrNothing mLoc p -> ParserAllOrNothing mLoc <$> go p-      ParserCheck mLoc forgivable f p -> ParserCheck mLoc forgivable f <$> go p+      ParserCheck mLoc forgivable caps f p -> ParserCheck mLoc forgivable caps f <$> go p       ParserCommands mLoc mDefault cs -> ParserCommands mLoc mDefault <$> traverse (commandTraverseSetting func) cs       ParserWithConfig mLoc p1 p2 -> ParserWithConfig mLoc <$> go p1 <*> go p2       ParserSetting mLoc s -> ParserSetting mLoc <$> func s@@ -1183,7 +1241,7 @@       ParserMany _ p -> go p       ParserSome _ p -> go p       ParserAllOrNothing _ p -> go p -- TODO is this right?-      ParserCheck _ _ _ p -> go p+      ParserCheck _ _ _ _ p -> go p       ParserCommands _ _ cs -> M.unions $ map (go . commandParser) cs       ParserWithConfig _ p1 p2 -> M.union (go p1) (go p2)       -- The nothing part shouldn't happen but I don't know when it doesn't
src/OptEnvConf/Run.hs view
@@ -1,15 +1,20 @@ {-# LANGUAGE ApplicativeDo #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}  module OptEnvConf.Run-  ( runSettingsParser,-    runParser,+  ( Capabilities (..),+    allCapabilities,+    enableCapability,+    disableCapability,     runParserOn,     runHelpParser,-    internalParser,   ) where @@ -29,282 +34,52 @@ import Data.Map (Map) import qualified Data.Map as M import Data.Maybe+import Data.Set (Set)+import qualified Data.Set as Set import qualified Data.Text as T import Data.Traversable-import Data.Version+import Data.Validity (Validity)+import GHC.Generics (Generic) import GHC.Stack (SrcLoc) import OptEnvConf.Args as Args-import OptEnvConf.Completion import OptEnvConf.Doc import OptEnvConf.EnvMap (EnvMap (..)) import qualified OptEnvConf.EnvMap as EnvMap import OptEnvConf.Error-import OptEnvConf.Lint-import OptEnvConf.Nix import OptEnvConf.NonDet import OptEnvConf.Output import OptEnvConf.Parser import OptEnvConf.Reader import OptEnvConf.Setting-import OptEnvConf.Terminal (getTerminalCapabilitiesFromHandle) import OptEnvConf.Validation-import Path-import System.Environment (getArgs, getEnvironment, getProgName)-import System.Exit import System.IO import Text.Colour --- | Run 'runParser' on your @Settings@' type's 'settingsParser'.------ __This is most likely the function you want to be using.__-runSettingsParser ::-  (HasParser a) =>-  -- | Program version, get this from Paths_your_package_name-  Version ->-  -- | Program description-  String ->-  IO a-runSettingsParser version progDesc =-  runParser version progDesc settingsParser---- | Run a parser------ This function with exit on:------     * Parse failure: show a nice error message.---     * @-h|--help@: Show help text---     * @--version@: Show version information---     * @--render-man-page@: Render a man page---     * @--bash-completion-script@: Render a bash completion script---     * @--zsh-completion-script@: Render a zsh completion script---     * @--fish-completion-script@: Render a fish completion script---     * @query-opt-env-conf-completion@: Perform a completion query------ This gets the arguments and environment variables from the current process.-runParser ::-  -- | Program version, get this from Paths_your_package_name-  Version ->-  -- | Program description-  String ->-  Parser a ->-  IO a-runParser version progDesc p = do-  allArgs <- getArgs-  let argMap' = parseArgs allArgs-  let mArgMap = consumeSwitch ["--debug-optparse"] argMap'-  let (debugMode, argMap) = case mArgMap of-        Nothing -> (False, argMap')-        Just am -> (True, am)--  completeEnv <- getEnvironment-  let envVars = EnvMap.parse completeEnv--  case lintParser p of-    Just errs -> do-      tc <- getTerminalCapabilitiesFromHandle stderr-      hPutChunksLocaleWith tc stderr $ renderLintErrors errs-      exitFailure-    Nothing -> do-      let docs = parserDocs p--      mDebugMode <--        if debugMode-          then Just <$> getTerminalCapabilitiesFromHandle stderr-          else pure Nothing+-- Set of disabled capabilities+newtype Capabilities = Capabilities {unCapabilities :: Set Capability}+  deriving (Show, Generic) -      let mHelpConsumed = consumeSwitch ["-h", "--help"] argMap-      let (helpMode, args') = case mHelpConsumed of-            Nothing -> (False, argMap)-            Just am -> (True, am)+instance Validity Capabilities -      if helpMode-        then do-          progname <- getProgName-          errOrDocs <- runHelpParser mDebugMode args' p-          case errOrDocs of-            Left errs -> do-              stderrTc <- getTerminalCapabilitiesFromHandle stderr-              hPutChunksLocaleWith stderrTc stderr $ renderErrors errs-              exitFailure-            Right mCommandDoc -> do-              tc <- getTerminalCapabilitiesFromHandle stdout-              hPutChunksLocaleWith tc stdout $ case mCommandDoc of-                Nothing -> renderHelpPage progname version progDesc docs-                Just (path, cDoc) -> renderCommandHelpPage progname path cDoc-              exitSuccess-        else do-          let mCheckConsumed = consumeSwitch ["--run-settings-check"] args'-          let (checkMode, args) = case mCheckConsumed of-                Nothing -> (False, args')-                Just am -> (True, am)+allCapabilities :: Capabilities+allCapabilities = Capabilities {unCapabilities = Set.empty} -          if checkMode-            then do-              stderrTc <- getTerminalCapabilitiesFromHandle stderr-              errOrSets <- runParserOn (Just stderrTc) p args envVars Nothing-              case errOrSets of-                Left errs -> do-                  hPutChunksLocaleWith stderrTc stderr $ renderErrors errs-                  exitFailure-                Right _ -> do-                  tc <- getTerminalCapabilitiesFromHandle stdout-                  hPutChunksLocaleWith tc stdout ["Settings parsed successfully."]-                  exitSuccess-            else do-              let p' = internalParser p-              errOrResult <--                runParserOn-                  mDebugMode-                  p'-                  args-                  envVars-                  Nothing-              case errOrResult of-                Left errs -> do-                  tc <- getTerminalCapabilitiesFromHandle stderr-                  hPutChunksLocaleWith tc stderr $ renderErrors errs-                  exitFailure-                Right i -> case i of-                  ShowVersion -> do-                    progname <- getProgName-                    tc <- getTerminalCapabilitiesFromHandle stdout-                    hPutChunksLocaleWith tc stdout $ renderVersionPage progname version-                    exitSuccess-                  RenderMan -> do-                    progname <- getProgName-                    tc <- getTerminalCapabilitiesFromHandle stdout-                    hPutChunksLocaleWith tc stdout $ renderManPage progname version progDesc docs-                    exitSuccess-                  RenderDocumentation -> do-                    progname <- getProgName-                    tc <- getTerminalCapabilitiesFromHandle stdout-                    hPutChunksLocaleWith tc stdout $ renderReferenceDocumentation progname docs-                    exitSuccess-                  RenderNixosOptions -> do-                    putStrLn $ T.unpack $ renderParserNixOptions p'-                    exitSuccess-                  BashCompletionScript progPath -> do-                    progname <- getProgName-                    generateBashCompletionScript progPath progname-                    exitSuccess-                  ZshCompletionScript progPath -> do-                    progname <- getProgName-                    generateZshCompletionScript progPath progname-                    exitSuccess-                  FishCompletionScript progPath -> do-                    progname <- getProgName-                    generateFishCompletionScript progPath progname-                    exitSuccess-                  CompletionQuery enriched index ws -> do-                    runCompletionQuery p' enriched index ws-                    exitSuccess-                  ParsedNormally a -> pure a+enableCapability :: Capability -> Capabilities -> Capabilities+enableCapability cap (Capabilities caps) =+  Capabilities (Set.delete cap caps) --- Internal structure to help us do what the framework--- is supposed to.-data Internal a-  = ShowVersion-  | RenderMan-  | RenderDocumentation-  | RenderNixosOptions-  | BashCompletionScript (Path Abs File)-  | ZshCompletionScript (Path Abs File)-  | FishCompletionScript (Path Abs File)-  | CompletionQuery-      -- Enriched-      !Bool-      -- Index-      !Int-      -- Args-      ![String]-  | ParsedNormally !a+disableCapability :: Capability -> Capabilities -> Capabilities+disableCapability cap (Capabilities caps) =+  Capabilities (Set.insert cap caps) -internalParser :: Parser a -> Parser (Internal a)-internalParser p =-  choice-    [ setting-        [ switch ShowVersion,-          long "version",-          hidden-        ],-      setting-        [ switch RenderMan,-          long "render-man-page",-          hidden,-          help "Render a manpage"-        ],-      setting-        [ switch RenderDocumentation,-          long "render-reference-documentation",-          hidden,-          help "Render reference documentation"-        ],-      setting-        [ switch RenderNixosOptions,-          long "render-nix-options",-          hidden,-          help "Render Nix options"-        ],-      BashCompletionScript-        <$> setting-          [ option,-            reader $ maybeReader parseAbsFile,-            long "bash-completion-script",-            hidden,-            help "Render the bash completion script"-          ],-      ZshCompletionScript-        <$> setting-          [ option,-            reader $ maybeReader parseAbsFile,-            long "zsh-completion-script",-            hidden,-            help "Render the zsh completion script"-          ],-      ZshCompletionScript-        <$> setting-          [ option,-            reader $ maybeReader parseAbsFile,-            long "fish-completion-script",-            hidden,-            help "Render the fish completion script"-          ],-      setting-        [ help "Query completion",-          switch CompletionQuery,-          -- Long string that no normal user would ever use.-          long "query-opt-env-conf-completion",-          hidden-        ]-        <*> setting-          [ switch True,-            long "completion-enriched",-            value False,-            hidden,-            help "Whether to enable enriched completion"-          ]-        <*> setting-          [ option,-            reader auto,-            long "completion-index",-            hidden,-            help "The index between the arguments where completion was invoked."-          ]-        <*> many-          ( setting-              [ option,-                reader str,-                long "completion-word",-                hidden,-                help "The words (arguments) that have already been typed"-              ]-          ),-      ParsedNormally <$> p-    ]+missingCapabilities :: Capabilities -> Set Capability -> Maybe (NonEmpty Capability)+missingCapabilities (Capabilities caps) requiredCapabilities =+  NE.nonEmpty (Set.toList (Set.intersection requiredCapabilities caps))  -- | Run a parser on given arguments and environment instead of getting them -- from the current process. runParserOn ::+  Capabilities ->   -- DebugMode   Maybe TerminalCapabilities ->   Parser a ->@@ -312,7 +87,7 @@   EnvMap ->   Maybe JSON.Object ->   IO (Either (NonEmpty ParseError) a)-runParserOn mDebugMode parser args envVars mConfig = do+runParserOn capabilities mDebugMode parser args envVars mConfig = do   let ppState =         PPState           { ppStateArgs = args,@@ -344,6 +119,7 @@                 Nothing ->                   pure $                     Left $+                      -- Only show source locations in debug mode.                       let f = case mDebugMode of                             Nothing -> eraseErrorSrcLocs                             Just _ -> id@@ -425,21 +201,30 @@                   if null parsedSettingsMap                     then ppErrors' errs                     else ppErrors' $ errs <> (ParseError mLoc (ParseErrorAllOrNothing parsedSettingsMap) :| [])-      ParserCheck mLoc forgivable f p' -> do+      ParserCheck mLoc forgivable requiredCapabilities f p' -> do         debug [syntaxChunk "Parser with check", ": ", mSrcLocChunk mLoc]+        when (not (Set.null requiredCapabilities)) $+          debug $+            "Requires capabilities: "+              : capabilitiesChunks requiredCapabilities+         ppIndent $ do           debug ["parser"]+          -- Definitely parse below           a <- ppIndent $ go p'           debug ["check"]-          ppIndent $ do-            errOrB <- liftIO $ f a-            case errOrB of-              Left err -> do-                debug ["failed, forgivable: ", chunk $ T.pack $ show forgivable]-                ppError mLoc $ ParseErrorCheckFailed forgivable err-              Right b -> do-                debug ["succeeded"]-                pure b+          -- Only perform the check (IO) if capabilities are sufficient+          ppIndent $ case missingCapabilities capabilities requiredCapabilities of+            Just missings -> ppErrors mLoc $ NE.map ParseErrorMissingCapability missings+            Nothing -> do+              errOrB <- liftIO $ f a+              case errOrB of+                Left err -> do+                  debug ["failed, forgivable: ", chunk $ T.pack $ show forgivable]+                  ppError mLoc $ ParseErrorCheckFailed forgivable err+                Right b -> do+                  debug ["succeeded"]+                  pure b       ParserCommands mLoc mDefault cs -> do         debug [syntaxChunk "Commands", ": ", mSrcLocChunk mLoc]         forM_ mDefault $ \d -> debug ["default:", chunk $ T.pack $ show d]@@ -778,7 +563,7 @@             ParserAllOrNothing mLoc p' -> do               debug [syntaxChunk "AllOrNothing", ": ", mSrcLocChunk mLoc]               ppIndent $ go p'-            ParserCheck mLoc _ _ p' -> do+            ParserCheck mLoc _ _ _ p' -> do               debug [syntaxChunk "Parser with check", ": ", mSrcLocChunk mLoc]               ppIndent $ go p'             ParserWithConfig mLoc pc pa -> do@@ -811,14 +596,23 @@                           Nothing -> Just (reverse path, commandParserDocs c)                           Just res -> pure res -type PP a = ReaderT PPEnv (ValidationT ParseError (StateT PPState (NonDetT IO))) a+newtype PP a = PP (ReaderT PPEnv (ValidationT ParseError (StateT PPState (NonDetT IO))) a)+  deriving+    ( Functor,+      Applicative,+      Selective,+      Monad,+      MonadIO,+      MonadReader PPEnv,+      MonadState PPState+    )  runPP ::   PP a ->   PPState ->   PPEnv ->   IO [(Validation ParseError a, PPState)]-runPP p args envVars =+runPP (PP p) args envVars =   runNonDetT (runStateT (runValidationT (runReaderT p envVars)) args)  runPPLazy ::@@ -831,7 +625,7 @@           NonDetT IO (Validation ParseError a, PPState)         )     )-runPPLazy p args envVars =+runPPLazy (PP p) args envVars =   runNonDetTLazy (runStateT (runValidationT (runReaderT p envVars)) args)  tryPP :: PP a -> PP (Maybe a)@@ -851,7 +645,7 @@       pure $ Just a  ppNonDet :: NonDetT IO a -> PP a-ppNonDet = lift . lift . lift+ppNonDet = PP . lift . lift . lift  ppNonDetList :: [a] -> PP a ppNonDetList = ppNonDet . liftNonDetTList@@ -923,7 +717,7 @@       pure (Just ())  ppErrors' :: NonEmpty ParseError -> PP a-ppErrors' = lift . ValidationT . lift . pure . Failure+ppErrors' = PP . lift . ValidationT . lift . pure . Failure  ppErrors :: Maybe SrcLoc -> NonEmpty ParseErrorMessage -> PP a ppErrors mLoc = ppErrors' . NE.map (ParseError mLoc)