Turtle

The Shell type represents a stream of values. You can think of Shell as [] + IO + Managed.

newtype Shell a = Shell { _foldIO :: forall r . FoldM IO a r -> IO r }

You invoke any external shell commands using proc or shell. If you don’t care about throwing an error instead of returning the error code you would use procs and shells; proc(s) is more secure but it won’t do any shell string interpolation.

shell
    :: Text        -- Command line
    -> Shell Line  -- Lines of standard input to feed to program
    -> io ExitCode

shells :: Text-> Shell Line -> io ()
select ::        [a] -> Shell a
liftIO ::      IO a  -> Shell a
using  :: Managed a  -> Shell a

-- usual construction primitive
empty :: Shell a

-- consume the stream by printing it to stdout
view   :: Show a => Shell a -> IO ()
stdout :: Shell Text -> IO ()

-- consume the (side-effect) stream, discarding any unused values
sh :: MonadIO io => Shell a -> io ()

You can simulate piping the result of a command with inshell or inproc:

inshell :: Text -> Shell Line -> Shell Line

inproc "curl" ["-s"
              , "http://"
              ] empty (1)
    & output "filename.ext" (2)
1 keep the result of a command as a stream
2 pipe and copy
When using inshell you lose the ability to care about the exit code of the command that produces the stream.

Shell is also an instance of MonadPlus (and thus Alternative). So you can concatenate two Shell streams using <|>.

Folding

Whenever you peek into the value of a shell stream using you are effectively looping over all values (as the list monad does). For instance this code is bogus :

bogus
do
  found <- testpath =<< find (prefix (text "/home/vagrant/zsh")) "/home/vagrant"
  unless found $ ...

You will need to consume the stream and one good way to do so is using fold from the foldl package:

import qualified Control.Foldl as Fold

main = do
  not_found <- fold (find (prefix (text "/home/vagrant/zsh")) "/home/vagrant") Fold.null
  when (not_found) $ do ...

Similarly here is an utility function that checks if a file is empty:

isFileEmpty :: MonadIO io => FilePath -> io Bool
isFileEmpty path =
  fold (input path) Fold.null

FilePath

Turtle is using the deprecated system-filepath package to handle filepaths in a more secure way[1]. Watch out as it is at time a bit surprising:

let first = "/home/vagrant" :: FilePath
     second = "/plugin" (1)
     test = first <> second -- "/plugin"

eclipseVersion = "4.5" (2)
let fp = "foo" </> "bar_" <> eclipseVersion </> "plugin" -> foo/bar_/4.5/plugin (3)
1 don’t start with a / as it means you want to concatenate an absolute path
2 give you an filepath type automatically thanks to the IsString instance
3 in system-filepath <> and </> are both alias for append

When appending filepath and text the best strategy is probably to keep the filepath encoding and then convert to text if necessary:

let path = "foo" </> fromText eclipseVersion </> "plugin"
    _path = format fp path
Use </> for appending filepaths, use <> for appending text.

Command line options

data Command
  = Console
  | Stack (Maybe StackName, StackCommand)
  deriving (Show)

commandParser :: Parser Command
commandParser =
      Console <$  subcommand "console" "Help msg" (pure ())
  <|> Stack   <$> subcommand "stack" "Help msg" stackParser (1)
1 remaining parser (after 'stack')

When using a group you will need a single datatype to extract the value of the rest of the command. Don’t do this:

data Command = Stack Int Text

commandParser :: Parser Command
commandParser = Stack <$> subcommand "stack" "Help" intParser <*> textParser

1. a mental model that might help is to look at each filepath as being a list of string not just one string