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 ExitCodeAnalogous to
&&in BashRuns the second command only if the first one returns
ExitSuccess
(.||.) :: Monad m => m ExitCode -> m ExitCode -> m ExitCodeAnalogous 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