{-# LANGUAGE NamedFieldPuns #-}

-- |
-- Module      : CLI.Parsing
-- License     : BSD3
-- Maintainer  : tbidne@gmail.com
-- Handles parsing of ['String'] args into 'Env'.
module CLI.Parsing
  ( parseArgs,
  )
where

import CLI.Parsing.Internal
import CLI.Types.Env
import Common.Parsing.Core
import qualified Control.Exception as Ex
import qualified Data.Map.Strict as M

-- | Maps parsed ['String'] args into 'IO' 'Right' 'Env', returning
-- any errors as `Left` `String`. All arguments are optional
-- (i.e. an empty list is valid), but if any are provided then they must
-- be valid or an error will be returned. Valid arguments are:
--
-- @
--   --legend=\<string>\
--       Path to the legend file.
--
--   --timeout=\<seconds\>
--       Non-negative integer. If we reach the timeout then all remaining
--       commands will be cancelled. If no timeout is given then the
--       timeout is infinite, i.e., we will keep running until all
--       commands have finished.
--
--   \<string\>
--       Any other string is considered a command. If the command has
--       whitespace then it must be quoted or it will be considered
--       a separate command.
-- @
parseArgs :: [String] -> IO (Either ParseErr Env)
parseArgs :: [String] -> IO (Either ParseErr Env)
parseArgs args :: [String]
args =
  case [String] -> ParseAnd Acc
pureParseArgs [String]
args of
    ParseAnd (PFailure (Help _)) -> Either ParseErr Env -> IO (Either ParseErr Env)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Either ParseErr Env -> IO (Either ParseErr Env))
-> Either ParseErr Env -> IO (Either ParseErr Env)
forall a b. (a -> b) -> a -> b
$ ParseErr -> Either ParseErr Env
forall a b. a -> Either a b
Left (ParseErr -> Either ParseErr Env)
-> ParseErr -> Either ParseErr Env
forall a b. (a -> b) -> a -> b
$ String -> ParseErr
Help String
help
    ParseAnd (PFailure (Err arg :: String
arg)) ->
      Either ParseErr Env -> IO (Either ParseErr Env)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Either ParseErr Env -> IO (Either ParseErr Env))
-> Either ParseErr Env -> IO (Either ParseErr Env)
forall a b. (a -> b) -> a -> b
$ ParseErr -> Either ParseErr Env
forall a b. a -> Either a b
Left (ParseErr -> Either ParseErr Env)
-> ParseErr -> Either ParseErr Env
forall a b. (a -> b) -> a -> b
$ String -> ParseErr
Err (String -> ParseErr) -> String -> ParseErr
forall a b. (a -> b) -> a -> b
$ "Could not parse `" String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String
arg String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "`. Try --help."
    ParseAnd (PSuccess acc :: Acc
acc) -> Acc -> IO (Either ParseErr Env)
accToEnv Acc
acc

accToEnv :: Acc -> IO (Either ParseErr Env)
accToEnv :: Acc -> IO (Either ParseErr Env)
accToEnv Acc {Maybe String
accLegend :: Acc -> Maybe String
accLegend :: Maybe String
accLegend, Maybe (RNonNegative Int)
accTimeout :: Acc -> Maybe (RNonNegative Int)
accTimeout :: Maybe (RNonNegative Int)
accTimeout, [Text]
accCommands :: Acc -> [Text]
accCommands :: [Text]
accCommands} = 
  case Maybe String
accLegend of
    Nothing -> Either ParseErr Env -> IO (Either ParseErr Env)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Either ParseErr Env -> IO (Either ParseErr Env))
-> Either ParseErr Env -> IO (Either ParseErr Env)
forall a b. (a -> b) -> a -> b
$ Env -> Either ParseErr Env
forall a b. b -> Either a b
Right (Env -> Either ParseErr Env) -> Env -> Either ParseErr Env
forall a b. (a -> b) -> a -> b
$ Map Text Text -> Maybe (RNonNegative Int) -> [Text] -> Env
Env Map Text Text
forall k a. Map k a
M.empty Maybe (RNonNegative Int)
accTimeout [Text]
accCommands
    Just path :: String
path -> do
      Either SomeException String
res <- IO String -> IO (Either SomeException String)
forall e a. Exception e => IO a -> IO (Either e a)
Ex.try (String -> IO String
readFile String
path) :: IO (Either Ex.SomeException String)
      Either ParseErr Env -> IO (Either ParseErr Env)
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Either ParseErr Env -> IO (Either ParseErr Env))
-> Either ParseErr Env -> IO (Either ParseErr Env)
forall a b. (a -> b) -> a -> b
$ case Either SomeException String
res of
        Left err :: SomeException
err -> ParseErr -> Either ParseErr Env
forall a b. a -> Either a b
Left (ParseErr -> Either ParseErr Env)
-> ParseErr -> Either ParseErr Env
forall a b. (a -> b) -> a -> b
$ String -> ParseErr
Err (String -> ParseErr) -> String -> ParseErr
forall a b. (a -> b) -> a -> b
$ "Error reading file " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> String -> String
forall a. Show a => a -> String
show String
path String -> String -> String
forall a. Semigroup a => a -> a -> a
<> ": " String -> String -> String
forall a. Semigroup a => a -> a -> a
<> (SomeException -> String
forall a. Show a => a -> String
show SomeException
err)
        Right contents :: String
contents -> [Text] -> Maybe (RNonNegative Int) -> String -> Either ParseErr Env
mapStrToEnv [Text]
accCommands Maybe (RNonNegative Int)
accTimeout String
contents

help :: String
help :: String
help =
  "\nUsage: cli-utils run-sh \"command 1\" \"command 2\" ... [OPTIONS]\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "Runs shell commands concurrently. Stdout is swallowed, so there is currently\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "no point to performing commands whose only effect is printing to stdout.\n\nOptions:\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "  --timeout=<seconds>\tNon-Negative integer. If we reach the timeout then all remaining\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tcommands will be cancelled. If no timeout is given then the timeout is\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tinfinite, i.e., we will keep running until all commands have finished.\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "  --legend=<string>\tPath to a legend file.\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tLines are formatted <cmd_key>=<command value> (no angle brackets).\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tEach line can be separated by as many new lines as desired, and comment lines start\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\twith a #. Command values themselves can include multiple commands delimited by\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tcommas, and they may reference other commands. For instance, given a legend file:\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tcmd1=echo \"command one\"\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\t# recursive references\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tcmd2=cmd1\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tcmd3=cmd2\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tcmd4=command four\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\t# runs 3 and 4\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tall=cmd3,cmd4,echo hi\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\tThen the command\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\t\tcli-utils run-sh --legend=path/to/legend all \"echo cat\"\n\n"
    String -> String -> String
forall a. Semigroup a => a -> a -> a
<> "\t\t\twill run `echo \"command one\"`, `command four`, `echo hi` and `echo cat` concurrently."