Options.hs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. {-# LANGUAGE PatternGuards #-}
  2. {-# LANGUAGE ScopedTypeVariables #-}
  3. {-# LANGUAGE CPP #-}
  4. module Options (injectDefaults) where
  5. import Control.Applicative
  6. import qualified Control.Exception as E
  7. import Control.Monad
  8. import Control.Monad.Trans.Except
  9. import Control.Monad.Trans.Reader
  10. import Data.Char (isAlphaNum, isSpace, toLower)
  11. import Data.List (foldl')
  12. import Data.List.Split (splitOn)
  13. import qualified Data.Map as M
  14. import Data.Maybe (mapMaybe)
  15. import Data.Monoid
  16. import Options.Applicative
  17. import Options.Applicative.Types
  18. import System.Directory
  19. import System.Environment
  20. import System.FilePath ((</>))
  21. -- | inject defaults from either files or environments
  22. -- in order of priority:
  23. -- 1. command line arguments: --long-option=value
  24. -- 2. environment variables: PREFIX_COMMAND_LONGOPTION=value
  25. -- 3. $HOME/.prefix/config: prefix.command.longoption=value
  26. --
  27. -- note: this automatically injects values for standard options and flags
  28. -- (also inside subcommands), but not for more complex parsers that use BindP
  29. -- (like `many'). As a workaround a single special case is supported,
  30. -- for `many' arguments that generate a list of strings.
  31. injectDefaults :: String -- ^ prefix, program name
  32. -> [(String, a -> [String] -> a)] -- ^ append extra options for arguments that are lists of strings
  33. -> ParserInfo a -- ^ original parsers
  34. -> IO (ParserInfo a)
  35. injectDefaults prefix lenses parser = do
  36. e <- getEnvironment
  37. config <- (readFile . (</> "config") =<< getAppUserDataDirectory prefix)
  38. `E.catch` \(_::E.SomeException) -> return ""
  39. let env = M.fromList . filter ((==[prefix]) . take 1 . fst) $
  40. configLines config <> -- config first
  41. map (\(k,v) -> (splitOn "_" $ map toLower k, v)) e -- env vars override config
  42. p' = parser { infoParser = injectDefaultP env [prefix] (infoParser parser) }
  43. return $ foldl' (\p (key,l) -> fmap (updateA env key l) p) p' lenses
  44. updateA :: M.Map [String] String -> String -> (a -> [String] -> a) -> a -> a
  45. updateA env key upd a =
  46. case M.lookup (splitOn "." key) env of
  47. Nothing -> a
  48. Just v -> upd a (splitOn ":" v)
  49. -- | really simple key/value file reader: x.y = z -> (["x","y"],"z")
  50. configLines :: String -> [([String], String)]
  51. configLines = mapMaybe (mkLine . takeWhile (/='#')) . lines
  52. where
  53. trim = let f = reverse . dropWhile isSpace in f . f
  54. mkLine l | (k, '=':v) <- break (=='=') l = Just (splitOn "." (trim k), trim v)
  55. | otherwise = Nothing
  56. -- | inject the environment into the parser
  57. -- the map contains the paths with the value that's passed into the reader if the
  58. -- command line parser gives no result
  59. injectDefaultP :: M.Map [String] String -> [String] -> Parser a -> Parser a
  60. injectDefaultP _env _path n@(NilP{}) = n
  61. injectDefaultP env path p@(OptP o)
  62. | (Option (CmdReader cmds f) props) <- o =
  63. let cmdMap = M.fromList (map (\c -> (c, mkCmd c)) cmds)
  64. mkCmd cmd =
  65. let (Just parseri) = f cmd
  66. in parseri { infoParser = injectDefaultP env (path ++ [normalizeName cmd]) (infoParser parseri) }
  67. in OptP (Option (CmdReader cmds (`M.lookup` cmdMap)) props)
  68. | (Option (OptReader names (CReader _ rdr) _) _) <- o =
  69. p <|> either (const empty)
  70. pure
  71. (runExcept . msum $
  72. map (maybe (throwE $ ErrorMsg "Missing environment variable")
  73. (runReaderT (unReadM rdr))
  74. . getEnvValue env path)
  75. names)
  76. | (Option (FlagReader names a) _) <- o =
  77. p <|> if any ((==Just "1") . getEnvValue env path) names then pure a else empty
  78. | otherwise = p
  79. injectDefaultP env path (MultP p1 p2) =
  80. MultP (injectDefaultP env path p1) (injectDefaultP env path p2)
  81. injectDefaultP env path (AltP p1 p2) =
  82. AltP (injectDefaultP env path p1) (injectDefaultP env path p2)
  83. injectDefaultP _env _path b@(BindP {}) = b
  84. getEnvValue :: M.Map [String] String -> [String] -> OptName -> Maybe String
  85. getEnvValue env path (OptLong l) = M.lookup (path ++ [normalizeName l]) env
  86. getEnvValue _ _ _ = Nothing
  87. normalizeName :: String -> String
  88. normalizeName = map toLower . filter isAlphaNum