I am writing a shell script in Haskell using turtle
and would like to know best practices on composing commands that could fail.
Now I have a case expression staircase, like so:
runRemote :: MonadIO io => Text -> Text -> io ()
runRemote oldVersion' newVersion' = sh $ do
mkdir "out"
e1 <- shell ("command " <> oldVersion') empty
case e1 of
ExitFailure n -> cleanup
ExitSuccess -> do
e2 <- shell ("command " <> newVersion') empty
case e2 of
ExitFailure n -> cleanup
ExitSuccess -> do
curDir <- pwd
cd (curDir <.> oldVersion')
e3 <- shell ("command something else") empty
case e3 of
-- ...
-- And so on...
If the case
expression was expanding on a Maybe
type, the solution would be to derive a Monad
instance.
Is there a special reason the library author didn't already derive a Monad
instance for ExitCode
or is there a better way to do error handling for Haskell shell code?
One alternative is using (.&&.)
and (.||.)
from Turtle.Prelude
.
(.&&.) :: Monad m => m ExitCode -> m ExitCode -> m ExitCode
Analogous to
&&
in BashRuns the second command only if the first one returns
ExitSuccess
(.||.) :: Monad m => m ExitCode -> m ExitCode -> m ExitCode
Analogous to
||
in BashRun the second command only if the first one returns
ExitFailure
They allow you to chain your commands like this (note that everything involved must return an ExitCode
, including the cleanup):
(command1 .&&. command2) .||. cleanup
Or, if you need different cleanup actions in each case:
(command1 .||. cleanup1) .&&. (command2 .||. cleanup2)
By the way, it is worth noting that ExitCode
is not defined by turtle but rather by base, in the System.Exit
module.
ExitCode
is not a monad, and is not a monad transformer. A monad needs to take a type argument, and a monad transformer needs to take two. ExitCode
takes none. Now suppose we ignore that not-so-little problem for a bit. Can you come up with a meaningful interpretation of
join :: ExitCode (ExitCode a) -> ExitCode a
Yeah, I can't either. You could argue reasonably that shell
should instead produce Either FailureCode ()
, or perhaps work in ExceptT FailureCode IO
, but the library authors may have thought that too confusing or inflexible for the job.
You can use MaybeT
to avoid staircasing this way:
{-# LANGUAGE OverloadedStrings #-}
import Turtle
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
check io = do ec <- lift io
MaybeT $ case ec of
ExitSuccess -> return (Just True)
_ -> return Nothing
checkShell a b = check (shell a b)
main = do
dostuff
putStrLn "cleaning up"
dostuff = runMaybeT $ do
checkShell "date" empty
checkShell "/usr/bin/false" empty
checkShell "pwd" empty
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With