{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RankNTypes #-}

-- |
-- Module      : Language.Github.Actions.Shell
-- Description : GitHub Actions shell configuration for steps
-- Copyright   : (c) 2025 Bellroy Pty Ltd
-- License     : BSD-3-Clause
-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'Shell' type for specifying which shell to use when
-- running commands in GitHub Actions steps.
--
-- Different shells provide different capabilities and are available on different
-- operating systems. The shell can be specified at the workflow, job, or step level.
--
-- For more information about GitHub Actions shell configuration, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>
module Language.Github.Actions.Shell
  ( Platform (..),
    Shell (..),
    arguments,
    gen,
    supportedPlatforms,
  )
where

import Control.Monad.Fail.Hoist (hoistFail')
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as Aeson
import Data.List.NonEmpty (NonEmpty (..))
import Data.String.Interpolate (i)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

-- | Shell types available for running commands in GitHub Actions steps.
--
-- Each shell has different capabilities and platform availability:
--
-- * 'Bash' - Available on all platforms, most commonly used
-- * 'Sh' - POSIX sh, available on Linux and macOS only
-- * 'Python' - Executes commands as Python code
-- * 'Cmd' - Windows Command Prompt, Windows only
-- * 'Powershell' - Windows PowerShell 5.1, Windows only
-- * 'Pwsh' - PowerShell Core, available on all platforms
--
-- Example usage:
--
-- @
-- import Language.Github.Actions.Shell
--
-- -- Use bash with default options
-- bashShell :: Shell
-- bashShell = Bash Nothing
--
-- -- Use bash with specific options
-- bashWithOptions :: Shell
-- bashWithOptions = Bash (Just "--noprofile --norc")
--
-- -- Use PowerShell Core
-- pwshShell :: Shell
-- pwshShell = Pwsh Nothing
-- @
--
-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>
data Shell
  = -- | Bash shell with optional arguments
    Bash (Maybe Text)
  | -- | POSIX sh shell (Linux/macOS only) with optional arguments
    Sh (Maybe Text)
  | -- | Python interpreter with optional arguments
    Python (Maybe Text)
  | -- | Windows cmd.exe with optional arguments
    Cmd (Maybe Text)
  | -- | Windows PowerShell 5.1 with optional arguments
    Powershell (Maybe Text)
  | -- | PowerShell Core with optional arguments
    Pwsh (Maybe Text)
  deriving stock (Shell -> Shell -> Bool
(Shell -> Shell -> Bool) -> (Shell -> Shell -> Bool) -> Eq Shell
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Shell -> Shell -> Bool
== :: Shell -> Shell -> Bool
$c/= :: Shell -> Shell -> Bool
/= :: Shell -> Shell -> Bool
Eq, (forall x. Shell -> Rep Shell x)
-> (forall x. Rep Shell x -> Shell) -> Generic Shell
forall x. Rep Shell x -> Shell
forall x. Shell -> Rep Shell x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cfrom :: forall x. Shell -> Rep Shell x
from :: forall x. Shell -> Rep Shell x
$cto :: forall x. Rep Shell x -> Shell
to :: forall x. Rep Shell x -> Shell
Generic, Eq Shell
Eq Shell =>
(Shell -> Shell -> Ordering)
-> (Shell -> Shell -> Bool)
-> (Shell -> Shell -> Bool)
-> (Shell -> Shell -> Bool)
-> (Shell -> Shell -> Bool)
-> (Shell -> Shell -> Shell)
-> (Shell -> Shell -> Shell)
-> Ord Shell
Shell -> Shell -> Bool
Shell -> Shell -> Ordering
Shell -> Shell -> Shell
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: Shell -> Shell -> Ordering
compare :: Shell -> Shell -> Ordering
$c< :: Shell -> Shell -> Bool
< :: Shell -> Shell -> Bool
$c<= :: Shell -> Shell -> Bool
<= :: Shell -> Shell -> Bool
$c> :: Shell -> Shell -> Bool
> :: Shell -> Shell -> Bool
$c>= :: Shell -> Shell -> Bool
>= :: Shell -> Shell -> Bool
$cmax :: Shell -> Shell -> Shell
max :: Shell -> Shell -> Shell
$cmin :: Shell -> Shell -> Shell
min :: Shell -> Shell -> Shell
Ord, Int -> Shell -> ShowS
[Shell] -> ShowS
Shell -> String
(Int -> Shell -> ShowS)
-> (Shell -> String) -> ([Shell] -> ShowS) -> Show Shell
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Shell -> ShowS
showsPrec :: Int -> Shell -> ShowS
$cshow :: Shell -> String
show :: Shell -> String
$cshowList :: [Shell] -> ShowS
showList :: [Shell] -> ShowS
Show)

instance FromJSON Shell where
  parseJSON :: Value -> Parser Shell
parseJSON =
    String -> (Text -> Parser Shell) -> Value -> Parser Shell
forall a. String -> (Text -> Parser a) -> Value -> Parser a
Aeson.withText String
"Shell" ((Text -> Parser Shell) -> Value -> Parser Shell)
-> (Text -> Parser Shell) -> Value -> Parser Shell
forall a b. (a -> b) -> a -> b
$
      Either String Shell -> Parser Shell
forall (t :: * -> *) (m :: * -> *) a.
(PluckError String t m, MonadFail m) =>
t a -> m a
hoistFail' (Either String Shell -> Parser Shell)
-> (Text -> Either String Shell) -> Text -> Parser Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Either String Shell
parseShell

instance ToJSON Shell where
  toJSON :: Shell -> Value
toJSON = Text -> Value
Aeson.String (Text -> Value) -> (Shell -> Text) -> Shell -> Value
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Shell -> Text
renderShell

renderShell :: Shell -> Text
renderShell :: Shell -> Text
renderShell = \case
  Bash Maybe Text
args -> [i|bash#{maybe "" ((" " <>)) args}|]
  Sh Maybe Text
args -> [i|sh#{maybe "" ((" " <>)) args}|]
  Python Maybe Text
args -> [i|python#{maybe "" ((" " <>)) args}|]
  Cmd Maybe Text
args -> [i|cmd#{maybe "" ((" " <>)) args}|]
  Powershell Maybe Text
args -> [i|powershell#{maybe "" ((" " <>)) args}|]
  Pwsh Maybe Text
args -> [i|pwsh#{maybe "" ((" " <>)) args}|]

parseShell :: Text -> Either String Shell
parseShell :: Text -> Either String Shell
parseShell Text
t =
  case [Text]
tokens of
    Text
"bash" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Bash (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    Text
"cmd" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Cmd (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    Text
"powershell" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Powershell (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    Text
"pwsh" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Pwsh (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    Text
"python" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Python (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    Text
"sh" : [Text]
args -> Shell -> Either String Shell
forall a b. b -> Either a b
Right (Shell -> Either String Shell)
-> (Maybe Text -> Shell) -> Maybe Text -> Either String Shell
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Maybe Text -> Shell
Sh (Maybe Text -> Either String Shell)
-> Maybe Text -> Either String Shell
forall a b. (a -> b) -> a -> b
$ [Text] -> Maybe Text
maybeArgs [Text]
args
    [Text]
_ -> String -> Either String Shell
forall a b. a -> Either a b
Left [i|Unknown shell: #{t}|]
  where
    tokens :: [Text]
tokens = Text -> [Text]
Text.words Text
t
    maybeArgs :: [Text] -> Maybe Text
maybeArgs = \case
      [] -> Maybe Text
forall a. Maybe a
Nothing
      [Text]
args -> Text -> Maybe Text
forall a. a -> Maybe a
Just (Text -> Maybe Text) -> Text -> Maybe Text
forall a b. (a -> b) -> a -> b
$ [Text] -> Text
Text.unwords [Text]
args

gen :: (MonadGen m) => m Shell
gen :: forall (m :: * -> *). MonadGen m => m Shell
gen =
  [m Shell] -> m Shell
forall (m :: * -> *) a. MonadGen m => [m a] -> m a
Gen.choice
    [ Maybe Text -> Shell
Bash (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText,
      Maybe Text -> Shell
Sh (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText,
      Maybe Text -> Shell
Python (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText,
      Maybe Text -> Shell
Cmd (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText,
      Maybe Text -> Shell
Powershell (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText,
      Maybe Text -> Shell
Pwsh (Maybe Text -> Shell) -> m (Maybe Text) -> m Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> m Text -> m (Maybe Text)
forall (m :: * -> *) a. MonadGen m => m a -> m (Maybe a)
Gen.maybe m Text
genText
    ]
  where
    genText :: m Text
genText = Range Int -> m Char -> m Text
forall (m :: * -> *). MonadGen m => Range Int -> m Char -> m Text
Gen.text (Int -> Int -> Range Int
forall a. Integral a => a -> a -> Range a
Range.linear Int
1 Int
5) m Char
forall (m :: * -> *). MonadGen m => m Char
Gen.alphaNum

data Platform = Windows | MacOS | Linux

supportedPlatforms :: Shell -> NonEmpty Platform
supportedPlatforms :: Shell -> NonEmpty Platform
supportedPlatforms = \case
  Bash Maybe Text
_ -> Platform
Linux Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| [Platform
MacOS, Platform
Windows]
  Sh Maybe Text
_ -> Platform
MacOS Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| []
  Python Maybe Text
_ -> Platform
Linux Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| [Platform
MacOS, Platform
Windows]
  Cmd Maybe Text
_ -> Platform
Windows Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| []
  Powershell Maybe Text
_ -> Platform
Windows Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| []
  Pwsh Maybe Text
_ -> Platform
Windows Platform -> [Platform] -> NonEmpty Platform
forall a. a -> [a] -> NonEmpty a
:| []

-- | A lens into the arguments for a shell, compatible with the "lens" package
--
-- @
-- arguments :: Lens' Shell (Maybe Text)
-- @
arguments :: forall f. (Functor f) => (Maybe Text -> f (Maybe Text)) -> Shell -> f Shell
arguments :: forall (f :: * -> *).
Functor f =>
(Maybe Text -> f (Maybe Text)) -> Shell -> f Shell
arguments Maybe Text -> f (Maybe Text)
f = \case
  Bash Maybe Text
args -> Maybe Text -> Shell
Bash (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args
  Sh Maybe Text
args -> Maybe Text -> Shell
Sh (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args
  Python Maybe Text
args -> Maybe Text -> Shell
Python (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args
  Cmd Maybe Text
args -> Maybe Text -> Shell
Cmd (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args
  Powershell Maybe Text
args -> Maybe Text -> Shell
Powershell (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args
  Pwsh Maybe Text
args -> Maybe Text -> Shell
Pwsh (Maybe Text -> Shell) -> f (Maybe Text) -> f Shell
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> Maybe Text -> f (Maybe Text)
f Maybe Text
args